import type { ExportResult } from "@opentelemetry/core"; import { ExportResultCode } from "@opentelemetry/core"; import type { LogRecordExporter, ReadableLogRecord } from "@opentelemetry/sdk-logs"; import { SeverityNumber } from "@opentelemetry/api-logs"; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions"; const SEVERITY_TEXT: Record = { [SeverityNumber.DEBUG]: "debug", [SeverityNumber.DEBUG2]: "debug", [SeverityNumber.DEBUG3]: "debug", [SeverityNumber.DEBUG4]: "debug", [SeverityNumber.INFO]: "info", [SeverityNumber.INFO2]: "info", [SeverityNumber.INFO3]: "info", [SeverityNumber.INFO4]: "info", [SeverityNumber.WARN]: "warn", [SeverityNumber.WARN2]: "warn", [SeverityNumber.WARN3]: "warn", [SeverityNumber.WARN4]: "warn", [SeverityNumber.ERROR]: "error", [SeverityNumber.ERROR2]: "error", [SeverityNumber.ERROR3]: "error", [SeverityNumber.ERROR4]: "error", [SeverityNumber.FATAL]: "fatal", [SeverityNumber.FATAL2]: "fatal", [SeverityNumber.FATAL3]: "fatal", [SeverityNumber.FATAL4]: "fatal", }; /** * Exports OTel log records as newline-delimited JSON to stdout. * * Output format: * {"timestamp":"2026-02-23T12:00:00.000Z","level":"info","message":"Server started","service":"atbb-appview","port":3000} * * Compatible with standard log aggregation tools (ELK, Grafana Loki, Datadog, etc.). */ export class StructuredLogExporter implements LogRecordExporter { export( records: ReadableLogRecord[], resultCallback: (result: ExportResult) => void ): void { try { for (const record of records) { const entry = this.formatRecord(record); process.stdout.write(JSON.stringify(entry) + "\n"); } resultCallback({ code: ExportResultCode.SUCCESS }); } catch { resultCallback({ code: ExportResultCode.FAILED }); } } shutdown(): Promise { return Promise.resolve(); } private formatRecord(record: ReadableLogRecord): Record { const resourceAttrs = record.resource?.attributes ?? {}; const logAttrs = record.attributes ?? {}; // Build the base log entry const entry: Record = { timestamp: this.hrTimeToISO(record.hrTime), level: SEVERITY_TEXT[record.severityNumber ?? SeverityNumber.INFO] ?? "info", message: record.body ?? "", }; // Add service metadata from resource const service = resourceAttrs[ATTR_SERVICE_NAME]; if (service) { entry.service = service; } const version = resourceAttrs[ATTR_SERVICE_VERSION]; if (version) { entry.version = version; } const environment = resourceAttrs["deployment.environment.name"]; if (environment) { entry.environment = environment; } // Spread user-provided attributes into the top level for (const [key, value] of Object.entries(logAttrs)) { if (value !== undefined && value !== null) { entry[key] = value; } } return entry; } private hrTimeToISO(hrTime: [number, number]): string { const ms = hrTime[0] * 1000 + hrTime[1] / 1_000_000; return new Date(ms).toISOString(); } }