Correlating LGTM telemetry in Grafana lets an operator move from a metric spike or log entry to the trace that handled the same request. The connection depends on shared fields, usually a trace ID and service name, appearing consistently in Loki logs and Tempo traces.
Tempo controls trace-to-log navigation from a span, while Loki controls log-to-trace navigation from a log line. Both data sources need matching settings, and the application or collector pipeline needs to place the same identifier in the records that reach each backend.
Keep high-cardinality values such as trace IDs out of Loki labels. Store them in the log message or structured metadata, then use a derived field or trace-to-logs query to link records without creating one stream per request.
$ GRAFANA_URL=http://127.0.0.1:3000
$ curl --silent \
--user admin:admin \
"$GRAFANA_URL/api/datasources"
[
{
"uid": "tempo",
"name": "Tempo",
"type": "tempo"
},
{
"uid": "loki",
"name": "Loki",
"type": "loki"
}
]
apiVersion: 1 datasources: - name: Tempo uid: tempo type: tempo access: proxy url: http://tempo:3200 jsonData: tracesToLogsV2: datasourceUid: loki spanStartTimeShift: "-2s" spanEndTimeShift: "2s" tags: - key: service.name value: service_name filterByTraceID: true filterBySpanID: false
The time shifts widen the Loki query around the span timestamps so small clock or write-order differences do not hide matching logs.
apiVersion: 1 datasources: - name: Loki uid: loki type: loki access: proxy url: http://loki:3100 jsonData: derivedFields: - name: TraceID matcherRegex: >- trace_id=([0-9a-fA-F]{32}) datasourceUid: tempo url: "$${__value.raw}"
The regular expression matches trace IDs in the log body. Keep the trace ID out of stream labels unless the Loki label design has been reviewed for cardinality.
$ docker compose up -d grafana Container lgtm-grafana-1 Recreate Container lgtm-grafana-1 Started
$ curl --silent --show-error --include --request POST http://127.0.0.1:4318/v1/traces \
--header 'Content-Type: application/json' \
--data @trace.json
HTTP/1.1 200 OK
{"partialSuccess":{}}
$ LOKI_URL=http://127.0.0.1:3100 $ curl --silent --show-error --request POST \ "$LOKI_URL/loki/api/v1/push" \ --header 'Content-Type: application/json' \ --data @loki-log.json
The log body should include a field such as trace_id=<trace-id>.
$ LOKI_URL=http://127.0.0.1:3100
$ TRACE_ID='<trace-id>'
$ curl --silent --get "$LOKI_URL/loki/api/v1/query_range" \
--data-urlencode \
"query={service_name=\"checkout-api\"} |= \"$TRACE_ID\""
{
"status": "success",
"data": {
"resultType": "streams",
"result": [
{
"stream": {"service_name": "checkout-api"},
"values": [
[
"1782008380038503000",
"checkout complete trace_id=5b8efff798..."
]
]
}
]
}
}
If no logs appear, check the time shifts, the selected Loki data source UID, and the label mapping between service.name and service_name.