Skip to content

Commit 98a6ac2

Browse files
authored
feat: pass HTTP headers to evlog:drain hook (#31)
1 parent 18c386c commit 98a6ac2

File tree

5 files changed

+373
-2
lines changed

5 files changed

+373
-2
lines changed

apps/docs/content/1.getting-started/2.installation.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,40 @@ export default defineNitroPlugin((nitroApp) => {
166166
The hook receives a `DrainContext` with:
167167
- `event`: The complete `WideEvent` (timestamp, level, service, and all accumulated context)
168168
- `request`: Optional request metadata (`method`, `path`, `requestId`)
169+
- `headers`: HTTP headers from the original request (useful for correlation with external services)
170+
171+
::callout{icon="i-lucide-shield-check" color="success"}
172+
**Security:** Sensitive headers (`authorization`, `cookie`, `set-cookie`, `x-api-key`, `x-auth-token`, `proxy-authorization`) are automatically filtered out and never passed to the drain hook.
173+
::
174+
175+
#### Using Headers for External Service Correlation
176+
177+
The `headers` field allows you to correlate logs with external services like PostHog, Sentry, or custom analytics:
178+
179+
```typescript [server/plugins/evlog-posthog.ts]
180+
export default defineNitroPlugin((nitroApp) => {
181+
const posthog = usePostHog()
182+
183+
nitroApp.hooks.hook('evlog:drain', (ctx) => {
184+
if (!posthog) return
185+
186+
// Extract correlation headers sent from the client
187+
const sessionId = ctx.headers?.['x-posthog-session-id']
188+
const distinctId = ctx.headers?.['x-posthog-distinct-id']
189+
190+
if (!distinctId) return
191+
192+
posthog.capture({
193+
distinctId,
194+
event: 'server_log',
195+
properties: {
196+
...ctx.event,
197+
$session_id: sessionId,
198+
},
199+
})
200+
})
201+
})
202+
```
169203

170204
### Client Transport
171205

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
// Example drain hook - demonstrates evlog:drain usage
22
export default defineNitroPlugin((nitroApp) => {
33
nitroApp.hooks.hook('evlog:drain', (ctx) => {
4-
// Example: log the event to console (replace with your external service)
5-
console.log('[DRAIN]', JSON.stringify(ctx.event, null, 2))
4+
// Example: log the event and headers to console (replace with your external service)
5+
console.log('[DRAIN]', JSON.stringify({
6+
event: ctx.event,
7+
request: ctx.request,
8+
headers: ctx.headers,
9+
}, null, 2))
610

711
// Example: send to Axiom (uncomment and configure)
812
// await fetch('https://api.axiom.co/v1/datasets/logs/ingest', {
913
// method: 'POST',
1014
// headers: { Authorization: `Bearer ${process.env.AXIOM_TOKEN}` },
1115
// body: JSON.stringify([ctx.event])
1216
// })
17+
18+
// Example: correlate with PostHog using headers (uncomment and configure)
19+
// const sessionId = ctx.headers?.['x-posthog-session-id']
20+
// const distinctId = ctx.headers?.['x-posthog-distinct-id']
21+
// if (distinctId) {
22+
// posthog.capture({ distinctId, event: 'server_log', properties: { ...ctx.event, $session_id: sessionId } })
23+
// }
1324
})
1425
})

packages/evlog/src/nitro/plugin.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { NitroApp } from 'nitropack/types'
22
import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime'
3+
import { getHeaders } from 'h3'
34
import { createRequestLogger, initLogger } from '../logger'
45
import type { RequestLogger, SamplingConfig, ServerEvent, TailSamplingContext, WideEvent } from '../types'
56
import { matchesPattern } from '../utils'
@@ -29,6 +30,29 @@ function shouldLog(path: string, include?: string[], exclude?: string[]): boolea
2930
return include.some(pattern => matchesPattern(path, pattern))
3031
}
3132

33+
/** Headers that should never be passed to the drain hook for security */
34+
const SENSITIVE_HEADERS = [
35+
'authorization',
36+
'cookie',
37+
'set-cookie',
38+
'x-api-key',
39+
'x-auth-token',
40+
'proxy-authorization',
41+
]
42+
43+
function getSafeHeaders(event: ServerEvent): Record<string, string> {
44+
const allHeaders = getHeaders(event as Parameters<typeof getHeaders>[0])
45+
const safeHeaders: Record<string, string> = {}
46+
47+
for (const [key, value] of Object.entries(allHeaders)) {
48+
if (!SENSITIVE_HEADERS.includes(key.toLowerCase())) {
49+
safeHeaders[key] = value
50+
}
51+
}
52+
53+
return safeHeaders
54+
}
55+
3256
function getResponseStatus(event: ServerEvent): number {
3357
// Node.js style
3458
if (event.node?.res?.statusCode) {
@@ -53,6 +77,7 @@ function callDrainHook(nitroApp: NitroApp, emittedEvent: WideEvent | null, event
5377
nitroApp.hooks.callHook('evlog:drain', {
5478
event: emittedEvent,
5579
request: { method: event.method, path: event.path, requestId: event.context.requestId as string | undefined },
80+
headers: getSafeHeaders(event),
5681
}).catch((err) => {
5782
console.error('[evlog] drain failed:', err)
5883
})

packages/evlog/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export interface DrainContext {
125125
path?: string
126126
requestId?: string
127127
}
128+
/** HTTP headers from the original request (useful for correlation with external services) */
129+
headers?: Record<string, string>
128130
}
129131

130132
/**

0 commit comments

Comments
 (0)