Grafana Loki Logs skill for Codex
Few labels, filter first, parse last. Codex checks label cardinality with logcli before writing a query, then builds LogQL from the cheapest stage up: tight stream selector, line filters, targeted parsing, label filters. It writes correct log-derived metrics with unwrap and quantile_over_time, ruler alerts for log-only events, and Alloy pipelines that keep request and trace IDs out of stream labels.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- loki-logs Installs to ~/.agents/skills/loki-logs/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install loki-logs The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project loki-logs Installs to .agents/skills/loki-logs/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/loki-logs.zipunzip -o loki-logs.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) loki-logs PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $loki-logs, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs Grafana Loki label schemes and writes LogQL that finds things fast - stream selectors on few low-cardinality labels, line filters before parsers, json and logfmt parsing, metric queries with rate, unwrap and quantile_over_time, ruler alerts, structured metadata, Grafana Alloy shipping and logcli. Use when querying or alerting on logs in Loki, when Loki queries time out or hit limits, when choosing labels for a new log pipeline, or when migrating from Promtail to Alloy.
Use it when
- You need to search, count or alert on logs stored in Loki
- Loki queries are slow or hit series and entry limits
- A new log pipeline needs a label scheme
- Promtail needs replacing with Alloy
Use something else when
- Logs live in Elasticsearch or OpenSearch: use elk-stack
- The application's log format itself needs fixing: use structured-logging
- Logs are in CloudWatch Logs: use aws-cloudwatch
How to check the result
Queries run with logcli --stats within acceptable bytes scanned, logcli series --analyze-labels shows stream counts within budget, and alert expressions fire over a past incident window.
The mistake it prevents
Promoting request IDs, user IDs or status codes to stream labels, which explodes stream count and slows every query.
Done well means: streams are selected by a handful of stable labels, queries narrow with cheap line filters before expensive parsing, metrics derived from logs are correct, and the label set stays small enough that ingestion and queries do not fall over.
Inspect first
- Read
AGENTS.md; find the shipping config (Alloy*.alloy, Promtailpromtail.yaml, Fluent Bit, OTel Collectorloki/otlphttpexporter) and the Loki version. - List labels and their cardinality before writing queries:
export LOKI_ADDR=http://loki-gateway.monitoring:80 # needs network to the cluster
logcli labels
logcli labels namespace
logcli series '{namespace="checkout"}' --analyze-labels --since=1h
- Look at a sample of raw lines to see the format (JSON, logfmt, plain):
logcli query '{namespace="checkout"}' --since=15m --limit=20.
Label design
Labels define streams; every unique label set is a separate stream and index entry.
| Good labels (bounded, describe the source) | Never labels (put in the line or structured metadata) |
|---|---|
cluster, namespace, app / service_name, env | user_id, request_id, trace_id |
container, job | status_code, path, ip |
level only if the shipper sets it reliably | timestamps, anything unbounded |
- Aim for well under 100k active streams per tenant and a handful of labels per stream; Loki rejects pushes beyond its per-stream label limit (15 by default).
- High-cardinality values that you filter on often (trace ID, user ID) go into structured metadata (Loki 3.x with the OTLP or Alloy pipeline), which can be filtered without being a stream label.
- Changing labels later splits history; decide them once per platform.
LogQL query order
Write queries from cheapest to most expensive:
{namespace="checkout", app="api"} # 1. stream selector: as specific as possible
|= "payment" # 2. line filters: substring is cheapest
!= "healthz"
|~ "timeout|deadline exceeded" # regex only when needed
| json # 3. parse only after narrowing
| status >= 500 and duration_ms > 1000 # 4. label filters on parsed fields
| line_format "{{.method}} {{.path}} {{.status}} {{.duration_ms}}ms"
- Parse only the fields you need:
| json status="response.status", path="request.path"is cheaper than full| jsonon wide lines. - Use
| logfmtforkey=valuelogs,| pattern "<ip> - - [<_>] \"<method> <path> <_>\" <status> <size>"for fixed formats, and| regexpas the last resort. - Filter parsing errors deliberately:
| json | __error__=""drops unparseable lines; look at them first with| __error__!=""to make sure you are not hiding real ones. - Structured metadata filters look like label filters after the selector:
{app="api"} | trace_id="4bf92f3577b34da6a3ce929d0e0e4736".
Metric queries
# error lines per second by app
sum by (app) (rate({namespace="checkout"} |= "level=error" [5m]))
# 5xx count by status over the last hour
sum by (status) (count_over_time({app="api"} | json | status >= 500 [1h]))
# p99 latency from a numeric field
quantile_over_time(0.99, {app="api"} | json | unwrap duration_ms | __error__="" [5m]) by (route)
# bytes per stream to find noisy sources
topk(10, sum by (app) (bytes_over_time({namespace=~".+"}[1h])))
rateandcount_over_timecount lines;unwrapturns a field into a sample value forsum_over_time,avg_over_time,quantile_over_time.- Always aggregate with
sum by (...)to keep result series bounded. - If you need this metric often, emit it as a Prometheus metric from the app instead; log-derived metrics are expensive to compute repeatedly.
Alerts from logs
Loki’s ruler evaluates LogQL in Prometheus rule format:
groups:
- name: checkout-logs
rules:
- alert: CheckoutPanicLogged
expr: sum(count_over_time({namespace="checkout"} |= "panic:" [5m])) > 0
for: 0m
labels: { severity: ticket }
annotations:
summary: "Panic logged in checkout"
runbook_url: "https://runbooks.example.com/checkout/panic"
Use log alerts for events that only exist in logs; prefer metrics for rates and SLOs.
See $alerting-rules for routing and severity.
Shipping with Alloy
Promtail is deprecated in favour of Grafana Alloy; new pipelines should use Alloy. Minimal Kubernetes pod logs to Loki:
discovery.kubernetes "pods" { role = "pod" }
discovery.relabel "pods" {
targets = discovery.kubernetes.pods.targets
rule { source_labels = ["__meta_kubernetes_namespace"] target_label = "namespace" }
rule { source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"] target_label = "app" }
rule { source_labels = ["__meta_kubernetes_pod_container_name"] target_label = "container" }
}
loki.source.kubernetes "pods" {
targets = discovery.relabel.pods.output
forward_to = [loki.process.drop_probes.receiver]
}
loki.process "drop_probes" {
stage.drop { expression = ".*(GET /healthz|GET /readyz).*" }
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint { url = "http://loki-gateway.monitoring/loki/api/v1/push" }
}
Check with alloy fmt and the Alloy UI’s component graph. alloy convert translates
Promtail configs as a starting point; review its output rather than trusting it blind.
Troubleshooting slow or failing queries
| Error or symptom | Cause | Fix |
|---|---|---|
max entries limit | Too many lines returned | Add filters or raise --limit only for export |
too many outstanding requests / timeouts | Broad selector over long range | Narrow labels and time; add line filter before parser |
maximum of series reached | Metric query grouped by high-cardinality field | Group by fewer labels |
| Empty result but logs exist | Wrong label name, or parse error filtered out | Check logcli series, inspect __error__ |
stream limit exceeded on push | Too many label combinations | Remove unbounded labels at the shipper |
Verification
- Run each query with
logcli query ... --stats(or Grafana’s query inspector) and check bytes processed; a dashboard query scanning tens of GB per refresh needs a narrower selector or a recording rule. - After label changes,
logcli series --analyze-labelsshows stream counts within budget. - For alerts, backfill-check by running the expression over a past incident window.
Output
Deliver the LogQL with a one-line explanation per stage, the label set with expected cardinality, and the shipper config diff when labels or drops changed.
Gotchas
|=is case-sensitive; use|~ "(?i)error"when case varies.levellabels added by some shippers guess from text and are often wrong; filter on parsed fields when it matters.- Logs arriving out of order beyond the accepted window are rejected; check clock sync on hosts.
- Retention is set per tenant or stream in Loki’s
limits_config, not per query; deleting logs uses the compactor’s delete API and is irreversible, so confirm with the user.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Grafana Loki Logs"
short_description: "Few labels, filter first, parse last"
default_prompt: "$loki-logs Write a LogQL query for checkout 5xx errors grouped by route over the last hour" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.