Pino structured log + OpenTelemetry correlation
Log Pino dengan trace_id dari OpenTelemetry. Correlate log + trace di Grafana / Loki / Tempo. Wajib untuk distributed system.
Dipublikasikan 27 Juni 2026
“Service A lambat, cek log!” — buka Kibana, search timestamp, tapi gak ketemu request mana yang slow. Tanpa trace_id, log distributed = mimpi buruk. Pino + OTel context bikin setiap log entry punya trace_id sama dengan span Jaeger / Tempo. Klik trace, dapat semua log terkait.
Kode
// src/logger.ts
import pino, { type Logger } from "pino";
import { trace, context } from "@opentelemetry/api";
interface LogContext {
trace_id?: string;
span_id?: string;
trace_flags?: number;
}
function getOtelContext(): LogContext {
const span = trace.getSpan(context.active());
if (!span) return {};
const ctx = span.spanContext();
return {
trace_id: ctx.traceId,
span_id: ctx.spanId,
trace_flags: ctx.traceFlags,
};
}
const isProd = process.env.NODE_ENV === "production";
export const logger: Logger = pino({
level: process.env.LOG_LEVEL ?? "info",
base: {
service: "api-tokopedia",
env: process.env.NODE_ENV ?? "development",
version: process.env.APP_VERSION ?? "dev",
},
// Mixin function dipanggil per log call — inject trace context dinamis
mixin: () => getOtelContext(),
formatters: {
level: (label) => ({ level: label }),
},
// Redact field sensitif sebelum log
redact: {
paths: ["password", "token", "*.headers.authorization", "*.body.password"],
censor: "[REDACTED]",
},
// Production: JSON ke stdout. Dev: pretty-print
transport: isProd
? undefined
: {
target: "pino-pretty",
options: { colorize: true, translateTime: "HH:MM:ss" },
},
});
// src/tracing.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: "api-tokopedia",
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION ?? "dev",
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_ENDPOINT ?? "http://localhost:4318/v1/traces",
}),
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-fs": { enabled: false },
}),
],
});
sdk.start();
// Shutdown gracefully
process.on("SIGTERM", () => {
sdk.shutdown().finally(() => process.exit(0));
});
export default sdk;
// src/middleware/log.ts
import type { FastifyInstance } from "fastify";
import { trace } from "@opentelemetry/api";
import { logger } from "../logger";
export function attachLogger(app: FastifyInstance) {
app.addHook("onRequest", async (req, reply) => {
const start = process.hrtime.bigint();
(req as any).log_start = start;
logger.info({
msg: "request_started",
method: req.method,
url: req.url,
ip: req.ip,
});
});
app.addHook("onResponse", async (req, reply) => {
const start = (req as any).log_start as bigint;
const duration_ms = Number((process.hrtime.bigint() - start) / 1_000_000n);
const span = trace.getActiveSpan();
if (span) {
span.setAttribute("http.status_code", reply.statusCode);
span.setAttribute("http.duration_ms", duration_ms);
}
logger.info({
msg: "request_completed",
method: req.method,
url: req.url,
status: reply.statusCode,
duration_ms,
});
});
app.setErrorHandler((err, _req, reply) => {
logger.error({
msg: "request_error",
err: { message: err.message, stack: err.stack, code: (err as any).code },
});
const span = trace.getActiveSpan();
if (span) {
span.recordException(err);
span.setStatus({ code: 2, message: err.message }); // ERROR
}
reply.code(500).send({ error: "Internal error" });
});
}
Pemakaian
// src/server.ts — bootstrap dengan tracing dulu
import "./tracing"; // HARUS first import — auto-instrument require()
import Fastify from "fastify";
import { logger } from "./logger";
import { attachLogger } from "./middleware/log";
const app = Fastify({ logger: false });
attachLogger(app);
app.get("/api/produk/:id", async (req, reply) => {
const { id } = req.params as { id: string };
logger.debug({ msg: "fetch_produk_start", produk_id: id });
const produk = await fetchFromDB(id);
logger.info({ msg: "fetch_produk_success", produk_id: id, harga: produk.harga });
return produk;
});
await app.listen({ port: 3000 });
logger.info("Server listening on :3000");
// Output log production (JSON line, satu per row)
{
"level": "info",
"time": 1717200000000,
"service": "api-tokopedia",
"env": "production",
"trace_id": "5f3b8a2c1d9e4f6a7b8c9d0e1f2a3b4c",
"span_id": "1a2b3c4d5e6f7a8b",
"trace_flags": 1,
"msg": "request_completed",
"method": "GET",
"url": "/api/produk/123",
"status": 200,
"duration_ms": 47
}
# Query di Loki — filter request slow
{service="api-tokopedia"} | json | duration_ms > 1000
# Lalu klik trace_id → langsung jump ke trace di Tempo
Kapan dipakai
- Microservice di production yang punya distributed trace.
- API gateway yang fan-out ke banyak service downstream.
- Service yang harus comply audit log (financial, healthcare).
- Setup observability stack: Grafana + Loki + Tempo.
Catatan
- mixin per call — Pino call mixin function setiap log. Untuk overhead minimum, snippet ini cuma get span context (cheap).
- import tracing first — auto-instrumentation patch core modules. Kalau import setelah module yang di-patch sudah loaded, instrumentation gak jalan.
- redact path — log password / token = breach. Pino redact lebih cepat dari regex post-process karena terjadi sebelum serialize.
- level di production: info default, debug hanya saat investigation. Setiap log entry punya cost — tinggi-traffic service bisa generate GB log per jam.
- pino-pretty hanya dev — di production, JSON line ke stdout, biarkan log shipper (Promtail / Fluent Bit) yang parse.
Pino sangat fast karena async write. Jangan log di hot path tanpa sampling — bahkan no-op
logger.debug()punya cost serialization untuk argument-nya.
# tags
pinologgingopentelemetrytracingobservability
Ditulis oleh Asti Larasati · 27 Juni 2026