Tracing & Trace Viewer
This page explains how traceId and spanId flow through your services and how to use the built‑in trace viewer backed by Loki.
How trace IDs are created and propagated
When LokiLoggerModule.apply(app) is called, the module:
- Attaches middleware that:
- Reads
x-loki-trace-idandx-parent-span-idfrom the incoming request (if present). - Generates new hex
traceId/spanIdwhen needed. - Stores a full
TraceContextinAsyncLocalStoragefor the lifetime of the request.
- Reads
- Echoes the headers on the response:
x-loki-trace-idx-parent-span-id
- Uses that context in:
LokiLoggerService(all log lines).LokiHttpService(outgoing HTTP).TypeOrmLokiLogger(database logs).
At a high level, the request flow looks like this:
- The frontend sends
x-loki-trace-idto the gateway. - The gateway continues that trace and forwards both
x-loki-trace-idandx-parent-span-idto downstream services. - Each NestJS service writes structured logs to Loki with the same
traceId. - If OpenTelemetry is enabled, each service also exports spans to Tempo.
- The built-in trace viewer queries Loki by
traceIdand reconstructs the full request journey across services.
Trace context API
Inside your code, you can access the active trace using helpers from trace-context.ts:
import {
getCurrentTrace,
getCurrentTraceId,
getCurrentSpanId,
addTraceTag,
nextSequence,
} from "@planetmoondrop/centralized-logger";
const trace = getCurrentTrace(); // undefined outside a request
const traceId = getCurrentTraceId(); // 'no-trace' outside a request
const spanId = getCurrentSpanId(); // 'no-span' outside a requestaddTraceTag(key, value)Attaches arbitrary key–value tags to the current request; they are merged into all subsequent log lines.
addTraceTag("orderId", order.id);
addTraceTag("tenant", tenantId);These tags become top‑level JSON fields in Loki, which makes them first‑class for LogQL queries and Grafana dashboards.
Log types
Logs carry a logType field that helps the trace viewer group events:
http_in– incoming HTTP request.http_in_res– outgoing HTTP response.http_out– outgoing HTTP calls viaLokiHttpService.db– TypeORM SQL logs viaTypeOrmLokiLogger.interceptor– handler entry/exit via the global interceptor.service– manual logs and@Logdecorator output.event– domain events logged vialogger.event(...).
The viewer UI and Grafana dashboards rely on this to build the waterfall.
Enabling the trace viewer
1. Configure options
In your module registration:
LokiLoggerModule.register({
serviceName: "backend",
lokiHost: "http://loki:3100",
enableTraceViewer: true,
traceViewerPath: "/_trace",
traceViewerServices: "backend,auth-service,customer-support",
});enableTraceViewer: true– turn on the viewer.traceViewerPath– base path (default/_trace).traceViewerServices– comma‑separated list of services to query in Loki.
If enableTraceViewer is true but traceViewerServices is not set, only the current service’s logs are included and a warning is logged on startup.
2. Mount the viewer
In main.ts, call:
LokiLoggerModule.mountViewer(app);This registers:
GET /_trace→ HTML SPA that visualizes traces.GET /_trace/api/:traceId→ JSON trace, used by the SPA and integrators.
3. Querying a trace
The underlying TraceViewerService uses Loki’s HTTP API with a LogQL query:
{app=~"backend|auth-service|customer-support"} | json | traceId="<TRACE_ID>"The service:
- Fetches JSON logs from Loki for the last 24 hours.
- Groups them by
spanId. - Infers span trees via
parentSpanId. - Computes
startTime,endTime, anddurationMsfrom log timestamps.
The /api/:traceId endpoint returns:
interface TraceResult {
traceId: string;
startTime: string;
durationMs: number;
services: string[];
spans: Span[];
rootSpans: Span[];
}Each Span includes its own logs (with logType, message, duration, statusCode, etc.) so UIs can reconstruct the full journey.
Production notes
- Make sure your Loki retention (
retention_periodinloki-config.yaml) is aligned with how long you expect traces to be viewable. - The viewer queries up to 24h of data per request; extremely high‑volume systems may want to tune
max_entries_limit_per_queryin Loki. - If you expose the viewer publicly, protect it with auth in your gateway or API gateway layer – the library itself does not implement authentication.
Need help or want to support the project? Visit Support us.