Skip to content

Commit 86cbba0

Browse files
rsbhclaude
andauthored
feat: add structured OTel request logs with source IP (#117)
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) <noreply@anthropic.com>
1 parent 06013f3 commit 86cbba0

3 files changed

Lines changed: 53 additions & 5 deletions

File tree

bun.lock

Lines changed: 15 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/chronicle/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@
4343
"@codemirror/view": "^6.39.14",
4444
"@heroicons/react": "^2.2.0",
4545
"@opentelemetry/api": "^1.9.1",
46+
"@opentelemetry/api-logs": "^0.218.0",
4647
"@opentelemetry/exporter-prometheus": "^0.214.0",
4748
"@opentelemetry/resources": "^2.6.1",
49+
"@opentelemetry/sdk-logs": "^0.218.0",
4850
"@opentelemetry/sdk-metrics": "^2.6.1",
4951
"@opentelemetry/semantic-conventions": "^1.40.0",
5052
"@radix-ui/react-icons": "^1.3.2",

packages/chronicle/src/server/plugins/telemetry.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { Counter, Histogram } from '@opentelemetry/api'
22
import { MeterProvider } from '@opentelemetry/sdk-metrics'
33
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
4+
import {
5+
LoggerProvider,
6+
SimpleLogRecordProcessor,
7+
ConsoleLogRecordExporter,
8+
} from '@opentelemetry/sdk-logs'
9+
import { SeverityNumber } from '@opentelemetry/api-logs'
410
import { resourceFromAttributes } from '@opentelemetry/resources'
511
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
612
import type { H3Event } from 'h3'
@@ -53,6 +59,12 @@ export default definePlugin((nitroApp) => {
5359
const provider = new MeterProvider({ resource, readers: [exporter] })
5460
const meter = provider.getMeter('chronicle')
5561

62+
const loggerProvider = new LoggerProvider({
63+
resource,
64+
processors: [new SimpleLogRecordProcessor(new ConsoleLogRecordExporter())],
65+
})
66+
const logger = loggerProvider.getLogger('chronicle')
67+
5668
const requestCounter: Counter = meter.createCounter('http_server_request_total', {
5769
description: 'Total HTTP requests',
5870
})
@@ -64,6 +76,7 @@ export default definePlugin((nitroApp) => {
6476
})
6577

6678
nitroApp.hooks.hook('close', async () => {
79+
await loggerProvider.shutdown()
6780
await provider.shutdown()
6881
await exporter.shutdown()
6982
})
@@ -81,8 +94,30 @@ export default definePlugin((nitroApp) => {
8194
if (start === undefined) return
8295
const duration = performance.now() - start
8396
const method = event.req.method
84-
const endpoint = toEndpoint(new URL(event.req.url).pathname)
97+
const route = new URL(event.req.url).pathname
98+
const endpoint = toEndpoint(route)
99+
100+
const clientIp =
101+
event.req.headers['x-forwarded-for']?.toString().split(',')[0].trim() ??
102+
event.req.headers['x-real-ip']?.toString() ??
103+
event.req.socket?.remoteAddress ??
104+
'unknown'
105+
85106
requestCounter.add(1, { method, endpoint, status: res.status })
86107
requestDuration.record(duration, { method, endpoint, status: res.status })
108+
109+
logger.emit({
110+
severityNumber: SeverityNumber.INFO,
111+
severityText: 'INFO',
112+
body: `${method} ${route} ${res.status} ${duration.toFixed(1)}ms`,
113+
attributes: {
114+
'client.address': clientIp,
115+
'http.request.method': method,
116+
'url.path': route,
117+
'http.response.status_code': res.status,
118+
'http.request.duration_ms': Math.round(duration),
119+
},
120+
})
121+
87122
})
88123
})

0 commit comments

Comments
 (0)