miti-loki

A Cloudflare Worker that forwards logs to My Grafana Cloud's Loki.

Usage

POST a JSON body to https://miti-loki.miti99.workers.dev/. GET redirects to this repo. Any other method returns 405; missing/empty body returns 400.

Single log entry

curl -X POST 'https://miti-loki.miti99.workers.dev/?app=demo&env=prod' \
  -H 'Content-Type: application/json' \
  -d '{"message": "Hello from miti-loki"}'

Batch (array of entries)

curl -X POST 'https://miti-loki.miti99.workers.dev/?app=demo' \
  -H 'Content-Type: application/json' \
  -d '[{"message":"first"},{"message":"second"}]'

Body schema

Each entry is {message, timestamp?, metadata?}:

  • message (string, required) — log line.
  • timestamp (string, optional) — Unix nanoseconds. Defaults to current time.
  • metadata (object, optional) — flat key-value pairs (no nested objects). Merged ON TOP of auto-injected metadata; caller wins on key collision.

Stream labels

URL query params become Loki stream labels. Label names must match [a-zA-Z_:][a-zA-Z0-9_:]* and cannot both start and end with _ (reserved). Invalid labels return 400.

Auto-injected labels (overwrite caller-supplied values on collision):

  • proxy=miti-loki
  • country — from request.cf.country (unknown if absent)
  • region — from request.cf.region (unknown if absent)
  • timezone — from request.cf.timezone (unknown if absent)

Breaking change (v2): ip was previously a stream label. It is now per-entry structured metadata (see below). Rewrite {ip="..."} queries as {proxy="miti-loki"} | ip="...".

Auto-injected per-entry metadata

Every log entry is enriched with these structured-metadata fields. Caller-supplied metadata.<key> wins on collision; missing values become string "unknown".

  • ip — from CF-Connecting-IP / X-Forwarded-For / X-Real-IP
  • user_agentUser-Agent header
  • cityrequest.cf.city
  • latitude, longituderequest.cf.latitude / .longitude
  • url — full request URL (incl. query string)
  • cf_rayCF-Ray header (per-request trace ID for CF support)
  • refererReferer header

LogQL examples

{proxy="miti-loki", country="VN"}                 # filter by low-cardinality label
{proxy="miti-loki"} | ip="1.2.3.4"                # filter by structured metadata
{proxy="miti-loki", app="demo"} | user_agent=~"curl/.*"

Errors

  • 400 — empty body, invalid JSON, missing message, nested metadata, or invalid label name.
  • 405 — non-POST/GET/OPTIONS method.
  • 500 — worker missing LOKI_HOST / LOKI_USERNAME / LOKI_PASSWORD env vars (deploy-time issue, not caller-fixable), or upstream fetch error.
  • Otherwise the response status, body, and content-type are passed through from Loki's /loki/api/v1/push.

Responses are CORS-permissive (Access-Control-Allow-Origin: *, POST, OPTIONS allowed).

Env vars (deploy-time)

  • LOKI_HOST — Loki host (e.g. logs-prod-XXX.grafana.net).
  • LOKI_USERNAME — Basic Auth user (Grafana Cloud instance ID).
  • LOKI_PASSWORD — Basic Auth password (Grafana Cloud API token).
  • LOKI_PORT (optional) — defaults to 443 (HTTPS). Any other value uses HTTP.

For AI agents / Claude Code routines

Read this section first, do not probe.

  • One POST per intended log batch. Do not send a "test" payload to verify the endpoint — every successful POST writes to Loki, so probes pollute the log stream.
  • Exact request:
    POST https://miti-loki.miti99.workers.dev/?<label>=<value>&...
    Content-Type: application/json
    {"message": "<your log>"}
    
  • Success: any 2xx (typically 204 from Loki). Auth-related errors: 500 (env vars missing — deploy-time issue).
  • Do not retry on 2xx. Each POST is a separate ingest; a retry would duplicate log lines.
S
Description
A Cloudflare Worker that forward message to my Grafana Cloud's Loki
Readme Apache-2.0 100 KiB
Languages
JavaScript 100%