evlog 1.3.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.**
@@ -389,6 +390,85 @@ Notes:
389
390
  - `request.cf` is included (colo, country, asn) unless disabled
390
391
  - Use `headerAllowlist` to avoid logging sensitive headers
391
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
+
392
472
  ## API Reference
393
473
 
394
474
  ### `initLogger(config)`
@@ -15,6 +15,15 @@ function shouldLog(path, include, exclude) {
15
15
  }
16
16
  return include.some((pattern) => matchesPattern(path, pattern));
17
17
  }
18
+ function getServiceForPath(path, routes) {
19
+ if (!routes) return void 0;
20
+ for (const [pattern, config] of Object.entries(routes)) {
21
+ if (matchesPattern(path, pattern)) {
22
+ return config.service;
23
+ }
24
+ }
25
+ return void 0;
26
+ }
18
27
  const SENSITIVE_HEADERS = [
19
28
  "authorization",
20
29
  "cookie",
@@ -46,14 +55,17 @@ function getResponseStatus(event) {
46
55
  return 200;
47
56
  }
48
57
  function callDrainHook(nitroApp, emittedEvent, event) {
49
- if (emittedEvent) {
50
- nitroApp.hooks.callHook("evlog:drain", {
51
- event: emittedEvent,
52
- request: { method: event.method, path: event.path, requestId: event.context.requestId },
53
- headers: getSafeHeaders(event)
54
- }).catch((err) => {
55
- console.error("[evlog] drain failed:", err);
56
- });
58
+ if (!emittedEvent) return;
59
+ const drainPromise = nitroApp.hooks.callHook("evlog:drain", {
60
+ event: emittedEvent,
61
+ request: { method: event.method, path: event.path, requestId: event.context.requestId },
62
+ headers: getSafeHeaders(event)
63
+ }).catch((err) => {
64
+ console.error("[evlog] drain failed:", err);
65
+ });
66
+ const waitUntil = event.context.cloudflare?.context?.waitUntil ?? event.context.waitUntil;
67
+ if (typeof waitUntil === "function") {
68
+ waitUntil(drainPromise);
57
69
  }
58
70
  }
59
71
  const plugin = defineNitroPlugin((nitroApp) => {
@@ -75,6 +87,10 @@ const plugin = defineNitroPlugin((nitroApp) => {
75
87
  path: e.path,
76
88
  requestId: e.context.requestId || crypto.randomUUID()
77
89
  });
90
+ const routeService = getServiceForPath(e.path, evlogConfig?.routes);
91
+ if (routeService) {
92
+ log.set({ service: routeService });
93
+ }
78
94
  e.context.log = log;
79
95
  });
80
96
  nitroApp.hooks.hook("error", async (error, { event }) => {
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { EnvironmentContext, SamplingConfig, TransportConfig } from '../types.mjs';
2
+ import { EnvironmentContext, RouteConfig, SamplingConfig, TransportConfig } from '../types.mjs';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
@@ -25,6 +25,20 @@ interface ModuleOptions {
25
25
  * @example ['/api/_nuxt_icon/**', '/health']
26
26
  */
27
27
  exclude?: string[];
28
+ /**
29
+ * Route-specific service configuration.
30
+ * Allows setting different service names for different routes.
31
+ * Patterns are matched using glob syntax.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * routes: {
36
+ * '/api/foo/**': { service: 'service1' },
37
+ * '/api/bar/**': { service: 'service2' }
38
+ * }
39
+ * ```
40
+ */
41
+ routes?: Record<string, RouteConfig>;
28
42
  /**
29
43
  * Sampling configuration for filtering logs.
30
44
  * Allows configuring what percentage of logs to keep per level.
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { EnvironmentContext, SamplingConfig, TransportConfig } from '../types.js';
2
+ import { EnvironmentContext, RouteConfig, SamplingConfig, TransportConfig } from '../types.js';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
@@ -25,6 +25,20 @@ interface ModuleOptions {
25
25
  * @example ['/api/_nuxt_icon/**', '/health']
26
26
  */
27
27
  exclude?: string[];
28
+ /**
29
+ * Route-specific service configuration.
30
+ * Allows setting different service names for different routes.
31
+ * Patterns are matched using glob syntax.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * routes: {
36
+ * '/api/foo/**': { service: 'service1' },
37
+ * '/api/bar/**': { service: 'service2' }
38
+ * }
39
+ * ```
40
+ */
41
+ routes?: Record<string, RouteConfig>;
28
42
  /**
29
43
  * Sampling configuration for filtering logs.
30
44
  * Allows configuring what percentage of logs to keep per level.
@@ -4,6 +4,7 @@ import { ServerEvent, RequestLogger } from '../../types.mjs';
4
4
  * Returns the request logger attached to the given server event.
5
5
  *
6
6
  * @param event - The current server event containing the context with the logger.
7
+ * @param service - Optional service name to override the default service.
7
8
  * @returns The request-scoped logger.
8
9
  * @throws Error if the logger is not initialized on the event context.
9
10
  *
@@ -13,7 +14,15 @@ import { ServerEvent, RequestLogger } from '../../types.mjs';
13
14
  * log.set({ foo: 'bar' })
14
15
  * // ...
15
16
  * })
17
+ *
18
+ * @example
19
+ * // Override service name for specific routes
20
+ * export default defineEventHandler((event) => {
21
+ * const log = useLogger(event, 'payment-service')
22
+ * log.set({ foo: 'bar' })
23
+ * // ...
24
+ * })
16
25
  */
17
- declare function useLogger(event: ServerEvent): RequestLogger;
26
+ declare function useLogger(event: ServerEvent, service?: string): RequestLogger;
18
27
 
19
28
  export { useLogger };
@@ -4,6 +4,7 @@ import { ServerEvent, RequestLogger } from '../../types.js';
4
4
  * Returns the request logger attached to the given server event.
5
5
  *
6
6
  * @param event - The current server event containing the context with the logger.
7
+ * @param service - Optional service name to override the default service.
7
8
  * @returns The request-scoped logger.
8
9
  * @throws Error if the logger is not initialized on the event context.
9
10
  *
@@ -13,7 +14,15 @@ import { ServerEvent, RequestLogger } from '../../types.js';
13
14
  * log.set({ foo: 'bar' })
14
15
  * // ...
15
16
  * })
17
+ *
18
+ * @example
19
+ * // Override service name for specific routes
20
+ * export default defineEventHandler((event) => {
21
+ * const log = useLogger(event, 'payment-service')
22
+ * log.set({ foo: 'bar' })
23
+ * // ...
24
+ * })
16
25
  */
17
- declare function useLogger(event: ServerEvent): RequestLogger;
26
+ declare function useLogger(event: ServerEvent, service?: string): RequestLogger;
18
27
 
19
28
  export { useLogger };
@@ -1,10 +1,13 @@
1
- function useLogger(event) {
1
+ function useLogger(event, service) {
2
2
  const log = event.context.log;
3
3
  if (!log) {
4
4
  throw new Error(
5
5
  '[evlog] Logger not initialized. Make sure the evlog Nitro plugin is registered. If using Nuxt, add "evlog" to your modules.'
6
6
  );
7
7
  }
8
+ if (service) {
9
+ log.set({ service });
10
+ }
8
11
  return log;
9
12
  }
10
13
 
package/dist/types.d.mts CHANGED
@@ -160,6 +160,13 @@ interface SamplingConfig {
160
160
  */
161
161
  keep?: TailSamplingCondition[];
162
162
  }
163
+ /**
164
+ * Route-based service configuration
165
+ */
166
+ interface RouteConfig {
167
+ /** Service name to use for routes matching this pattern */
168
+ service: string;
169
+ }
163
170
  /**
164
171
  * Environment context automatically included in every log event
165
172
  */
@@ -325,7 +332,16 @@ interface H3EventContext {
325
332
  interface ServerEvent {
326
333
  method: string;
327
334
  path: string;
328
- context: H3EventContext;
335
+ context: H3EventContext & {
336
+ /** Cloudflare Workers context (available when deployed to CF Workers) */
337
+ cloudflare?: {
338
+ context: {
339
+ waitUntil: (promise: Promise<unknown>) => void;
340
+ };
341
+ };
342
+ /** Vercel Edge context (available when deployed to Vercel Edge) */
343
+ waitUntil?: (promise: Promise<unknown>) => void;
344
+ };
329
345
  node?: {
330
346
  res?: {
331
347
  statusCode?: number;
@@ -345,4 +361,4 @@ interface ParsedError {
345
361
  raw: unknown;
346
362
  }
347
363
 
348
- export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent };
364
+ export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, RouteConfig, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent };
package/dist/types.d.ts CHANGED
@@ -160,6 +160,13 @@ interface SamplingConfig {
160
160
  */
161
161
  keep?: TailSamplingCondition[];
162
162
  }
163
+ /**
164
+ * Route-based service configuration
165
+ */
166
+ interface RouteConfig {
167
+ /** Service name to use for routes matching this pattern */
168
+ service: string;
169
+ }
163
170
  /**
164
171
  * Environment context automatically included in every log event
165
172
  */
@@ -325,7 +332,16 @@ interface H3EventContext {
325
332
  interface ServerEvent {
326
333
  method: string;
327
334
  path: string;
328
- context: H3EventContext;
335
+ context: H3EventContext & {
336
+ /** Cloudflare Workers context (available when deployed to CF Workers) */
337
+ cloudflare?: {
338
+ context: {
339
+ waitUntil: (promise: Promise<unknown>) => void;
340
+ };
341
+ };
342
+ /** Vercel Edge context (available when deployed to Vercel Edge) */
343
+ waitUntil?: (promise: Promise<unknown>) => void;
344
+ };
329
345
  node?: {
330
346
  res?: {
331
347
  statusCode?: number;
@@ -345,4 +361,4 @@ interface ParsedError {
345
361
  raw: unknown;
346
362
  }
347
363
 
348
- export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent };
364
+ export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, RouteConfig, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evlog",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Wide event logging library with structured error handling. Inspired by LoggingSucks.",
5
5
  "author": "HugoRCD <contact@hrcd.fr>",
6
6
  "homepage": "https://evlog.dev",