evlog 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -61
- package/dist/_http-DVDwNag0.mjs +76 -0
- package/dist/_http-DVDwNag0.mjs.map +1 -0
- package/dist/_severity-CXfyvxQi.mjs +17 -0
- package/dist/_severity-CXfyvxQi.mjs.map +1 -0
- package/dist/adapters/axiom.d.mts +1 -0
- package/dist/adapters/axiom.d.mts.map +1 -1
- package/dist/adapters/axiom.mjs +40 -44
- package/dist/adapters/axiom.mjs.map +1 -1
- package/dist/adapters/better-stack.d.mts +1 -0
- package/dist/adapters/better-stack.d.mts.map +1 -1
- package/dist/adapters/better-stack.mjs +34 -45
- package/dist/adapters/better-stack.mjs.map +1 -1
- package/dist/adapters/otlp.d.mts +1 -0
- package/dist/adapters/otlp.d.mts.map +1 -1
- package/dist/adapters/otlp.mjs +61 -81
- package/dist/adapters/otlp.mjs.map +1 -1
- package/dist/adapters/posthog.d.mts +35 -1
- package/dist/adapters/posthog.d.mts.map +1 -1
- package/dist/adapters/posthog.mjs +91 -45
- package/dist/adapters/posthog.mjs.map +1 -1
- package/dist/adapters/sentry.d.mts +1 -0
- package/dist/adapters/sentry.d.mts.map +1 -1
- package/dist/adapters/sentry.mjs +41 -53
- package/dist/adapters/sentry.mjs.map +1 -1
- package/dist/browser.d.mts +63 -0
- package/dist/browser.d.mts.map +1 -0
- package/dist/browser.mjs +95 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/logger.d.mts +6 -2
- package/dist/logger.d.mts.map +1 -1
- package/dist/logger.mjs +56 -3
- package/dist/logger.mjs.map +1 -1
- package/dist/nitro/errorHandler.mjs +6 -17
- package/dist/nitro/errorHandler.mjs.map +1 -1
- package/dist/nitro/module.d.mts +11 -0
- package/dist/nitro/module.d.mts.map +1 -0
- package/dist/nitro/module.mjs +23 -0
- package/dist/nitro/module.mjs.map +1 -0
- package/dist/nitro/plugin.mjs +28 -52
- package/dist/nitro/plugin.mjs.map +1 -1
- package/dist/nitro/v3/errorHandler.d.mts +24 -0
- package/dist/nitro/v3/errorHandler.d.mts.map +1 -0
- package/dist/nitro/v3/errorHandler.mjs +36 -0
- package/dist/nitro/v3/errorHandler.mjs.map +1 -0
- package/dist/nitro/v3/index.d.mts +4 -0
- package/dist/nitro/v3/index.mjs +4 -0
- package/dist/nitro/v3/module.d.mts +10 -0
- package/dist/nitro/v3/module.d.mts.map +1 -0
- package/dist/nitro/v3/module.mjs +22 -0
- package/dist/nitro/v3/module.mjs.map +1 -0
- package/dist/nitro/v3/plugin.d.mts +14 -0
- package/dist/nitro/v3/plugin.d.mts.map +1 -0
- package/dist/nitro/v3/plugin.mjs +157 -0
- package/dist/nitro/v3/plugin.mjs.map +1 -0
- package/dist/nitro/v3/useLogger.d.mts +24 -0
- package/dist/nitro/v3/useLogger.d.mts.map +1 -0
- package/dist/nitro/v3/useLogger.mjs +27 -0
- package/dist/nitro/v3/useLogger.mjs.map +1 -0
- package/dist/nitro-D57TWGyN.mjs +73 -0
- package/dist/nitro-D57TWGyN.mjs.map +1 -0
- package/dist/nitro-D81NBVPi.d.mts +42 -0
- package/dist/nitro-D81NBVPi.d.mts.map +1 -0
- package/dist/nuxt/module.d.mts +12 -0
- package/dist/nuxt/module.d.mts.map +1 -1
- package/dist/nuxt/module.mjs +17 -2
- package/dist/nuxt/module.mjs.map +1 -1
- package/dist/runtime/client/log.d.mts +5 -2
- package/dist/runtime/client/log.d.mts.map +1 -1
- package/dist/runtime/client/log.mjs +16 -3
- package/dist/runtime/client/log.mjs.map +1 -1
- package/dist/runtime/client/plugin.mjs +1 -0
- package/dist/runtime/client/plugin.mjs.map +1 -1
- package/dist/runtime/server/routes/_evlog/ingest.post.mjs +1 -1
- package/dist/types.d.mts +32 -2
- package/dist/types.d.mts.map +1 -1
- package/package.json +30 -7
- package/dist/_utils-DZA9nou3.mjs +0 -23
- package/dist/_utils-DZA9nou3.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
[](https://npm.chart.dev/evlog)
|
|
5
5
|
[](https://github.com/HugoRCD/evlog/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](https://nuxt.com/)
|
|
8
7
|
[](https://evlog.dev)
|
|
9
8
|
[](https://github.com/HugoRCD/evlog/blob/main/LICENSE)
|
|
10
9
|
|
|
11
10
|
**Your logs are lying to you.**
|
|
12
11
|
|
|
13
|
-
A single request generates 10+ log lines. When production breaks at 3am, you're grep-ing through noise, praying you'll find signal. Your errors say "Something went wrong"
|
|
12
|
+
A single request generates 10+ log lines. When production breaks at 3am, you're grep-ing through noise, praying you'll find signal. Your errors say "Something went wrong" -- thanks, very helpful.
|
|
14
13
|
|
|
15
14
|
**evlog fixes this.** One log per request. All context included. Errors that explain themselves.
|
|
16
15
|
|
|
@@ -21,13 +20,13 @@ A single request generates 10+ log lines. When production breaks at 3am, you're
|
|
|
21
20
|
```typescript
|
|
22
21
|
// server/api/checkout.post.ts
|
|
23
22
|
|
|
24
|
-
//
|
|
23
|
+
// Scattered logs - impossible to debug
|
|
25
24
|
console.log('Request received')
|
|
26
25
|
console.log('User:', user.id)
|
|
27
26
|
console.log('Cart loaded')
|
|
28
27
|
console.log('Payment failed') // Good luck finding this at 3am
|
|
29
28
|
|
|
30
|
-
throw new Error('Something went wrong')
|
|
29
|
+
throw new Error('Something went wrong')
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
### The Solution
|
|
@@ -36,7 +35,7 @@ throw new Error('Something went wrong') // 🤷♂️
|
|
|
36
35
|
// server/api/checkout.post.ts
|
|
37
36
|
import { useLogger } from 'evlog'
|
|
38
37
|
|
|
39
|
-
//
|
|
38
|
+
// One comprehensive event per request
|
|
40
39
|
export default defineEventHandler(async (event) => {
|
|
41
40
|
const log = useLogger(event) // Auto-injected by evlog
|
|
42
41
|
|
|
@@ -176,18 +175,43 @@ The wide event emitted at the end contains **everything**:
|
|
|
176
175
|
|
|
177
176
|
Works with **any framework powered by Nitro**: Nuxt, Analog, Vinxi, SolidStart, TanStack Start, and more.
|
|
178
177
|
|
|
178
|
+
### Nitro v3
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// nitro.config.ts
|
|
182
|
+
import { defineConfig } from 'nitro'
|
|
183
|
+
import evlog from 'evlog/nitro/v3'
|
|
184
|
+
|
|
185
|
+
export default defineConfig({
|
|
186
|
+
modules: [
|
|
187
|
+
evlog({ env: { service: 'my-api' } })
|
|
188
|
+
],
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Nitro v2
|
|
193
|
+
|
|
179
194
|
```typescript
|
|
180
195
|
// nitro.config.ts
|
|
196
|
+
import { defineNitroConfig } from 'nitropack/config'
|
|
197
|
+
import evlog from 'evlog/nitro'
|
|
198
|
+
|
|
181
199
|
export default defineNitroConfig({
|
|
182
|
-
|
|
200
|
+
modules: [
|
|
201
|
+
evlog({ env: { service: 'my-api' } })
|
|
202
|
+
],
|
|
183
203
|
})
|
|
184
204
|
```
|
|
185
205
|
|
|
186
|
-
|
|
206
|
+
Then use `useLogger` in any route. Import from `evlog/nitro/v3` (v3) or `evlog/nitro` (v2):
|
|
187
207
|
|
|
188
208
|
```typescript
|
|
189
209
|
// routes/api/documents/[id]/export.post.ts
|
|
190
|
-
import { useLogger
|
|
210
|
+
// Nitro v3: import { defineHandler } from 'nitro/h3' + import { useLogger } from 'evlog/nitro/v3'
|
|
211
|
+
// Nitro v2: import { defineEventHandler } from 'h3' + import { useLogger } from 'evlog/nitro'
|
|
212
|
+
import { defineEventHandler } from 'h3'
|
|
213
|
+
import { useLogger } from 'evlog/nitro'
|
|
214
|
+
import { createError } from 'evlog'
|
|
191
215
|
|
|
192
216
|
export default defineEventHandler(async (event) => {
|
|
193
217
|
const log = useLogger(event)
|
|
@@ -248,47 +272,6 @@ Output when the export completes:
|
|
|
248
272
|
}
|
|
249
273
|
```
|
|
250
274
|
|
|
251
|
-
## Structured Errors
|
|
252
|
-
|
|
253
|
-
Errors should tell you **what** happened, **why**, and **how to fix it**.
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
// server/api/repos/sync.post.ts
|
|
257
|
-
import { useLogger, createError } from 'evlog'
|
|
258
|
-
|
|
259
|
-
export default defineEventHandler(async (event) => {
|
|
260
|
-
const log = useLogger(event)
|
|
261
|
-
|
|
262
|
-
log.set({ repo: { owner: 'acme', name: 'my-project' } })
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
const result = await syncWithGitHub()
|
|
266
|
-
log.set({ sync: { commits: result.commits, files: result.files } })
|
|
267
|
-
return result
|
|
268
|
-
} catch (error) {
|
|
269
|
-
log.error(error, { step: 'github-sync' })
|
|
270
|
-
|
|
271
|
-
throw createError({
|
|
272
|
-
message: 'Failed to sync repository',
|
|
273
|
-
status: 503,
|
|
274
|
-
why: 'GitHub API rate limit exceeded',
|
|
275
|
-
fix: 'Wait 1 hour or use a different token',
|
|
276
|
-
link: 'https://docs.github.com/en/rest/rate-limit',
|
|
277
|
-
cause: error,
|
|
278
|
-
})
|
|
279
|
-
}
|
|
280
|
-
})
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
Console output (development):
|
|
284
|
-
|
|
285
|
-
```
|
|
286
|
-
Error: Failed to sync repository
|
|
287
|
-
Why: GitHub API rate limit exceeded
|
|
288
|
-
Fix: Wait 1 hour or use a different token
|
|
289
|
-
More info: https://docs.github.com/en/rest/rate-limit
|
|
290
|
-
```
|
|
291
|
-
|
|
292
275
|
## Standalone TypeScript
|
|
293
276
|
|
|
294
277
|
For scripts, workers, or any TypeScript project:
|
|
@@ -390,6 +373,130 @@ Notes:
|
|
|
390
373
|
- `request.cf` is included (colo, country, asn) unless disabled
|
|
391
374
|
- Use `headerAllowlist` to avoid logging sensitive headers
|
|
392
375
|
|
|
376
|
+
## Hono
|
|
377
|
+
|
|
378
|
+
Use the standalone API to create one wide event per request from a Hono middleware.
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// src/index.ts
|
|
382
|
+
import { serve } from '@hono/node-server'
|
|
383
|
+
import { Hono } from 'hono'
|
|
384
|
+
import { createRequestLogger, initLogger } from 'evlog'
|
|
385
|
+
|
|
386
|
+
initLogger({
|
|
387
|
+
env: { service: 'hono-api' },
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const app = new Hono()
|
|
391
|
+
|
|
392
|
+
app.use('*', async (c, next) => {
|
|
393
|
+
const startedAt = Date.now()
|
|
394
|
+
const log = createRequestLogger({ method: c.req.method, path: c.req.path })
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
await next()
|
|
398
|
+
} catch (error) {
|
|
399
|
+
log.error(error as Error)
|
|
400
|
+
throw error
|
|
401
|
+
} finally {
|
|
402
|
+
log.emit({
|
|
403
|
+
status: c.res.status,
|
|
404
|
+
duration: Date.now() - startedAt,
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
app.get('/health', (c) => c.json({ ok: true }))
|
|
410
|
+
|
|
411
|
+
serve({ fetch: app.fetch, port: 3000 })
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
See the full [hono example](https://github.com/HugoRCD/evlog/tree/main/examples/hono) for a complete working project.
|
|
415
|
+
|
|
416
|
+
## Browser
|
|
417
|
+
|
|
418
|
+
Use the `log` API on the client side for structured browser logging:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { log } from 'evlog/browser'
|
|
422
|
+
|
|
423
|
+
log.info('checkout', 'User initiated checkout')
|
|
424
|
+
log.error({ action: 'payment', error: 'validation_failed' })
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
In Nuxt, `log` is auto-imported -- no import needed in Vue components:
|
|
428
|
+
|
|
429
|
+
```vue
|
|
430
|
+
<script setup>
|
|
431
|
+
log.info('checkout', 'User initiated checkout')
|
|
432
|
+
</script>
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Client logs output to the browser console with colored tags in development.
|
|
436
|
+
|
|
437
|
+
### Client Transport
|
|
438
|
+
|
|
439
|
+
To send client logs to the server for centralized logging, enable the transport:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// nuxt.config.ts
|
|
443
|
+
export default defineNuxtConfig({
|
|
444
|
+
modules: ['evlog/nuxt'],
|
|
445
|
+
evlog: {
|
|
446
|
+
transport: {
|
|
447
|
+
enabled: true, // Send client logs to server
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
When enabled:
|
|
454
|
+
1. Client logs are sent to `/api/_evlog/ingest` via POST
|
|
455
|
+
2. Server enriches with environment context (service, version, etc.)
|
|
456
|
+
3. `evlog:drain` hook is called with `source: 'client'`
|
|
457
|
+
4. External services receive the log
|
|
458
|
+
|
|
459
|
+
## Structured Errors
|
|
460
|
+
|
|
461
|
+
Errors should tell you **what** happened, **why**, and **how to fix it**.
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
// server/api/repos/sync.post.ts
|
|
465
|
+
import { useLogger, createError } from 'evlog'
|
|
466
|
+
|
|
467
|
+
export default defineEventHandler(async (event) => {
|
|
468
|
+
const log = useLogger(event)
|
|
469
|
+
|
|
470
|
+
log.set({ repo: { owner: 'acme', name: 'my-project' } })
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
const result = await syncWithGitHub()
|
|
474
|
+
log.set({ sync: { commits: result.commits, files: result.files } })
|
|
475
|
+
return result
|
|
476
|
+
} catch (error) {
|
|
477
|
+
log.error(error, { step: 'github-sync' })
|
|
478
|
+
|
|
479
|
+
throw createError({
|
|
480
|
+
message: 'Failed to sync repository',
|
|
481
|
+
status: 503,
|
|
482
|
+
why: 'GitHub API rate limit exceeded',
|
|
483
|
+
fix: 'Wait 1 hour or use a different token',
|
|
484
|
+
link: 'https://docs.github.com/en/rest/rate-limit',
|
|
485
|
+
cause: error,
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Console output (development):
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
Error: Failed to sync repository
|
|
495
|
+
Why: GitHub API rate limit exceeded
|
|
496
|
+
Fix: Wait 1 hour or use a different token
|
|
497
|
+
More info: https://docs.github.com/en/rest/rate-limit
|
|
498
|
+
```
|
|
499
|
+
|
|
393
500
|
## Enrichment Hook
|
|
394
501
|
|
|
395
502
|
Use the `evlog:enrich` hook to add derived context after emit, before drain.
|
|
@@ -428,6 +535,58 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
428
535
|
})
|
|
429
536
|
```
|
|
430
537
|
|
|
538
|
+
Each enricher adds a specific field to the event:
|
|
539
|
+
|
|
540
|
+
| Enricher | Event Field | Shape |
|
|
541
|
+
|----------|-------------|-------|
|
|
542
|
+
| `createUserAgentEnricher()` | `event.userAgent` | `{ raw, browser?: { name, version? }, os?: { name, version? }, device?: { type } }` |
|
|
543
|
+
| `createGeoEnricher()` | `event.geo` | `{ country?, region?, regionCode?, city?, latitude?, longitude? }` |
|
|
544
|
+
| `createRequestSizeEnricher()` | `event.requestSize` | `{ requestBytes?, responseBytes? }` |
|
|
545
|
+
| `createTraceContextEnricher()` | `event.traceContext` + `event.traceId` + `event.spanId` | `{ traceparent?, tracestate?, traceId?, spanId? }` |
|
|
546
|
+
|
|
547
|
+
All enrichers accept an optional `{ overwrite?: boolean }` option. By default (`overwrite: false`), user-provided data on the event takes precedence over enricher-computed values. Set `overwrite: true` to always replace existing fields.
|
|
548
|
+
|
|
549
|
+
> **Cloudflare geo note:** Only `cf-ipcountry` is a real Cloudflare HTTP header. The `cf-region`, `cf-city`, `cf-latitude`, `cf-longitude` headers are NOT standard -- they are properties of `request.cf`. For full geo data on Cloudflare, write a custom enricher that reads `request.cf`, or use a Workers middleware to forward `cf` properties as custom headers.
|
|
550
|
+
|
|
551
|
+
### Custom Enrichers
|
|
552
|
+
|
|
553
|
+
The `evlog:enrich` hook receives an `EnrichContext` with these fields:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
interface EnrichContext {
|
|
557
|
+
event: WideEvent // The emitted wide event (mutable -- modify it directly)
|
|
558
|
+
request?: { // Request metadata
|
|
559
|
+
method?: string
|
|
560
|
+
path?: string
|
|
561
|
+
requestId?: string
|
|
562
|
+
}
|
|
563
|
+
headers?: Record<string, string> // Safe HTTP headers (sensitive headers filtered)
|
|
564
|
+
response?: { // Response metadata
|
|
565
|
+
status?: number
|
|
566
|
+
headers?: Record<string, string>
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Example custom enricher:
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
// server/plugins/evlog-enrich.ts
|
|
575
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
576
|
+
nitroApp.hooks.hook('evlog:enrich', (ctx) => {
|
|
577
|
+
// Add deployment metadata
|
|
578
|
+
ctx.event.deploymentId = process.env.DEPLOYMENT_ID
|
|
579
|
+
ctx.event.region = process.env.FLY_REGION
|
|
580
|
+
|
|
581
|
+
// Extract data from headers
|
|
582
|
+
const tenantId = ctx.headers?.['x-tenant-id']
|
|
583
|
+
if (tenantId) {
|
|
584
|
+
ctx.event.tenantId = tenantId
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
```
|
|
589
|
+
|
|
431
590
|
## Adapters
|
|
432
591
|
|
|
433
592
|
Send your logs to external observability platforms with built-in adapters.
|
|
@@ -469,6 +628,24 @@ Set environment variables:
|
|
|
469
628
|
NUXT_OTLP_ENDPOINT=http://localhost:4318
|
|
470
629
|
```
|
|
471
630
|
|
|
631
|
+
### PostHog
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
// server/plugins/evlog-drain.ts
|
|
635
|
+
import { createPostHogDrain } from 'evlog/posthog'
|
|
636
|
+
|
|
637
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
638
|
+
nitroApp.hooks.hook('evlog:drain', createPostHogDrain())
|
|
639
|
+
})
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Set environment variables:
|
|
643
|
+
|
|
644
|
+
```bash
|
|
645
|
+
NUXT_POSTHOG_API_KEY=phc_your-key
|
|
646
|
+
NUXT_POSTHOG_HOST=https://us.i.posthog.com # Optional: for EU or self-hosted
|
|
647
|
+
```
|
|
648
|
+
|
|
472
649
|
### Sentry
|
|
473
650
|
|
|
474
651
|
```typescript
|
|
@@ -486,6 +663,23 @@ Set environment variables:
|
|
|
486
663
|
NUXT_SENTRY_DSN=https://public@o0.ingest.sentry.io/123
|
|
487
664
|
```
|
|
488
665
|
|
|
666
|
+
### Better Stack
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
// server/plugins/evlog-drain.ts
|
|
670
|
+
import { createBetterStackDrain } from 'evlog/better-stack'
|
|
671
|
+
|
|
672
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
673
|
+
nitroApp.hooks.hook('evlog:drain', createBetterStackDrain())
|
|
674
|
+
})
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Set environment variables:
|
|
678
|
+
|
|
679
|
+
```bash
|
|
680
|
+
NUXT_BETTER_STACK_SOURCE_TOKEN=your-source-token
|
|
681
|
+
```
|
|
682
|
+
|
|
489
683
|
### Multiple Destinations
|
|
490
684
|
|
|
491
685
|
Send logs to multiple services:
|
|
@@ -571,15 +765,15 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
571
765
|
| `retry.initialDelayMs` | `1000` | Base delay for first retry |
|
|
572
766
|
| `retry.maxDelayMs` | `30000` | Upper bound for any retry delay |
|
|
573
767
|
| `maxBufferSize` | `1000` | Max buffered events before dropping oldest |
|
|
574
|
-
| `onDropped` |
|
|
768
|
+
| `onDropped` | -- | Callback when events are dropped |
|
|
575
769
|
|
|
576
770
|
### Returned drain function
|
|
577
771
|
|
|
578
772
|
The function returned by `pipeline(drain)` is hook-compatible and exposes:
|
|
579
773
|
|
|
580
|
-
- **`drain(ctx)`**
|
|
581
|
-
- **`drain.flush()`**
|
|
582
|
-
- **`drain.pending`**
|
|
774
|
+
- **`drain(ctx)`** -- Push a single event into the buffer
|
|
775
|
+
- **`drain.flush()`** -- Force-flush all buffered events (call on server shutdown)
|
|
776
|
+
- **`drain.pending`** -- Number of events currently buffered
|
|
583
777
|
|
|
584
778
|
## API Reference
|
|
585
779
|
|
|
@@ -589,6 +783,7 @@ Initialize the logger. Required for standalone usage, automatic with Nuxt/Nitro
|
|
|
589
783
|
|
|
590
784
|
```typescript
|
|
591
785
|
initLogger({
|
|
786
|
+
enabled: boolean // Optional. Enable/disable all logging (default: true)
|
|
592
787
|
env: {
|
|
593
788
|
service: string // Service name
|
|
594
789
|
environment: string // 'production' | 'development' | 'test'
|
|
@@ -679,9 +874,9 @@ In development, evlog uses a compact tree format:
|
|
|
679
874
|
|
|
680
875
|
```
|
|
681
876
|
16:45:31.060 INFO [my-app] GET /api/checkout 200 in 234ms
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
877
|
+
|- user: id=123 plan=premium
|
|
878
|
+
|- cart: items=3 total=9999
|
|
879
|
+
+- payment: id=pay_xyz method=card
|
|
685
880
|
```
|
|
686
881
|
|
|
687
882
|
In production (`pretty: false`), logs are emitted as JSON for machine parsing.
|
|
@@ -797,11 +992,12 @@ evlog works with any framework powered by [Nitro](https://nitro.unjs.io/):
|
|
|
797
992
|
| Framework | Integration |
|
|
798
993
|
|-----------|-------------|
|
|
799
994
|
| **Nuxt** | `modules: ['evlog/nuxt']` |
|
|
800
|
-
| **
|
|
801
|
-
| **
|
|
802
|
-
| **
|
|
803
|
-
| **
|
|
804
|
-
| **
|
|
995
|
+
| **Nitro v3** | `modules: [evlog()]` with `import evlog from 'evlog/nitro/v3'` |
|
|
996
|
+
| **Nitro v2** | `modules: [evlog()]` with `import evlog from 'evlog/nitro'` |
|
|
997
|
+
| **Analog** | Nitro v2 module setup |
|
|
998
|
+
| **Vinxi** | Nitro v2 module setup |
|
|
999
|
+
| **SolidStart** | Nitro v2 module setup ([example](./examples/solidstart)) |
|
|
1000
|
+
| **TanStack Start** | Nitro v2 module setup |
|
|
805
1001
|
|
|
806
1002
|
## Agent Skills
|
|
807
1003
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/adapters/_config.ts
|
|
8
|
+
/**
|
|
9
|
+
* Try to get runtime config from Nitro/Nuxt environment.
|
|
10
|
+
* Returns undefined if not in a Nitro context.
|
|
11
|
+
*/
|
|
12
|
+
function getRuntimeConfig() {
|
|
13
|
+
try {
|
|
14
|
+
const { useRuntimeConfig } = __require("nitropack/runtime");
|
|
15
|
+
return useRuntimeConfig();
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function resolveAdapterConfig(namespace, fields, overrides) {
|
|
21
|
+
const runtimeConfig = getRuntimeConfig();
|
|
22
|
+
const evlogNs = runtimeConfig?.evlog?.[namespace];
|
|
23
|
+
const rootNs = runtimeConfig?.[namespace];
|
|
24
|
+
const config = {};
|
|
25
|
+
for (const { key, env } of fields) config[key] = overrides?.[key] ?? evlogNs?.[key] ?? rootNs?.[key] ?? resolveEnv(env);
|
|
26
|
+
return config;
|
|
27
|
+
}
|
|
28
|
+
function resolveEnv(envKeys) {
|
|
29
|
+
if (!envKeys) return void 0;
|
|
30
|
+
for (const key of envKeys) {
|
|
31
|
+
const val = process.env[key];
|
|
32
|
+
if (val) return val;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/adapters/_drain.ts
|
|
38
|
+
function defineDrain(options) {
|
|
39
|
+
return async (ctx) => {
|
|
40
|
+
const contexts = Array.isArray(ctx) ? ctx : [ctx];
|
|
41
|
+
if (contexts.length === 0) return;
|
|
42
|
+
const config = options.resolve();
|
|
43
|
+
if (!config) return;
|
|
44
|
+
try {
|
|
45
|
+
await options.send(contexts.map((c) => c.event), config);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`[evlog/${options.name}] Failed to send events:`, error);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/adapters/_http.ts
|
|
54
|
+
async function httpPost({ url, headers, body, timeout, label }) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(url, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers,
|
|
61
|
+
body,
|
|
62
|
+
signal: controller.signal
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const text = await response.text().catch(() => "Unknown error");
|
|
66
|
+
const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
|
|
67
|
+
throw new Error(`${label} API error: ${response.status} ${response.statusText} - ${safeText}`);
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timeoutId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { defineDrain as n, resolveAdapterConfig as r, httpPost as t };
|
|
76
|
+
//# sourceMappingURL=_http-DVDwNag0.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_http-DVDwNag0.mjs","names":[],"sources":["../src/adapters/_config.ts","../src/adapters/_drain.ts","../src/adapters/_http.ts"],"sourcesContent":["/**\n * Try to get runtime config from Nitro/Nuxt environment.\n * Returns undefined if not in a Nitro context.\n */\nexport function getRuntimeConfig(): Record<string, any> | undefined {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { useRuntimeConfig } = require('nitropack/runtime')\n return useRuntimeConfig()\n } catch {\n return undefined\n }\n}\n\nexport interface ConfigField<T> {\n key: keyof T & string\n env?: string[]\n}\n\nexport function resolveAdapterConfig<T>(\n namespace: string,\n fields: ConfigField<T>[],\n overrides?: Partial<T>,\n): Partial<T> {\n const runtimeConfig = getRuntimeConfig()\n const evlogNs = runtimeConfig?.evlog?.[namespace]\n const rootNs = runtimeConfig?.[namespace]\n\n const config: Record<string, unknown> = {}\n\n for (const { key, env } of fields) {\n config[key] =\n overrides?.[key]\n ?? evlogNs?.[key]\n ?? rootNs?.[key]\n ?? resolveEnv(env)\n }\n\n return config as Partial<T>\n}\n\nfunction resolveEnv(envKeys?: string[]): string | undefined {\n if (!envKeys) return undefined\n for (const key of envKeys) {\n const val = process.env[key]\n if (val) return val\n }\n return undefined\n}\n","import type { DrainContext, WideEvent } from '../types'\n\nexport interface DrainOptions<TConfig> {\n name: string\n resolve: () => TConfig | null\n send: (events: WideEvent[], config: TConfig) => Promise<void>\n}\n\nexport function defineDrain<TConfig>(options: DrainOptions<TConfig>): (ctx: DrainContext | DrainContext[]) => Promise<void> {\n return async (ctx: DrainContext | DrainContext[]) => {\n const contexts = Array.isArray(ctx) ? ctx : [ctx]\n if (contexts.length === 0) return\n\n const config = options.resolve()\n if (!config) return\n\n try {\n await options.send(contexts.map(c => c.event), config)\n } catch (error) {\n console.error(`[evlog/${options.name}] Failed to send events:`, error)\n }\n }\n}\n","export interface HttpPostOptions {\n url: string\n headers: Record<string, string>\n body: string\n timeout: number\n label: string\n}\n\nexport async function httpPost({ url, headers, body, timeout, label }: HttpPostOptions): Promise<void> {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body,\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const text = await response.text().catch(() => 'Unknown error')\n const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text\n throw new Error(`${label} API error: ${response.status} ${response.statusText} - ${safeText}`)\n }\n } finally {\n clearTimeout(timeoutId)\n }\n}\n"],"mappings":";;;;;;;;;;;AAIA,SAAgB,mBAAoD;AAClE,KAAI;EAEF,MAAM,EAAE,+BAA6B,oBAAoB;AACzD,SAAO,kBAAkB;SACnB;AACN;;;AASJ,SAAgB,qBACd,WACA,QACA,WACY;CACZ,MAAM,gBAAgB,kBAAkB;CACxC,MAAM,UAAU,eAAe,QAAQ;CACvC,MAAM,SAAS,gBAAgB;CAE/B,MAAM,SAAkC,EAAE;AAE1C,MAAK,MAAM,EAAE,KAAK,SAAS,OACzB,QAAO,OACL,YAAY,QACT,UAAU,QACV,SAAS,QACT,WAAW,IAAI;AAGtB,QAAO;;AAGT,SAAS,WAAW,SAAwC;AAC1D,KAAI,CAAC,QAAS,QAAO;AACrB,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,IAAK,QAAO;;;;;;ACrCpB,SAAgB,YAAqB,SAAuF;AAC1H,QAAO,OAAO,QAAuC;EACnD,MAAM,WAAW,MAAM,QAAQ,IAAI,GAAG,MAAM,CAAC,IAAI;AACjD,MAAI,SAAS,WAAW,EAAG;EAE3B,MAAM,SAAS,QAAQ,SAAS;AAChC,MAAI,CAAC,OAAQ;AAEb,MAAI;AACF,SAAM,QAAQ,KAAK,SAAS,KAAI,MAAK,EAAE,MAAM,EAAE,OAAO;WAC/C,OAAO;AACd,WAAQ,MAAM,UAAU,QAAQ,KAAK,2BAA2B,MAAM;;;;;;;ACX5E,eAAsB,SAAS,EAAE,KAAK,SAAS,MAAM,SAAS,SAAyC;CACrG,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,QAAQ;AAE/D,KAAI;EACF,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ;GACR;GACA;GACA,QAAQ,WAAW;GACpB,CAAC;AAEF,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,YAAY,gBAAgB;GAC/D,MAAM,WAAW,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,kBAAkB;AAC7E,SAAM,IAAI,MAAM,GAAG,MAAM,cAAc,SAAS,OAAO,GAAG,SAAS,WAAW,KAAK,WAAW;;WAExF;AACR,eAAa,UAAU"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/adapters/_severity.ts
|
|
2
|
+
const OTEL_SEVERITY_NUMBER = {
|
|
3
|
+
debug: 5,
|
|
4
|
+
info: 9,
|
|
5
|
+
warn: 13,
|
|
6
|
+
error: 17
|
|
7
|
+
};
|
|
8
|
+
const OTEL_SEVERITY_TEXT = {
|
|
9
|
+
debug: "DEBUG",
|
|
10
|
+
info: "INFO",
|
|
11
|
+
warn: "WARN",
|
|
12
|
+
error: "ERROR"
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { OTEL_SEVERITY_TEXT as n, OTEL_SEVERITY_NUMBER as t };
|
|
17
|
+
//# sourceMappingURL=_severity-CXfyvxQi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_severity-CXfyvxQi.mjs","names":[],"sources":["../src/adapters/_severity.ts"],"sourcesContent":["import type { LogLevel } from '../types'\n\nexport const OTEL_SEVERITY_NUMBER: Record<LogLevel, number> = {\n debug: 5,\n info: 9,\n warn: 13,\n error: 17,\n}\n\nexport const OTEL_SEVERITY_TEXT: Record<LogLevel, string> = {\n debug: 'DEBUG',\n info: 'INFO',\n warn: 'WARN',\n error: 'ERROR',\n}\n"],"mappings":";AAEA,MAAa,uBAAiD;CAC5D,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACR;AAED,MAAa,qBAA+C;CAC1D,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"axiom.d.mts","names":[],"sources":["../../src/adapters/axiom.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"axiom.d.mts","names":[],"sources":["../../src/adapters/axiom.ts"],"mappings":";;;;UAMiB,WAAA;;EAEf,OAAA;EAFe;EAIf,KAAA;;EAEA,KAAA;EAJA;EAMA,OAAA;EAFA;EAIA,OAAA;AAAA;;;AA+BF;;;;;;;;;;;;;;;;;;iBAAgB,gBAAA,CAAiB,SAAA,GAAY,OAAA,CAAQ,WAAA,KAAY,GAAA,EAAb,YAAA,GAAa,YAAA,OAAA,OAAA;AA0BjE;;;;;;;;;;;AAAA,iBAAsB,WAAA,CAAY,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,WAAA,GAAc,OAAA;;;;AAe1E;;;;;;;;iBAAsB,gBAAA,CAAiB,MAAA,EAAQ,SAAA,IAAa,MAAA,EAAQ,WAAA,GAAc,OAAA"}
|
package/dist/adapters/axiom.mjs
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
import { t as
|
|
1
|
+
import { n as defineDrain, r as resolveAdapterConfig, t as httpPost } from "../_http-DVDwNag0.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/adapters/axiom.ts
|
|
4
|
+
const AXIOM_FIELDS = [
|
|
5
|
+
{
|
|
6
|
+
key: "dataset",
|
|
7
|
+
env: ["NUXT_AXIOM_DATASET", "AXIOM_DATASET"]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
key: "token",
|
|
11
|
+
env: ["NUXT_AXIOM_TOKEN", "AXIOM_TOKEN"]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
key: "orgId",
|
|
15
|
+
env: ["NUXT_AXIOM_ORG_ID", "AXIOM_ORG_ID"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: "baseUrl",
|
|
19
|
+
env: ["NUXT_AXIOM_URL", "AXIOM_URL"]
|
|
20
|
+
},
|
|
21
|
+
{ key: "timeout" }
|
|
22
|
+
];
|
|
4
23
|
/**
|
|
5
24
|
* Create a drain function for sending logs to Axiom.
|
|
6
25
|
*
|
|
@@ -22,29 +41,18 @@ import { t as getRuntimeConfig } from "../_utils-DZA9nou3.mjs";
|
|
|
22
41
|
* ```
|
|
23
42
|
*/
|
|
24
43
|
function createAxiomDrain(overrides) {
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
if (!config.dataset || !config.token) {
|
|
39
|
-
console.error("[evlog/axiom] Missing dataset or token. Set NUXT_AXIOM_TOKEN/NUXT_AXIOM_DATASET env vars or pass to createAxiomDrain()");
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
await sendBatchToAxiom(contexts.map((c) => c.event), config);
|
|
44
|
-
} catch (error) {
|
|
45
|
-
console.error("[evlog/axiom] Failed to send events to Axiom:", error);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
44
|
+
return defineDrain({
|
|
45
|
+
name: "axiom",
|
|
46
|
+
resolve: () => {
|
|
47
|
+
const config = resolveAdapterConfig("axiom", AXIOM_FIELDS, overrides);
|
|
48
|
+
if (!config.dataset || !config.token) {
|
|
49
|
+
console.error("[evlog/axiom] Missing dataset or token. Set NUXT_AXIOM_TOKEN/NUXT_AXIOM_DATASET env vars or pass to createAxiomDrain()");
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return config;
|
|
53
|
+
},
|
|
54
|
+
send: sendBatchToAxiom
|
|
55
|
+
});
|
|
48
56
|
}
|
|
49
57
|
/**
|
|
50
58
|
* Send a single event to Axiom.
|
|
@@ -72,31 +80,19 @@ async function sendToAxiom(event, config) {
|
|
|
72
80
|
* ```
|
|
73
81
|
*/
|
|
74
82
|
async function sendBatchToAxiom(events, config) {
|
|
75
|
-
const
|
|
76
|
-
const timeout = config.timeout ?? 5e3;
|
|
77
|
-
const url = `${baseUrl}/v1/datasets/${encodeURIComponent(config.dataset)}/ingest`;
|
|
83
|
+
const url = `${config.baseUrl ?? "https://api.axiom.co"}/v1/datasets/${encodeURIComponent(config.dataset)}/ingest`;
|
|
78
84
|
const headers = {
|
|
79
85
|
"Content-Type": "application/json",
|
|
80
86
|
"Authorization": `Bearer ${config.token}`
|
|
81
87
|
};
|
|
82
88
|
if (config.orgId) headers["X-Axiom-Org-Id"] = config.orgId;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
signal: controller.signal
|
|
91
|
-
});
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
const text = await response.text().catch(() => "Unknown error");
|
|
94
|
-
const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
|
|
95
|
-
throw new Error(`Axiom API error: ${response.status} ${response.statusText} - ${safeText}`);
|
|
96
|
-
}
|
|
97
|
-
} finally {
|
|
98
|
-
clearTimeout(timeoutId);
|
|
99
|
-
}
|
|
89
|
+
await httpPost({
|
|
90
|
+
url,
|
|
91
|
+
headers,
|
|
92
|
+
body: JSON.stringify(events),
|
|
93
|
+
timeout: config.timeout ?? 5e3,
|
|
94
|
+
label: "Axiom"
|
|
95
|
+
});
|
|
100
96
|
}
|
|
101
97
|
|
|
102
98
|
//#endregion
|