diff --git a/apps/server/package.json b/apps/server/package.json index bf6d28fba..ee975590a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,6 +24,7 @@ "@moeru/eventa": "catalog:", "@moeru/std": "catalog:", "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.214.0", "@opentelemetry/exporter-logs-otlp-proto": "^0.214.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 20b8e2733..d78a611fb 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -16,7 +16,7 @@ import type { HonoEnv } from './types/hono' import process from 'node:process' -import { initLogger, LoggerFormat, LoggerLevel, useLogger } from '@guiiai/logg' +import { initLogger, LoggerFormat, LoggerLevel, setGlobalHookPostLog, useLogger } from '@guiiai/logg' import { serve } from '@hono/node-server' import { createNodeWebSocket } from '@hono/node-ws' import { Hono } from 'hono' @@ -29,7 +29,7 @@ import { createAuth } from './libs/auth' import { createDrizzle, migrateDatabase } from './libs/db' import { parsedEnv } from './libs/env' import { initializeExternalDependency } from './libs/external-dependency' -import { initOtel } from './libs/otel' +import { emitOtelLog, initOtel } from './libs/otel' import { createRedis } from './libs/redis' import { sessionMiddleware } from './middlewares/auth' import { otelMiddleware } from './middlewares/otel' @@ -190,6 +190,11 @@ export async function createApp() { injeca.setLogger(createLoggLogger(useLogger('injeca').useGlobalConfig())) const logger = useLogger('app').useGlobalConfig() + // Forward logg output to OpenTelemetry log exporter + setGlobalHookPostLog((log) => { + emitOtelLog(log.level, log.context, log.message, log.fields as Record) + }) + const otel = injeca.provide('libs:otel', { dependsOn: { env: parsedEnv, lifecycle }, build: ({ dependsOn }) => { diff --git a/apps/server/src/libs/otel.ts b/apps/server/src/libs/otel.ts index 0f37ffe56..6dd1f29db 100644 --- a/apps/server/src/libs/otel.ts +++ b/apps/server/src/libs/otel.ts @@ -7,7 +7,8 @@ import type { Env } from './env' import { env as processEnv } from 'node:process' import { useLogger } from '@guiiai/logg' -import { diag, DiagConsoleLogger, DiagLogLevel, metrics } from '@opentelemetry/api' +import { diag, DiagConsoleLogger, DiagLogLevel, metrics, trace } from '@opentelemetry/api' +import { logs, SeverityNumber } from '@opentelemetry/api-logs' import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto' import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' @@ -298,3 +299,39 @@ export function initOtel(env: Env): OtelInstance | undefined { shutdown, } } + +const severityMap: Record = { + debug: SeverityNumber.DEBUG, + verbose: SeverityNumber.TRACE, + log: SeverityNumber.INFO, + info: SeverityNumber.INFO, + warn: SeverityNumber.WARN, + error: SeverityNumber.ERROR, +} + +/** + * Emit a log record to OpenTelemetry. + * Automatically attaches the active span's traceId/spanId when available. + */ +export function emitOtelLog( + level: string, + context: string, + message: string, + attributes?: Record, +): void { + const otelLogger = logs.getLogger(context) + const spanContext = trace.getActiveSpan()?.spanContext() + + otelLogger.emit({ + severityNumber: severityMap[level.toLowerCase()] ?? SeverityNumber.INFO, + severityText: level.toUpperCase(), + body: message, + attributes: { + ...attributes, + ...(spanContext && { + trace_id: spanContext.traceId, + span_id: spanContext.spanId, + }), + }, + }) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64bdd15ec..c502c47bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -530,6 +530,9 @@ importers: '@opentelemetry/api': specifier: ^1.9.1 version: 1.9.1 + '@opentelemetry/api-logs': + specifier: ^0.214.0 + version: 0.214.0 '@opentelemetry/exporter-logs-otlp-proto': specifier: ^0.214.0 version: 0.214.0(@opentelemetry/api@1.9.1)