From 957891ebed873342ca56bf196ed2b02e7599463a Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 3 Jun 2026 11:31:53 +0530 Subject: [PATCH] feat: add structured OTel request logs with source IP Adds OpenTelemetry SDK logs to the telemetry plugin, emitting structured log records to stdout for every HTTP request. Each log includes client.address (from x-forwarded-for/x-real-ip/remoteAddress), method, route, status code, and duration. Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 19 ++++++++--- packages/chronicle/package.json | 2 ++ .../chronicle/src/server/plugins/telemetry.ts | 32 +++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index cd6271c2..c84a7f2c 100644 --- a/bun.lock +++ b/bun.lock @@ -5,10 +5,7 @@ "": { "name": "chronicle", "dependencies": { - "@analytics/google-analytics": "^1.1.0", - "analytics": "^0.8.19", "std-env": "^4.0.0", - "use-analytics": "^1.1.0", }, "devDependencies": { "@raystack/chronicle": "workspace:*", @@ -29,8 +26,10 @@ "@codemirror/view": "^6.39.14", "@heroicons/react": "^2.2.0", "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.218.0", "@opentelemetry/exporter-prometheus": "^0.214.0", "@opentelemetry/resources": "^2.6.1", + "@opentelemetry/sdk-logs": "^0.218.0", "@opentelemetry/sdk-metrics": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@radix-ui/react-icons": "^1.3.2", @@ -308,11 +307,15 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.218.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw=="], + "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4TGYoZKebUWVuYkV6r5wS2dUF4zH7EbWFw/Uqz1ZM1tGHQeFT9wzHGXq3iSIXMUrwu5jRdxjfMaXrYejPu2kpQ=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + "@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.218.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.218.0", "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag=="], "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], @@ -1220,6 +1223,14 @@ "@antfu/install-pkg/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + "color-convert/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="], "color-string/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="], diff --git a/packages/chronicle/package.json b/packages/chronicle/package.json index 58282d29..4e79bde7 100644 --- a/packages/chronicle/package.json +++ b/packages/chronicle/package.json @@ -43,8 +43,10 @@ "@codemirror/view": "^6.39.14", "@heroicons/react": "^2.2.0", "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.218.0", "@opentelemetry/exporter-prometheus": "^0.214.0", "@opentelemetry/resources": "^2.6.1", + "@opentelemetry/sdk-logs": "^0.218.0", "@opentelemetry/sdk-metrics": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@radix-ui/react-icons": "^1.3.2", diff --git a/packages/chronicle/src/server/plugins/telemetry.ts b/packages/chronicle/src/server/plugins/telemetry.ts index 7f31d5fd..c3986135 100644 --- a/packages/chronicle/src/server/plugins/telemetry.ts +++ b/packages/chronicle/src/server/plugins/telemetry.ts @@ -1,6 +1,12 @@ import type { Counter, Histogram } from '@opentelemetry/api' import { MeterProvider } from '@opentelemetry/sdk-metrics' import { PrometheusExporter } from '@opentelemetry/exporter-prometheus' +import { + LoggerProvider, + SimpleLogRecordProcessor, + ConsoleLogRecordExporter, +} from '@opentelemetry/sdk-logs' +import { SeverityNumber } from '@opentelemetry/api-logs' import { resourceFromAttributes } from '@opentelemetry/resources' import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions' import type { H3Event } from 'h3' @@ -26,6 +32,12 @@ export default definePlugin((nitroApp) => { const provider = new MeterProvider({ resource, readers: [exporter] }) const meter = provider.getMeter('chronicle') + const loggerProvider = new LoggerProvider({ + resource, + processors: [new SimpleLogRecordProcessor(new ConsoleLogRecordExporter())], + }) + const logger = loggerProvider.getLogger('chronicle') + const requestCounter: Counter = meter.createCounter('http_server_request_total', { description: 'Total HTTP requests', }) @@ -37,6 +49,7 @@ export default definePlugin((nitroApp) => { }) nitroApp.hooks.hook('close', async () => { + await loggerProvider.shutdown() await provider.shutdown() await exporter.shutdown() }) @@ -55,7 +68,26 @@ export default definePlugin((nitroApp) => { const duration = performance.now() - start const method = event.req.method const route = new URL(event.req.url).pathname + const clientIp = + event.req.headers['x-forwarded-for']?.toString().split(',')[0].trim() ?? + event.req.headers['x-real-ip']?.toString() ?? + event.req.socket?.remoteAddress ?? + 'unknown' + requestCounter.add(1, { method, route, status: res.status }) requestDuration.record(duration, { method, route, status: res.status }) + + logger.emit({ + severityNumber: SeverityNumber.INFO, + severityText: 'INFO', + body: `${method} ${route} ${res.status} ${duration.toFixed(1)}ms`, + attributes: { + 'client.address': clientIp, + 'http.request.method': method, + 'url.path': route, + 'http.response.status_code': res.status, + 'http.request.duration_ms': Math.round(duration), + }, + }) }) })