evlog 1.2.0 → 1.4.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 CHANGED
@@ -3,8 +3,9 @@
3
3
  [![npm version](https://img.shields.io/npm/v/evlog?color=black)](https://npmjs.com/package/evlog)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/evlog?color=black)](https://npm.chart.dev/evlog)
5
5
  [![CI](https://img.shields.io/github/actions/workflow/status/HugoRCD/evlog/ci.yml?branch=main&color=black)](https://github.com/HugoRCD/evlog/actions/workflows/ci.yml)
6
- [![bundle size](https://img.shields.io/bundlephobia/minzip/evlog?color=black&label=size)](https://bundlephobia.com/package/evlog)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-black?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
7
  [![Nuxt](https://img.shields.io/badge/Nuxt-black?logo=nuxt&logoColor=white)](https://nuxt.com/)
8
+ [![Documentation](https://img.shields.io/badge/Documentation-black?logo=readme&logoColor=white)](https://evlog.dev)
8
9
  [![license](https://img.shields.io/github/license/HugoRCD/evlog?color=black)](https://github.com/HugoRCD/evlog/blob/main/LICENSE)
9
10
 
10
11
  **Your logs are lying to you.**
@@ -346,6 +347,128 @@ async function processSyncJob(job: Job) {
346
347
  }
347
348
  ```
348
349
 
350
+ ## Cloudflare Workers
351
+
352
+ Use the Workers adapter for structured logs and correct platform severity.
353
+
354
+ ```typescript
355
+ // src/index.ts
356
+ import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
357
+
358
+ initWorkersLogger({
359
+ env: { service: 'edge-api' },
360
+ })
361
+
362
+ export default {
363
+ async fetch(request: Request) {
364
+ const log = createWorkersLogger(request)
365
+
366
+ try {
367
+ log.set({ route: 'health' })
368
+ const response = new Response('ok', { status: 200 })
369
+ log.emit({ status: response.status })
370
+ return response
371
+ } catch (error) {
372
+ log.error(error as Error)
373
+ log.emit({ status: 500 })
374
+ throw error
375
+ }
376
+ },
377
+ }
378
+ ```
379
+
380
+ Disable invocation logs to avoid duplicate request logs:
381
+
382
+ ```toml
383
+ # wrangler.toml
384
+ [observability.logs]
385
+ invocation_logs = false
386
+ ```
387
+
388
+ Notes:
389
+ - `requestId` defaults to `cf-ray` when available
390
+ - `request.cf` is included (colo, country, asn) unless disabled
391
+ - Use `headerAllowlist` to avoid logging sensitive headers
392
+
393
+ ## Adapters
394
+
395
+ Send your logs to external observability platforms with built-in adapters.
396
+
397
+ ### Axiom
398
+
399
+ ```typescript
400
+ // server/plugins/evlog-drain.ts
401
+ import { createAxiomDrain } from 'evlog/axiom'
402
+
403
+ export default defineNitroPlugin((nitroApp) => {
404
+ nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
405
+ })
406
+ ```
407
+
408
+ Set environment variables:
409
+
410
+ ```bash
411
+ NUXT_AXIOM_TOKEN=xaat-your-token
412
+ NUXT_AXIOM_DATASET=your-dataset
413
+ ```
414
+
415
+ ### OTLP (OpenTelemetry)
416
+
417
+ Works with Grafana, Datadog, Honeycomb, and any OTLP-compatible backend.
418
+
419
+ ```typescript
420
+ // server/plugins/evlog-drain.ts
421
+ import { createOTLPDrain } from 'evlog/otlp'
422
+
423
+ export default defineNitroPlugin((nitroApp) => {
424
+ nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
425
+ })
426
+ ```
427
+
428
+ Set environment variables:
429
+
430
+ ```bash
431
+ NUXT_OTLP_ENDPOINT=http://localhost:4318
432
+ ```
433
+
434
+ ### Multiple Destinations
435
+
436
+ Send logs to multiple services:
437
+
438
+ ```typescript
439
+ // server/plugins/evlog-drain.ts
440
+ import { createAxiomDrain } from 'evlog/axiom'
441
+ import { createOTLPDrain } from 'evlog/otlp'
442
+
443
+ export default defineNitroPlugin((nitroApp) => {
444
+ const axiom = createAxiomDrain()
445
+ const otlp = createOTLPDrain()
446
+
447
+ nitroApp.hooks.hook('evlog:drain', async (ctx) => {
448
+ await Promise.allSettled([axiom(ctx), otlp(ctx)])
449
+ })
450
+ })
451
+ ```
452
+
453
+ ### Custom Adapters
454
+
455
+ Build your own adapter for any destination:
456
+
457
+ ```typescript
458
+ // server/plugins/evlog-drain.ts
459
+ export default defineNitroPlugin((nitroApp) => {
460
+ nitroApp.hooks.hook('evlog:drain', async (ctx) => {
461
+ await fetch('https://your-service.com/logs', {
462
+ method: 'POST',
463
+ headers: { 'Content-Type': 'application/json' },
464
+ body: JSON.stringify(ctx.event),
465
+ })
466
+ })
467
+ })
468
+ ```
469
+
470
+ > See the [full documentation](https://evlog.hrcd.fr/adapters/overview) for adapter configuration options, troubleshooting, and advanced patterns.
471
+
349
472
  ## API Reference
350
473
 
351
474
  ### `initLogger(config)`
@@ -362,6 +485,7 @@ initLogger({
362
485
  region?: string // Deployment region
363
486
  },
364
487
  pretty?: boolean // Pretty print (default: true in dev)
488
+ stringify?: boolean // JSON.stringify output (default: true, false for Workers)
365
489
  include?: string[] // Route patterns to log (glob), e.g. ['/api/**']
366
490
  sampling?: {
367
491
  rates?: { // Head sampling (random per level)
@@ -479,6 +603,34 @@ log.emit() // Emit final event
479
603
  log.getContext() // Get current context
480
604
  ```
481
605
 
606
+ ### `initWorkersLogger(options?)`
607
+
608
+ Initialize evlog for Cloudflare Workers (object logs + correct severity).
609
+
610
+ ```typescript
611
+ import { initWorkersLogger } from 'evlog/workers'
612
+
613
+ initWorkersLogger({
614
+ env: { service: 'edge-api' },
615
+ })
616
+ ```
617
+
618
+ ### `createWorkersLogger(request, options?)`
619
+
620
+ Create a request-scoped logger for Workers. Auto-extracts `cf-ray`, `request.cf`, method, and path.
621
+
622
+ ```typescript
623
+ import { createWorkersLogger } from 'evlog/workers'
624
+
625
+ const log = createWorkersLogger(request, {
626
+ requestId: 'custom-id', // Override cf-ray (default: cf-ray header)
627
+ headers: ['x-request-id'], // Headers to include (default: none)
628
+ })
629
+
630
+ log.set({ user: { id: '123' } })
631
+ log.emit({ status: 200 })
632
+ ```
633
+
482
634
  ### `createError(options)`
483
635
 
484
636
  Create a structured error with HTTP status support. Import from `evlog` directly to avoid conflicts with Nuxt/Nitro's `createError`.
@@ -0,0 +1,62 @@
1
+ import { DrainContext, WideEvent } from '../types.mjs';
2
+
3
+ interface AxiomConfig {
4
+ /** Axiom dataset name */
5
+ dataset: string;
6
+ /** Axiom API token */
7
+ token: string;
8
+ /** Organization ID (required for Personal Access Tokens) */
9
+ orgId?: string;
10
+ /** Base URL for Axiom API. Default: https://api.axiom.co */
11
+ baseUrl?: string;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /**
16
+ * Create a drain function for sending logs to Axiom.
17
+ *
18
+ * Configuration priority (highest to lowest):
19
+ * 1. Overrides passed to createAxiomDrain()
20
+ * 2. runtimeConfig.evlog.axiom
21
+ * 3. runtimeConfig.axiom
22
+ * 4. Environment variables: NUXT_AXIOM_*, AXIOM_*
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // Zero config - just set NUXT_AXIOM_TOKEN and NUXT_AXIOM_DATASET env vars
27
+ * nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
28
+ *
29
+ * // With overrides
30
+ * nitroApp.hooks.hook('evlog:drain', createAxiomDrain({
31
+ * dataset: 'my-dataset',
32
+ * }))
33
+ * ```
34
+ */
35
+ declare function createAxiomDrain(overrides?: Partial<AxiomConfig>): (ctx: DrainContext) => Promise<void>;
36
+ /**
37
+ * Send a single event to Axiom.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * await sendToAxiom(event, {
42
+ * dataset: 'my-logs',
43
+ * token: process.env.AXIOM_TOKEN!,
44
+ * })
45
+ * ```
46
+ */
47
+ declare function sendToAxiom(event: WideEvent, config: AxiomConfig): Promise<void>;
48
+ /**
49
+ * Send a batch of events to Axiom.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * await sendBatchToAxiom(events, {
54
+ * dataset: 'my-logs',
55
+ * token: process.env.AXIOM_TOKEN!,
56
+ * })
57
+ * ```
58
+ */
59
+ declare function sendBatchToAxiom(events: WideEvent[], config: AxiomConfig): Promise<void>;
60
+
61
+ export { createAxiomDrain, sendBatchToAxiom, sendToAxiom };
62
+ export type { AxiomConfig };
@@ -0,0 +1,62 @@
1
+ import { DrainContext, WideEvent } from '../types.js';
2
+
3
+ interface AxiomConfig {
4
+ /** Axiom dataset name */
5
+ dataset: string;
6
+ /** Axiom API token */
7
+ token: string;
8
+ /** Organization ID (required for Personal Access Tokens) */
9
+ orgId?: string;
10
+ /** Base URL for Axiom API. Default: https://api.axiom.co */
11
+ baseUrl?: string;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /**
16
+ * Create a drain function for sending logs to Axiom.
17
+ *
18
+ * Configuration priority (highest to lowest):
19
+ * 1. Overrides passed to createAxiomDrain()
20
+ * 2. runtimeConfig.evlog.axiom
21
+ * 3. runtimeConfig.axiom
22
+ * 4. Environment variables: NUXT_AXIOM_*, AXIOM_*
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // Zero config - just set NUXT_AXIOM_TOKEN and NUXT_AXIOM_DATASET env vars
27
+ * nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
28
+ *
29
+ * // With overrides
30
+ * nitroApp.hooks.hook('evlog:drain', createAxiomDrain({
31
+ * dataset: 'my-dataset',
32
+ * }))
33
+ * ```
34
+ */
35
+ declare function createAxiomDrain(overrides?: Partial<AxiomConfig>): (ctx: DrainContext) => Promise<void>;
36
+ /**
37
+ * Send a single event to Axiom.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * await sendToAxiom(event, {
42
+ * dataset: 'my-logs',
43
+ * token: process.env.AXIOM_TOKEN!,
44
+ * })
45
+ * ```
46
+ */
47
+ declare function sendToAxiom(event: WideEvent, config: AxiomConfig): Promise<void>;
48
+ /**
49
+ * Send a batch of events to Axiom.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * await sendBatchToAxiom(events, {
54
+ * dataset: 'my-logs',
55
+ * token: process.env.AXIOM_TOKEN!,
56
+ * })
57
+ * ```
58
+ */
59
+ declare function sendBatchToAxiom(events: WideEvent[], config: AxiomConfig): Promise<void>;
60
+
61
+ export { createAxiomDrain, sendBatchToAxiom, sendToAxiom };
62
+ export type { AxiomConfig };
@@ -0,0 +1,65 @@
1
+ function getRuntimeConfig() {
2
+ try {
3
+ const { useRuntimeConfig } = require("nitropack/runtime");
4
+ return useRuntimeConfig();
5
+ } catch {
6
+ return void 0;
7
+ }
8
+ }
9
+ function createAxiomDrain(overrides) {
10
+ return async (ctx) => {
11
+ const runtimeConfig = getRuntimeConfig();
12
+ const evlogAxiom = runtimeConfig?.evlog?.axiom;
13
+ const rootAxiom = runtimeConfig?.axiom;
14
+ const config = {
15
+ dataset: overrides?.dataset ?? evlogAxiom?.dataset ?? rootAxiom?.dataset ?? process.env.NUXT_AXIOM_DATASET ?? process.env.AXIOM_DATASET,
16
+ token: overrides?.token ?? evlogAxiom?.token ?? rootAxiom?.token ?? process.env.NUXT_AXIOM_TOKEN ?? process.env.AXIOM_TOKEN,
17
+ orgId: overrides?.orgId ?? evlogAxiom?.orgId ?? rootAxiom?.orgId ?? process.env.NUXT_AXIOM_ORG_ID ?? process.env.AXIOM_ORG_ID,
18
+ baseUrl: overrides?.baseUrl ?? evlogAxiom?.baseUrl ?? rootAxiom?.baseUrl ?? process.env.NUXT_AXIOM_URL ?? process.env.AXIOM_URL,
19
+ timeout: overrides?.timeout ?? evlogAxiom?.timeout ?? rootAxiom?.timeout
20
+ };
21
+ if (!config.dataset || !config.token) {
22
+ console.error("[evlog/axiom] Missing dataset or token. Set NUXT_AXIOM_TOKEN/NUXT_AXIOM_DATASET env vars or pass to createAxiomDrain()");
23
+ return;
24
+ }
25
+ try {
26
+ await sendToAxiom(ctx.event, config);
27
+ } catch (error) {
28
+ console.error("[evlog/axiom] Failed to send event:", error);
29
+ }
30
+ };
31
+ }
32
+ async function sendToAxiom(event, config) {
33
+ await sendBatchToAxiom([event], config);
34
+ }
35
+ async function sendBatchToAxiom(events, config) {
36
+ const baseUrl = config.baseUrl ?? "https://api.axiom.co";
37
+ const timeout = config.timeout ?? 5e3;
38
+ const url = `${baseUrl}/v1/datasets/${encodeURIComponent(config.dataset)}/ingest`;
39
+ const headers = {
40
+ "Content-Type": "application/json",
41
+ "Authorization": `Bearer ${config.token}`
42
+ };
43
+ if (config.orgId) {
44
+ headers["X-Axiom-Org-Id"] = config.orgId;
45
+ }
46
+ const controller = new AbortController();
47
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
48
+ try {
49
+ const response = await fetch(url, {
50
+ method: "POST",
51
+ headers,
52
+ body: JSON.stringify(events),
53
+ signal: controller.signal
54
+ });
55
+ if (!response.ok) {
56
+ const text = await response.text().catch(() => "Unknown error");
57
+ const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
58
+ throw new Error(`Axiom API error: ${response.status} ${response.statusText} - ${safeText}`);
59
+ }
60
+ } finally {
61
+ clearTimeout(timeoutId);
62
+ }
63
+ }
64
+
65
+ export { createAxiomDrain, sendBatchToAxiom, sendToAxiom };
@@ -0,0 +1,83 @@
1
+ import { DrainContext, WideEvent } from '../types.mjs';
2
+
3
+ interface OTLPConfig {
4
+ /** OTLP HTTP endpoint (e.g., http://localhost:4318) */
5
+ endpoint: string;
6
+ /** Override service name (defaults to event.service) */
7
+ serviceName?: string;
8
+ /** Additional resource attributes */
9
+ resourceAttributes?: Record<string, string | number | boolean>;
10
+ /** Custom headers (e.g., for authentication) */
11
+ headers?: Record<string, string>;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /** OTLP Log Record structure */
16
+ interface OTLPLogRecord {
17
+ timeUnixNano: string;
18
+ severityNumber: number;
19
+ severityText: string;
20
+ body: {
21
+ stringValue: string;
22
+ };
23
+ attributes: Array<{
24
+ key: string;
25
+ value: {
26
+ stringValue?: string;
27
+ intValue?: string;
28
+ boolValue?: boolean;
29
+ };
30
+ }>;
31
+ traceId?: string;
32
+ spanId?: string;
33
+ }
34
+ /**
35
+ * Convert an evlog WideEvent to an OTLP LogRecord.
36
+ */
37
+ declare function toOTLPLogRecord(event: WideEvent): OTLPLogRecord;
38
+ /**
39
+ * Create a drain function for sending logs to an OTLP endpoint.
40
+ *
41
+ * Configuration priority (highest to lowest):
42
+ * 1. Overrides passed to createOTLPDrain()
43
+ * 2. runtimeConfig.evlog.otlp (NUXT_EVLOG_OTLP_*)
44
+ * 3. runtimeConfig.otlp (NUXT_OTLP_*)
45
+ * 4. Environment variables: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * // Zero config - reads from runtimeConfig or env vars
50
+ * nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
51
+ *
52
+ * // With overrides
53
+ * nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
54
+ * endpoint: 'http://localhost:4318',
55
+ * }))
56
+ * ```
57
+ */
58
+ declare function createOTLPDrain(overrides?: Partial<OTLPConfig>): (ctx: DrainContext) => Promise<void>;
59
+ /**
60
+ * Send a single event to an OTLP endpoint.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * await sendToOTLP(event, {
65
+ * endpoint: 'http://localhost:4318',
66
+ * })
67
+ * ```
68
+ */
69
+ declare function sendToOTLP(event: WideEvent, config: OTLPConfig): Promise<void>;
70
+ /**
71
+ * Send a batch of events to an OTLP endpoint.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * await sendBatchToOTLP(events, {
76
+ * endpoint: 'http://localhost:4318',
77
+ * })
78
+ * ```
79
+ */
80
+ declare function sendBatchToOTLP(events: WideEvent[], config: OTLPConfig): Promise<void>;
81
+
82
+ export { createOTLPDrain, sendBatchToOTLP, sendToOTLP, toOTLPLogRecord };
83
+ export type { OTLPConfig, OTLPLogRecord };
@@ -0,0 +1,83 @@
1
+ import { DrainContext, WideEvent } from '../types.js';
2
+
3
+ interface OTLPConfig {
4
+ /** OTLP HTTP endpoint (e.g., http://localhost:4318) */
5
+ endpoint: string;
6
+ /** Override service name (defaults to event.service) */
7
+ serviceName?: string;
8
+ /** Additional resource attributes */
9
+ resourceAttributes?: Record<string, string | number | boolean>;
10
+ /** Custom headers (e.g., for authentication) */
11
+ headers?: Record<string, string>;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /** OTLP Log Record structure */
16
+ interface OTLPLogRecord {
17
+ timeUnixNano: string;
18
+ severityNumber: number;
19
+ severityText: string;
20
+ body: {
21
+ stringValue: string;
22
+ };
23
+ attributes: Array<{
24
+ key: string;
25
+ value: {
26
+ stringValue?: string;
27
+ intValue?: string;
28
+ boolValue?: boolean;
29
+ };
30
+ }>;
31
+ traceId?: string;
32
+ spanId?: string;
33
+ }
34
+ /**
35
+ * Convert an evlog WideEvent to an OTLP LogRecord.
36
+ */
37
+ declare function toOTLPLogRecord(event: WideEvent): OTLPLogRecord;
38
+ /**
39
+ * Create a drain function for sending logs to an OTLP endpoint.
40
+ *
41
+ * Configuration priority (highest to lowest):
42
+ * 1. Overrides passed to createOTLPDrain()
43
+ * 2. runtimeConfig.evlog.otlp (NUXT_EVLOG_OTLP_*)
44
+ * 3. runtimeConfig.otlp (NUXT_OTLP_*)
45
+ * 4. Environment variables: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * // Zero config - reads from runtimeConfig or env vars
50
+ * nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
51
+ *
52
+ * // With overrides
53
+ * nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
54
+ * endpoint: 'http://localhost:4318',
55
+ * }))
56
+ * ```
57
+ */
58
+ declare function createOTLPDrain(overrides?: Partial<OTLPConfig>): (ctx: DrainContext) => Promise<void>;
59
+ /**
60
+ * Send a single event to an OTLP endpoint.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * await sendToOTLP(event, {
65
+ * endpoint: 'http://localhost:4318',
66
+ * })
67
+ * ```
68
+ */
69
+ declare function sendToOTLP(event: WideEvent, config: OTLPConfig): Promise<void>;
70
+ /**
71
+ * Send a batch of events to an OTLP endpoint.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * await sendBatchToOTLP(events, {
76
+ * endpoint: 'http://localhost:4318',
77
+ * })
78
+ * ```
79
+ */
80
+ declare function sendBatchToOTLP(events: WideEvent[], config: OTLPConfig): Promise<void>;
81
+
82
+ export { createOTLPDrain, sendBatchToOTLP, sendToOTLP, toOTLPLogRecord };
83
+ export type { OTLPConfig, OTLPLogRecord };