Grafana Loki¶
Grafana Loki is a horizontally scalable, highly available log aggregation system designed to be cost-effective and easy to operate. SerOps uses it as a log provider supporting full-text search, regex queries, live tail, and multi-tenant isolation via the LogQL query language.
| Type | Logs |
| Auth | Basic / Bearer / Tenant ID |
| Query Language | LogQL |
Prerequisites¶
- Grafana Loki 2.x or later
- Network access from the SerOps backend to the Loki endpoint
- Valid credentials (basic auth or bearer token)
- Tenant ID if using multi-tenant mode
Configuration¶
{
"provider": "loki",
"display_name": "Grafana Cloud Loki",
"capabilities": ["logs"],
"host": "https://logs-prod.grafana.net",
"username": "user-id",
"extra_config": {
"bearer_token": "glc_xxx",
"tenant_id": "my-tenant"
}
}
Multi-Tenant Loki
If your Loki instance uses multi-tenancy, set the tenant_id in extra_config. SerOps sends it via the X-Scope-OrgID header.
Authentication Options¶
Loki supports multiple authentication methods:
- Basic Auth: Provide
usernameandpasswordat the top level. Common with Grafana Cloud where the username is your user ID and the password is a Grafana Cloud API key. - Bearer Token: Provide
bearer_tokeninextra_config. Useful for token-based authentication. - Tenant ID: When
tenant_idis set, SerOps includes theX-Scope-OrgIDheader on all requests to support multi-tenant deployments.
These can be combined (e.g., basic auth with a tenant ID).
Extra Configuration¶
| Field | Type | Description |
|---|---|---|
bearer_token |
string | Bearer token for authentication |
tenant_id |
string | Tenant ID for multi-tenant Loki; sent via X-Scope-OrgID header |
Kubernetes setup¶
Loki has a subtle limitation compared to Elasticsearch / Datadog / CloudWatch: stream selectors AND across labels and can't easily OR the same value against different label names. That means SerOps' adapter-side aliasing (Datadog USM service → service_name, OpenTelemetry service.name → service_name, Kubernetes app → service_name) works for logs from ES / DD / CW / OpenSearch — but NOT for Loki. Loki users must tag with SerOps' canonical names directly.
Recommended Promtail scrape config for a Kubernetes cluster. This block is validated end-to-end on Kubernetes 1.31 / containerd with Promtail 3.5 — copy it whole, and read the two notes below it before changing anything:
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
pipeline_stages:
- cri: {}
relabel_configs:
# service_name, in TWO rules. Order and `regex` both matter — see
# "Why the service_name rules look like that" below.
# Fallback first: pods labelled app.kubernetes.io/name.
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
regex: (.+)
target_label: service_name
# Canonical `app` second, so it wins when a pod carries both.
- source_labels: [__meta_kubernetes_pod_label_app]
regex: (.+)
target_label: service_name
# Namespace → environment (adjust to your convention).
- source_labels: [__meta_kubernetes_namespace]
target_label: environment
# Constant labels — replace with your actual org + project names.
- target_label: organisation_name
replacement: <YOUR-ORG>
- target_label: project_name
replacement: <YOUR-PROJECT>
# Pod + container labels for drill-down.
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
# REQUIRED — Promtail tails nothing without it. See "__path__ is
# not optional" below.
- source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]
separator: /
target_label: __path__
replacement: /var/log/pods/*$1/*.log
__path__ is not optional
Kubernetes service discovery finds pods; it does not tell Promtail
which file to read. Without the __path__ relabel, Promtail
discovers every target and then drops each one, logging
msg="no path for target". Its /ready endpoint returns 500
forever, the pod never becomes Ready, and Loki receives nothing at
all — no error surfaces in SerOps, because no data ever arrives.
Check it in one command:
Any output means __path__ is missing or wrong.
Why the service_name rules look like that
A relabel rule with action: replace (the default) writes an empty
value when its source label is absent, and Prometheus deletes a
label that is set to empty. Two consequences:
- Without
regex: (.+)on each rule, the second rule wipes theservice_namethe first one just set. A pod labelled onlyapp: checkoutends up with noservice_nameat all — and per the warning below, that means zero results in SerOps. - Order is reversed from what reads naturally. Relabel rules apply
top to bottom and the last writer wins, so the fallback goes
first and
appsecond. Putappfirst and a pod carrying both labels resolves to itsapp.kubernetes.io/namevalue instead.
Verify what actually landed, per service:
Alias limitation on Loki
If your Promtail config only sets service (Datadog USM style) or app (K8s convention) without also setting service_name, SerOps' LogQL queries will return zero results. Add the relabel step above to promote whichever label you have to service_name. Every other log provider handles this transparently — Loki is the exception because of how LogQL stream selectors work.
log_level is deliberately not a label
The config above sets four of SerOps' five canonical fields. The fifth,
log_level, is intentionally left out: severity changes on almost every
line, and promoting it to a stream label would multiply Loki's label
cardinality — the one thing Loki asks you not to do.
SerOps reads severity out of the log line instead, recognising bare
(ERROR ...), bracketed ([ERROR]), logfmt (level=error) and JSON
("level":"error") forms. Only the first ~200 characters are scanned,
so the level must appear in the line prefix — where every common
format puts it — and the word "error" inside a message body will not
mislabel an otherwise-INFO line.
If you do already have a low-cardinality log_level or level
label, SerOps prefers it over parsing the line.
Verify with the ingest validator:
The report shows whether Loki streams have service_name / project_name /
environment as labels. Note that log_level is reported from SerOps'
line parsing, not from a Loki label, so it reads as present even though the
config above never sets it.
Health Check¶
Response:
{
"integrations": [
{
"provider": "LokiLogProvider",
"provider_type": "log",
"healthy": true,
"message": "OK"
}
]
}
Troubleshooting¶
| Issue | Solution |
|---|---|
| "Connection refused" | Verify host URL is reachable from SerOps backend |
| "401 Unauthorized" | Check username/password or bearer_token are correct |
| "Tenant not found" | Verify tenant_id in extra_config matches an existing tenant |
| "No data returned" | Verify the time range has data; check that capabilities includes "logs" |