evlog 0.1.0 → 1.0.1

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.
Files changed (39) hide show
  1. package/README.md +91 -12
  2. package/dist/error.d.mts +21 -11
  3. package/dist/error.d.ts +21 -11
  4. package/dist/error.mjs +12 -8
  5. package/dist/index.d.mts +4 -4
  6. package/dist/index.d.ts +4 -4
  7. package/dist/index.mjs +3 -2
  8. package/dist/logger.d.mts +1 -6
  9. package/dist/logger.d.ts +1 -6
  10. package/dist/logger.mjs +32 -16
  11. package/dist/nitro/plugin.mjs +41 -10
  12. package/dist/nuxt/module.d.mts +7 -0
  13. package/dist/nuxt/module.d.ts +7 -0
  14. package/dist/nuxt/module.mjs +11 -7
  15. package/dist/runtime/client/log.d.mts +9 -0
  16. package/dist/runtime/client/log.d.ts +9 -0
  17. package/dist/runtime/{composables → client}/log.mjs +2 -3
  18. package/dist/runtime/{plugin.client.mjs → client/plugin.mjs} +4 -3
  19. package/dist/runtime/server/useLogger.d.mts +5 -0
  20. package/dist/runtime/server/useLogger.d.ts +5 -0
  21. package/dist/runtime/utils/parseError.d.mts +5 -0
  22. package/dist/runtime/utils/parseError.d.ts +5 -0
  23. package/dist/runtime/utils/parseError.mjs +28 -0
  24. package/dist/types.d.mts +40 -2
  25. package/dist/types.d.ts +40 -2
  26. package/dist/utils.d.mts +3 -22
  27. package/dist/utils.d.ts +3 -22
  28. package/dist/utils.mjs +4 -1
  29. package/package.json +12 -8
  30. package/dist/runtime/composables/index.d.mts +0 -4
  31. package/dist/runtime/composables/index.d.ts +0 -4
  32. package/dist/runtime/composables/index.mjs +0 -2
  33. package/dist/runtime/composables/log.d.mts +0 -18
  34. package/dist/runtime/composables/log.d.ts +0 -18
  35. package/dist/runtime/composables/useLogger.d.mts +0 -21
  36. package/dist/runtime/composables/useLogger.d.ts +0 -21
  37. /package/dist/runtime/{plugin.client.d.mts → client/plugin.d.mts} +0 -0
  38. /package/dist/runtime/{plugin.client.d.ts → client/plugin.d.ts} +0 -0
  39. /package/dist/runtime/{composables → server}/useLogger.mjs +0 -0
package/README.md CHANGED
@@ -96,6 +96,8 @@ export default defineNuxtConfig({
96
96
  service: 'my-app',
97
97
  environment: process.env.NODE_ENV,
98
98
  },
99
+ // Optional: only log specific routes (supports glob patterns)
100
+ include: ['/api/**'],
99
101
  },
100
102
  })
101
103
  ```
@@ -104,7 +106,7 @@ That's it. Now use `useLogger(event)` in any API route:
104
106
 
105
107
  ```typescript
106
108
  // server/api/checkout.post.ts
107
- import { useLogger, defineError } from 'evlog'
109
+ import { useLogger, createError } from 'evlog'
108
110
 
109
111
  export default defineEventHandler(async (event) => {
110
112
  const log = useLogger(event)
@@ -124,8 +126,9 @@ export default defineEventHandler(async (event) => {
124
126
  } catch (error) {
125
127
  log.error(error, { step: 'payment' })
126
128
 
127
- throw defineError({
129
+ throw createError({
128
130
  message: 'Payment failed',
131
+ status: 402,
129
132
  why: error.message,
130
133
  fix: 'Try a different payment method or contact your bank',
131
134
  })
@@ -173,7 +176,7 @@ Same API, same wide events:
173
176
 
174
177
  ```typescript
175
178
  // routes/api/documents/[id]/export.post.ts
176
- import { useLogger, defineError } from 'evlog'
179
+ import { useLogger, createError } from 'evlog'
177
180
 
178
181
  export default defineEventHandler(async (event) => {
179
182
  const log = useLogger(event)
@@ -189,8 +192,9 @@ export default defineEventHandler(async (event) => {
189
192
  // Load document from database
190
193
  const document = await db.documents.findUnique({ where: { id: documentId } })
191
194
  if (!document) {
192
- throw defineError({
195
+ throw createError({
193
196
  message: 'Document not found',
197
+ status: 404,
194
198
  why: `No document with ID "${documentId}" exists`,
195
199
  fix: 'Check the document ID and try again',
196
200
  })
@@ -206,8 +210,9 @@ export default defineEventHandler(async (event) => {
206
210
  } catch (error) {
207
211
  log.error(error, { step: 'export-generation' })
208
212
 
209
- throw defineError({
213
+ throw createError({
210
214
  message: 'Export failed',
215
+ status: 500,
211
216
  why: `Failed to generate ${body.format} export: ${error.message}`,
212
217
  fix: 'Try a different format or contact support',
213
218
  })
@@ -238,7 +243,7 @@ Errors should tell you **what** happened, **why**, and **how to fix it**.
238
243
 
239
244
  ```typescript
240
245
  // server/api/repos/sync.post.ts
241
- import { useLogger, defineError } from 'evlog'
246
+ import { useLogger, createError } from 'evlog'
242
247
 
243
248
  export default defineEventHandler(async (event) => {
244
249
  const log = useLogger(event)
@@ -252,8 +257,9 @@ export default defineEventHandler(async (event) => {
252
257
  } catch (error) {
253
258
  log.error(error, { step: 'github-sync' })
254
259
 
255
- throw defineError({
260
+ throw createError({
256
261
  message: 'Failed to sync repository',
262
+ status: 503,
257
263
  why: 'GitHub API rate limit exceeded',
258
264
  fix: 'Wait 1 hour or use a different token',
259
265
  link: 'https://docs.github.com/en/rest/rate-limit',
@@ -302,7 +308,7 @@ migrationLog.emit()
302
308
 
303
309
  ```typescript
304
310
  // workers/sync-job.ts
305
- import { initLogger, createRequestLogger, defineError } from 'evlog'
311
+ import { initLogger, createRequestLogger, createError } from 'evlog'
306
312
 
307
313
  initLogger({
308
314
  env: {
@@ -346,9 +352,23 @@ initLogger({
346
352
  region?: string // Deployment region
347
353
  },
348
354
  pretty?: boolean // Pretty print (default: true in dev)
355
+ include?: string[] // Route patterns to log (glob), e.g. ['/api/**']
349
356
  })
350
357
  ```
351
358
 
359
+ ### Pretty Output Format
360
+
361
+ In development, evlog uses a compact tree format:
362
+
363
+ ```
364
+ 16:45:31.060 INFO [my-app] GET /api/checkout 200 in 234ms
365
+ ├─ user: id=123 plan=premium
366
+ ├─ cart: items=3 total=9999
367
+ └─ payment: id=pay_xyz method=card
368
+ ```
369
+
370
+ In production (`pretty: false`), logs are emitted as JSON for machine parsing.
371
+
352
372
  ### `log`
353
373
 
354
374
  Simple logging API.
@@ -378,13 +398,18 @@ log.emit() // Emit final event
378
398
  log.getContext() // Get current context
379
399
  ```
380
400
 
381
- ### `defineError(options)`
401
+ ### `createError(options)`
402
+
403
+ Create a structured error with HTTP status support. Import from `evlog` directly to avoid conflicts with Nuxt/Nitro's `createError`.
382
404
 
383
- Create a structured error.
405
+ > **Note**: `createEvlogError` is also available as an auto-imported alias in Nuxt/Nitro to avoid conflicts.
384
406
 
385
407
  ```typescript
386
- defineError({
408
+ import { createError } from 'evlog'
409
+
410
+ createError({
387
411
  message: string // What happened
412
+ status?: number // HTTP status code (default: 500)
388
413
  why?: string // Why it happened
389
414
  fix?: string // How to fix it
390
415
  link?: string // Documentation URL
@@ -392,6 +417,34 @@ defineError({
392
417
  })
393
418
  ```
394
419
 
420
+ ### `parseError(error)`
421
+
422
+ Parse a caught error into a flat structure with all evlog fields. Auto-imported in Nuxt.
423
+
424
+ ```typescript
425
+ import { parseError } from 'evlog'
426
+
427
+ try {
428
+ await $fetch('/api/checkout')
429
+ } catch (err) {
430
+ const error = parseError(err)
431
+
432
+ // Direct access to all fields
433
+ console.log(error.message) // "Payment failed"
434
+ console.log(error.status) // 402
435
+ console.log(error.why) // "Card declined"
436
+ console.log(error.fix) // "Try another card"
437
+ console.log(error.link) // "https://docs.example.com/..."
438
+
439
+ // Use with toast
440
+ toast.add({
441
+ title: error.message,
442
+ description: error.why,
443
+ color: 'error',
444
+ })
445
+ }
446
+ ```
447
+
395
448
  ## Framework Support
396
449
 
397
450
  evlog works with any framework powered by [Nitro](https://nitro.unjs.io/):
@@ -405,9 +458,35 @@ evlog works with any framework powered by [Nitro](https://nitro.unjs.io/):
405
458
  | **TanStack Start** | `plugins: ['evlog/nitro']` |
406
459
  | **Standalone Nitro** | `plugins: ['evlog/nitro']` |
407
460
 
461
+ ## Agent Skills
462
+
463
+ evlog provides [Agent Skills](https://github.com/boristane/agent-skills) to help AI coding assistants understand and implement proper logging patterns in your codebase.
464
+
465
+ ### Installation
466
+
467
+ ```bash
468
+ npx add-skill hugorcd/evlog
469
+ ```
470
+
471
+ ### What it does
472
+
473
+ Once installed, your AI assistant will:
474
+ - Review your logging code and suggest wide event patterns
475
+ - Help refactor scattered `console.log` calls into structured events
476
+ - Guide you to use `createError()` for self-documenting errors
477
+ - Ensure proper use of `useLogger(event)` in Nuxt/Nitro routes
478
+
479
+ ### Examples
480
+
481
+ ```
482
+ Add logging to this endpoint
483
+ Review my logging code
484
+ Help me set up logging for this service
485
+ ```
486
+
408
487
  ## Philosophy
409
488
 
410
- Inspired by [Logging Sucks](https://loggingsucks.com/) by [Boris Tane](https://github.com/boristane).
489
+ Inspired by [Logging Sucks](https://loggingsucks.com/) by [Boris Tane](https://x.com/boristane).
411
490
 
412
491
  1. **Wide Events**: One log per request with all context
413
492
  2. **Structured Errors**: Errors that explain themselves
package/dist/error.d.mts CHANGED
@@ -7,6 +7,7 @@ import { ErrorOptions } from './types.mjs';
7
7
  * ```ts
8
8
  * throw new EvlogError({
9
9
  * message: 'Failed to sync repository',
10
+ * status: 503,
10
11
  * why: 'GitHub API rate limit exceeded',
11
12
  * fix: 'Wait 1 hour or use a different token',
12
13
  * link: 'https://docs.github.com/en/rest/rate-limit',
@@ -15,33 +16,42 @@ import { ErrorOptions } from './types.mjs';
15
16
  * ```
16
17
  */
17
18
  declare class EvlogError extends Error {
19
+ readonly status: number;
18
20
  readonly why?: string;
19
21
  readonly fix?: string;
20
22
  readonly link?: string;
21
23
  constructor(options: ErrorOptions | string);
22
- /**
23
- * Format error for console output with colors
24
- */
24
+ get statusCode(): number;
25
+ get data(): {
26
+ why: string | undefined;
27
+ fix: string | undefined;
28
+ link: string | undefined;
29
+ } | undefined;
25
30
  toString(): string;
26
- /**
27
- * Convert to plain object for JSON serialization
28
- */
29
31
  toJSON(): Record<string, unknown>;
30
32
  }
31
33
  /**
32
- * Create an EvlogError (functional alternative to `new EvlogError()`)
34
+ * Create a structured error with context for debugging and user-facing messages.
33
35
  *
34
- * Named `defineError` to avoid conflict with Nuxt's built-in `createError`
36
+ * @param options - Error message string or full options object
37
+ * @returns EvlogError instance compatible with Nitro's error handling
35
38
  *
36
39
  * @example
37
40
  * ```ts
38
- * throw defineError({
41
+ * // Simple error
42
+ * throw createError('Something went wrong')
43
+ *
44
+ * // Structured error with context
45
+ * throw createError({
39
46
  * message: 'Payment failed',
47
+ * status: 402,
40
48
  * why: 'Card declined by issuer',
41
49
  * fix: 'Try a different payment method',
50
+ * link: 'https://docs.example.com/payments',
42
51
  * })
43
52
  * ```
44
53
  */
45
- declare function defineError(options: ErrorOptions | string): EvlogError;
54
+ declare function createError(options: ErrorOptions | string): EvlogError;
55
+ declare const createEvlogError: typeof createError;
46
56
 
47
- export { EvlogError, defineError };
57
+ export { EvlogError, createError, createEvlogError };
package/dist/error.d.ts CHANGED
@@ -7,6 +7,7 @@ import { ErrorOptions } from './types.js';
7
7
  * ```ts
8
8
  * throw new EvlogError({
9
9
  * message: 'Failed to sync repository',
10
+ * status: 503,
10
11
  * why: 'GitHub API rate limit exceeded',
11
12
  * fix: 'Wait 1 hour or use a different token',
12
13
  * link: 'https://docs.github.com/en/rest/rate-limit',
@@ -15,33 +16,42 @@ import { ErrorOptions } from './types.js';
15
16
  * ```
16
17
  */
17
18
  declare class EvlogError extends Error {
19
+ readonly status: number;
18
20
  readonly why?: string;
19
21
  readonly fix?: string;
20
22
  readonly link?: string;
21
23
  constructor(options: ErrorOptions | string);
22
- /**
23
- * Format error for console output with colors
24
- */
24
+ get statusCode(): number;
25
+ get data(): {
26
+ why: string | undefined;
27
+ fix: string | undefined;
28
+ link: string | undefined;
29
+ } | undefined;
25
30
  toString(): string;
26
- /**
27
- * Convert to plain object for JSON serialization
28
- */
29
31
  toJSON(): Record<string, unknown>;
30
32
  }
31
33
  /**
32
- * Create an EvlogError (functional alternative to `new EvlogError()`)
34
+ * Create a structured error with context for debugging and user-facing messages.
33
35
  *
34
- * Named `defineError` to avoid conflict with Nuxt's built-in `createError`
36
+ * @param options - Error message string or full options object
37
+ * @returns EvlogError instance compatible with Nitro's error handling
35
38
  *
36
39
  * @example
37
40
  * ```ts
38
- * throw defineError({
41
+ * // Simple error
42
+ * throw createError('Something went wrong')
43
+ *
44
+ * // Structured error with context
45
+ * throw createError({
39
46
  * message: 'Payment failed',
47
+ * status: 402,
40
48
  * why: 'Card declined by issuer',
41
49
  * fix: 'Try a different payment method',
50
+ * link: 'https://docs.example.com/payments',
42
51
  * })
43
52
  * ```
44
53
  */
45
- declare function defineError(options: ErrorOptions | string): EvlogError;
54
+ declare function createError(options: ErrorOptions | string): EvlogError;
55
+ declare const createEvlogError: typeof createError;
46
56
 
47
- export { EvlogError, defineError };
57
+ export { EvlogError, createError, createEvlogError };
package/dist/error.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { colors, isServer } from './utils.mjs';
2
2
 
3
3
  class EvlogError extends Error {
4
+ status;
4
5
  why;
5
6
  fix;
6
7
  link;
@@ -8,6 +9,7 @@ class EvlogError extends Error {
8
9
  const opts = typeof options === "string" ? { message: options } : options;
9
10
  super(opts.message, { cause: opts.cause });
10
11
  this.name = "EvlogError";
12
+ this.status = opts.status ?? 500;
11
13
  this.why = opts.why;
12
14
  this.fix = opts.fix;
13
15
  this.link = opts.link;
@@ -15,9 +17,12 @@ class EvlogError extends Error {
15
17
  Error.captureStackTrace(this, EvlogError);
16
18
  }
17
19
  }
18
- /**
19
- * Format error for console output with colors
20
- */
20
+ get statusCode() {
21
+ return this.status;
22
+ }
23
+ get data() {
24
+ return this.why || this.fix || this.link ? { why: this.why, fix: this.fix, link: this.link } : void 0;
25
+ }
21
26
  toString() {
22
27
  const useColors = isServer();
23
28
  const red = useColors ? colors.red : "";
@@ -42,13 +47,11 @@ class EvlogError extends Error {
42
47
  }
43
48
  return lines.join("\n");
44
49
  }
45
- /**
46
- * Convert to plain object for JSON serialization
47
- */
48
50
  toJSON() {
49
51
  return {
50
52
  name: this.name,
51
53
  message: this.message,
54
+ status: this.status,
52
55
  why: this.why,
53
56
  fix: this.fix,
54
57
  link: this.link,
@@ -57,8 +60,9 @@ class EvlogError extends Error {
57
60
  };
58
61
  }
59
62
  }
60
- function defineError(options) {
63
+ function createError(options) {
61
64
  return new EvlogError(options);
62
65
  }
66
+ const createEvlogError = createError;
63
67
 
64
- export { EvlogError, defineError };
68
+ export { EvlogError, createError, createEvlogError };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- export { EvlogError, defineError } from './error.mjs';
1
+ export { EvlogError, createError, createEvlogError } from './error.mjs';
2
2
  export { createRequestLogger, getEnvironment, initLogger, log } from './logger.mjs';
3
- export { useLogger } from './runtime/composables/useLogger.mjs';
4
- export { BaseWideEvent, EnvironmentContext, ErrorOptions, EvlogEventContext, Log, LogLevel, LoggerConfig, RequestLogger, WideEvent } from './types.mjs';
5
- import 'h3';
3
+ export { useLogger } from './runtime/server/useLogger.mjs';
4
+ export { parseError } from './runtime/utils/parseError.mjs';
5
+ export { BaseWideEvent, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, ServerEvent, WideEvent } from './types.mjs';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { EvlogError, defineError } from './error.js';
1
+ export { EvlogError, createError, createEvlogError } from './error.js';
2
2
  export { createRequestLogger, getEnvironment, initLogger, log } from './logger.js';
3
- export { useLogger } from './runtime/composables/useLogger.js';
4
- export { BaseWideEvent, EnvironmentContext, ErrorOptions, EvlogEventContext, Log, LogLevel, LoggerConfig, RequestLogger, WideEvent } from './types.js';
5
- import 'h3';
3
+ export { useLogger } from './runtime/server/useLogger.js';
4
+ export { parseError } from './runtime/utils/parseError.js';
5
+ export { BaseWideEvent, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, ServerEvent, WideEvent } from './types.js';
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- export { EvlogError, defineError } from './error.mjs';
1
+ export { EvlogError, createError, createEvlogError } from './error.mjs';
2
2
  export { createRequestLogger, getEnvironment, initLogger, log } from './logger.mjs';
3
- export { useLogger } from './runtime/composables/useLogger.mjs';
3
+ export { useLogger } from './runtime/server/useLogger.mjs';
4
+ export { parseError } from './runtime/utils/parseError.mjs';
4
5
  import './utils.mjs';
package/dist/logger.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { RequestLogger, EnvironmentContext, LoggerConfig, Log } from './types.mjs';
1
+ import { RequestLoggerOptions, RequestLogger, EnvironmentContext, LoggerConfig, Log } from './types.mjs';
2
2
 
3
3
  /**
4
4
  * Initialize the logger with configuration.
@@ -15,11 +15,6 @@ declare function initLogger(config?: LoggerConfig): void;
15
15
  * ```
16
16
  */
17
17
  declare const log: Log;
18
- interface RequestLoggerOptions {
19
- method?: string;
20
- path?: string;
21
- requestId?: string;
22
- }
23
18
  /**
24
19
  * Create a request-scoped logger for building wide events.
25
20
  *
package/dist/logger.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { RequestLogger, EnvironmentContext, LoggerConfig, Log } from './types.js';
1
+ import { RequestLoggerOptions, RequestLogger, EnvironmentContext, LoggerConfig, Log } from './types.js';
2
2
 
3
3
  /**
4
4
  * Initialize the logger with configuration.
@@ -15,11 +15,6 @@ declare function initLogger(config?: LoggerConfig): void;
15
15
  * ```
16
16
  */
17
17
  declare const log: Log;
18
- interface RequestLoggerOptions {
19
- method?: string;
20
- path?: string;
21
- requestId?: string;
22
- }
23
18
  /**
24
19
  * Create a request-scoped logger for building wide events.
25
20
  *
package/dist/logger.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { isDev, detectEnvironment, formatDuration, colors, getLevelColor } from './utils.mjs';
1
+ import { isDev, detectEnvironment, formatDuration, getConsoleMethod, colors, getLevelColor } from './utils.mjs';
2
2
 
3
3
  let globalEnv = {
4
4
  service: "app",
@@ -16,9 +16,6 @@ function initLogger(config = {}) {
16
16
  };
17
17
  globalPretty = config.pretty ?? isDev();
18
18
  }
19
- function getConsoleMethod(level) {
20
- return level === "error" ? "error" : level === "warn" ? "warn" : "log";
21
- }
22
19
  function emitWideEvent(level, event) {
23
20
  const formatted = {
24
21
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -41,6 +38,25 @@ function emitTaggedLog(level, tag, message) {
41
38
  emitWideEvent(level, { tag, message });
42
39
  }
43
40
  }
41
+ function formatValue(value) {
42
+ if (value === null || value === void 0) {
43
+ return String(value);
44
+ }
45
+ if (typeof value === "object") {
46
+ const pairs = [];
47
+ for (const [k, v] of Object.entries(value)) {
48
+ if (v !== void 0 && v !== null) {
49
+ if (typeof v === "object") {
50
+ pairs.push(`${k}=${JSON.stringify(v)}`);
51
+ } else {
52
+ pairs.push(`${k}=${v}`);
53
+ }
54
+ }
55
+ }
56
+ return pairs.join(" ");
57
+ }
58
+ return String(value);
59
+ }
44
60
  function prettyPrintWideEvent(event) {
45
61
  const { timestamp, level, service, environment, version, ...rest } = event;
46
62
  const levelColor = getLevelColor(level);
@@ -52,24 +68,24 @@ function prettyPrintWideEvent(event) {
52
68
  delete rest.method;
53
69
  delete rest.path;
54
70
  }
55
- if (rest.duration) {
56
- header += ` ${colors.dim}${rest.duration}${colors.reset}`;
57
- delete rest.duration;
58
- }
59
71
  if (rest.status) {
60
72
  const statusColor = rest.status >= 400 ? colors.red : colors.green;
61
73
  header += ` ${statusColor}${rest.status}${colors.reset}`;
62
74
  delete rest.status;
63
75
  }
64
- console.log(header);
65
- if (Object.keys(rest).length > 0) {
66
- for (const [key, value] of Object.entries(rest)) {
67
- if (value !== void 0) {
68
- const formatted = typeof value === "object" ? JSON.stringify(value) : value;
69
- console.log(` ${colors.dim}${key}:${colors.reset} ${formatted}`);
70
- }
71
- }
76
+ if (rest.duration) {
77
+ header += ` ${colors.dim}in ${rest.duration}${colors.reset}`;
78
+ delete rest.duration;
72
79
  }
80
+ console.log(header);
81
+ const entries = Object.entries(rest).filter(([_, v]) => v !== void 0);
82
+ const lastIndex = entries.length - 1;
83
+ entries.forEach(([key, value], index) => {
84
+ const isLast = index === lastIndex;
85
+ const prefix = isLast ? "\u2514\u2500" : "\u251C\u2500";
86
+ const formatted = formatValue(value);
87
+ console.log(` ${colors.dim}${prefix}${colors.reset} ${colors.cyan}${key}:${colors.reset} ${formatted}`);
88
+ });
73
89
  }
74
90
  function createLogMethod(level) {
75
91
  return function logMethod(tagOrEvent, message) {
@@ -1,29 +1,60 @@
1
- import { defineNitroPlugin } from 'nitropack/runtime';
1
+ import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime';
2
2
  import { initLogger, createRequestLogger } from '../logger.mjs';
3
3
  import '../utils.mjs';
4
4
 
5
+ function matchesPattern(path, pattern) {
6
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*").replace(/\?/g, "[^/]");
7
+ const regex = new RegExp(`^${regexPattern}$`);
8
+ return regex.test(path);
9
+ }
10
+ function shouldLog(path, include) {
11
+ if (!include || include.length === 0) {
12
+ return true;
13
+ }
14
+ return include.some((pattern) => matchesPattern(path, pattern));
15
+ }
5
16
  function getResponseStatus(event) {
6
- return event.node?.res?.statusCode ?? 200;
17
+ if (event.node?.res?.statusCode) {
18
+ return event.node.res.statusCode;
19
+ }
20
+ if (event.response?.status) {
21
+ return event.response.status;
22
+ }
23
+ if (typeof event.context.status === "number") {
24
+ return event.context.status;
25
+ }
26
+ return 200;
7
27
  }
8
28
  const plugin = defineNitroPlugin((nitroApp) => {
9
- initLogger();
29
+ const config = useRuntimeConfig();
30
+ const evlogConfig = config.evlog;
31
+ initLogger({
32
+ env: evlogConfig?.env,
33
+ pretty: evlogConfig?.pretty
34
+ });
10
35
  nitroApp.hooks.hook("request", (event) => {
36
+ const e = event;
37
+ if (!shouldLog(e.path, evlogConfig?.include)) {
38
+ return;
39
+ }
11
40
  const log = createRequestLogger({
12
- method: event.method,
13
- path: event.path,
14
- requestId: event.context.requestId || crypto.randomUUID()
41
+ method: e.method,
42
+ path: e.path,
43
+ requestId: e.context.requestId || crypto.randomUUID()
15
44
  });
16
- event.context.log = log;
45
+ e.context.log = log;
17
46
  });
18
47
  nitroApp.hooks.hook("afterResponse", (event) => {
19
- const log = event.context.log;
48
+ const e = event;
49
+ const log = e.context.log;
20
50
  if (log) {
21
- log.set({ status: getResponseStatus(event) });
51
+ log.set({ status: getResponseStatus(e) });
22
52
  log.emit();
23
53
  }
24
54
  });
25
55
  nitroApp.hooks.hook("error", (error, { event }) => {
26
- const log = event?.context.log;
56
+ const e = event;
57
+ const log = e?.context.log;
27
58
  if (log) {
28
59
  log.error(error);
29
60
  }
@@ -11,6 +11,13 @@ interface ModuleOptions {
11
11
  * @default true in development, false in production
12
12
  */
13
13
  pretty?: boolean;
14
+ /**
15
+ * Route patterns to include in logging.
16
+ * Supports glob patterns like '/api/**'.
17
+ * If not set, all routes are logged.
18
+ * @example ['/api/**', '/auth/**']
19
+ */
20
+ include?: string[];
14
21
  }
15
22
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
16
23
 
@@ -11,6 +11,13 @@ interface ModuleOptions {
11
11
  * @default true in development, false in production
12
12
  */
13
13
  pretty?: boolean;
14
+ /**
15
+ * Route patterns to include in logging.
16
+ * Supports glob patterns like '/api/**'.
17
+ * If not set, all routes are logged.
18
+ * @example ['/api/**', '/auth/**']
19
+ */
20
+ include?: string[];
14
21
  }
15
22
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
16
23
 
@@ -4,7 +4,7 @@ const module$1 = defineNuxtModule({
4
4
  meta: {
5
5
  name: "evlog",
6
6
  configKey: "evlog",
7
- docs: "https://github.com/hugorcd/evlog"
7
+ docs: "https://evlog.dev"
8
8
  },
9
9
  defaults: {},
10
10
  setup(options, nuxt) {
@@ -15,30 +15,34 @@ const module$1 = defineNuxtModule({
15
15
  };
16
16
  addServerPlugin(resolver.resolve("../nitro/plugin"));
17
17
  addPlugin({
18
- src: resolver.resolve("../runtime/plugin.client"),
18
+ src: resolver.resolve("../runtime/client/plugin"),
19
19
  mode: "client"
20
20
  });
21
21
  addImports([
22
22
  {
23
23
  name: "log",
24
- from: resolver.resolve("../runtime/composables/log")
24
+ from: resolver.resolve("../runtime/client/log")
25
25
  },
26
26
  {
27
- name: "defineError",
27
+ name: "createEvlogError",
28
28
  from: resolver.resolve("../error")
29
+ },
30
+ {
31
+ name: "parseError",
32
+ from: resolver.resolve("../runtime/utils/parseError")
29
33
  }
30
34
  ]);
31
35
  addServerImports([
32
36
  {
33
37
  name: "useLogger",
34
- from: resolver.resolve("../runtime/composables/useLogger")
38
+ from: resolver.resolve("../runtime/server/useLogger")
35
39
  },
36
40
  {
37
41
  name: "log",
38
- from: resolver.resolve("../runtime/composables/log")
42
+ from: resolver.resolve("../runtime/client/log")
39
43
  },
40
44
  {
41
- name: "defineError",
45
+ name: "createEvlogError",
42
46
  from: resolver.resolve("../error")
43
47
  }
44
48
  ]);
@@ -0,0 +1,9 @@
1
+ import { Log } from '../../types.mjs';
2
+
3
+ declare function initLog(options?: {
4
+ pretty?: boolean;
5
+ service?: string;
6
+ }): void;
7
+ declare const log: Log;
8
+
9
+ export { initLog, log };
@@ -0,0 +1,9 @@
1
+ import { Log } from '../../types.js';
2
+
3
+ declare function initLog(options?: {
4
+ pretty?: boolean;
5
+ service?: string;
6
+ }): void;
7
+ declare const log: Log;
8
+
9
+ export { initLog, log };
@@ -1,3 +1,5 @@
1
+ import { getConsoleMethod } from '../../utils.mjs';
2
+
1
3
  const IS_CLIENT = typeof window !== "undefined";
2
4
  let clientPretty = true;
3
5
  let clientService = "client";
@@ -11,9 +13,6 @@ function initLog(options = {}) {
11
13
  clientPretty = options.pretty ?? true;
12
14
  clientService = options.service ?? "client";
13
15
  }
14
- function getConsoleMethod(level) {
15
- return level === "error" ? "error" : level === "warn" ? "warn" : "log";
16
- }
17
16
  function emitClientWideEvent(level, event) {
18
17
  const formatted = {
19
18
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1,7 +1,8 @@
1
- import { initLog } from './composables/log.mjs';
1
+ import { initLog } from './log.mjs';
2
2
  import { defineNuxtPlugin, useRuntimeConfig } from '#app';
3
+ import '../../utils.mjs';
3
4
 
4
- const plugin_client = defineNuxtPlugin((_nuxtApp) => {
5
+ const plugin = defineNuxtPlugin(() => {
5
6
  const config = useRuntimeConfig();
6
7
  const evlogConfig = config.public?.evlog;
7
8
  initLog({
@@ -10,4 +11,4 @@ const plugin_client = defineNuxtPlugin((_nuxtApp) => {
10
11
  });
11
12
  });
12
13
 
13
- export { plugin_client as default };
14
+ export { plugin as default };
@@ -0,0 +1,5 @@
1
+ import { ServerEvent, RequestLogger } from '../../types.mjs';
2
+
3
+ declare function useLogger(event: ServerEvent): RequestLogger;
4
+
5
+ export { useLogger };
@@ -0,0 +1,5 @@
1
+ import { ServerEvent, RequestLogger } from '../../types.js';
2
+
3
+ declare function useLogger(event: ServerEvent): RequestLogger;
4
+
5
+ export { useLogger };
@@ -0,0 +1,5 @@
1
+ import { ParsedError } from '../../types.mjs';
2
+
3
+ declare function parseError(error: unknown): ParsedError;
4
+
5
+ export { ParsedError, parseError };
@@ -0,0 +1,5 @@
1
+ import { ParsedError } from '../../types.js';
2
+
3
+ declare function parseError(error: unknown): ParsedError;
4
+
5
+ export { ParsedError, parseError };
@@ -0,0 +1,28 @@
1
+ function parseError(error) {
2
+ if (error && typeof error === "object" && "data" in error) {
3
+ const { data, message: fetchMessage, statusCode: fetchStatusCode } = error;
4
+ const evlogData = data?.data;
5
+ return {
6
+ message: data?.message || fetchMessage || "An error occurred",
7
+ status: data?.statusCode || fetchStatusCode || 500,
8
+ why: evlogData?.why,
9
+ fix: evlogData?.fix,
10
+ link: evlogData?.link,
11
+ raw: error
12
+ };
13
+ }
14
+ if (error instanceof Error) {
15
+ return {
16
+ message: error.message,
17
+ status: 500,
18
+ raw: error
19
+ };
20
+ }
21
+ return {
22
+ message: String(error),
23
+ status: 500,
24
+ raw: error
25
+ };
26
+ }
27
+
28
+ export { parseError };
package/dist/types.d.mts CHANGED
@@ -116,6 +116,8 @@ interface Log {
116
116
  interface ErrorOptions {
117
117
  /** What actually happened */
118
118
  message: string;
119
+ /** HTTP status code (default: 500) */
120
+ status?: number;
119
121
  /** Why this error occurred */
120
122
  why?: string;
121
123
  /** How to fix this issue */
@@ -125,11 +127,47 @@ interface ErrorOptions {
125
127
  /** The original error that caused this */
126
128
  cause?: Error;
127
129
  }
130
+ /**
131
+ * Options for creating a request logger
132
+ */
133
+ interface RequestLoggerOptions {
134
+ method?: string;
135
+ path?: string;
136
+ requestId?: string;
137
+ }
128
138
  /**
129
139
  * H3 event context with evlog logger attached
130
140
  */
131
- interface EvlogEventContext {
141
+ interface H3EventContext {
132
142
  log?: RequestLogger;
143
+ requestId?: string;
144
+ status?: number;
145
+ [key: string]: unknown;
146
+ }
147
+ /**
148
+ * Server event type for Nitro/h3 handlers
149
+ */
150
+ interface ServerEvent {
151
+ method: string;
152
+ path: string;
153
+ context: H3EventContext;
154
+ node?: {
155
+ res?: {
156
+ statusCode?: number;
157
+ };
158
+ };
159
+ response?: Response;
160
+ }
161
+ /**
162
+ * Parsed evlog error with all fields at the top level
163
+ */
164
+ interface ParsedError {
165
+ message: string;
166
+ status: number;
167
+ why?: string;
168
+ fix?: string;
169
+ link?: string;
170
+ raw: unknown;
133
171
  }
134
172
 
135
- export type { BaseWideEvent, EnvironmentContext, ErrorOptions, EvlogEventContext, Log, LogLevel, LoggerConfig, RequestLogger, WideEvent };
173
+ export type { BaseWideEvent, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, ServerEvent, WideEvent };
package/dist/types.d.ts CHANGED
@@ -116,6 +116,8 @@ interface Log {
116
116
  interface ErrorOptions {
117
117
  /** What actually happened */
118
118
  message: string;
119
+ /** HTTP status code (default: 500) */
120
+ status?: number;
119
121
  /** Why this error occurred */
120
122
  why?: string;
121
123
  /** How to fix this issue */
@@ -125,11 +127,47 @@ interface ErrorOptions {
125
127
  /** The original error that caused this */
126
128
  cause?: Error;
127
129
  }
130
+ /**
131
+ * Options for creating a request logger
132
+ */
133
+ interface RequestLoggerOptions {
134
+ method?: string;
135
+ path?: string;
136
+ requestId?: string;
137
+ }
128
138
  /**
129
139
  * H3 event context with evlog logger attached
130
140
  */
131
- interface EvlogEventContext {
141
+ interface H3EventContext {
132
142
  log?: RequestLogger;
143
+ requestId?: string;
144
+ status?: number;
145
+ [key: string]: unknown;
146
+ }
147
+ /**
148
+ * Server event type for Nitro/h3 handlers
149
+ */
150
+ interface ServerEvent {
151
+ method: string;
152
+ path: string;
153
+ context: H3EventContext;
154
+ node?: {
155
+ res?: {
156
+ statusCode?: number;
157
+ };
158
+ };
159
+ response?: Response;
160
+ }
161
+ /**
162
+ * Parsed evlog error with all fields at the top level
163
+ */
164
+ interface ParsedError {
165
+ message: string;
166
+ status: number;
167
+ why?: string;
168
+ fix?: string;
169
+ link?: string;
170
+ raw: unknown;
133
171
  }
134
172
 
135
- export type { BaseWideEvent, EnvironmentContext, ErrorOptions, EvlogEventContext, Log, LogLevel, LoggerConfig, RequestLogger, WideEvent };
173
+ export type { BaseWideEvent, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, ServerEvent, WideEvent };
package/dist/utils.d.mts CHANGED
@@ -1,27 +1,11 @@
1
- import { EnvironmentContext } from './types.mjs';
1
+ import { EnvironmentContext, LogLevel } from './types.mjs';
2
2
 
3
- /**
4
- * Format duration for display
5
- * < 1s: shows milliseconds (e.g., "42ms")
6
- * >= 1s: shows seconds (e.g., "1.5s")
7
- */
8
3
  declare function formatDuration(ms: number): string;
9
- /**
10
- * Check if running on server
11
- */
12
4
  declare function isServer(): boolean;
13
- /**
14
- * Check if running on client
15
- */
16
5
  declare function isClient(): boolean;
17
- /**
18
- * Check if in development mode
19
- */
20
6
  declare function isDev(): boolean;
21
- /**
22
- * Auto-detect environment context from env variables
23
- */
24
7
  declare function detectEnvironment(): Partial<EnvironmentContext>;
8
+ declare function getConsoleMethod(level: LogLevel): 'log' | 'error' | 'warn';
25
9
  declare const colors: {
26
10
  readonly reset: "\u001B[0m";
27
11
  readonly bold: "\u001B[1m";
@@ -35,9 +19,6 @@ declare const colors: {
35
19
  readonly white: "\u001B[37m";
36
20
  readonly gray: "\u001B[90m";
37
21
  };
38
- /**
39
- * Get color for log level
40
- */
41
22
  declare function getLevelColor(level: string): string;
42
23
 
43
- export { colors, detectEnvironment, formatDuration, getLevelColor, isClient, isDev, isServer };
24
+ export { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isClient, isDev, isServer };
package/dist/utils.d.ts CHANGED
@@ -1,27 +1,11 @@
1
- import { EnvironmentContext } from './types.js';
1
+ import { EnvironmentContext, LogLevel } from './types.js';
2
2
 
3
- /**
4
- * Format duration for display
5
- * < 1s: shows milliseconds (e.g., "42ms")
6
- * >= 1s: shows seconds (e.g., "1.5s")
7
- */
8
3
  declare function formatDuration(ms: number): string;
9
- /**
10
- * Check if running on server
11
- */
12
4
  declare function isServer(): boolean;
13
- /**
14
- * Check if running on client
15
- */
16
5
  declare function isClient(): boolean;
17
- /**
18
- * Check if in development mode
19
- */
20
6
  declare function isDev(): boolean;
21
- /**
22
- * Auto-detect environment context from env variables
23
- */
24
7
  declare function detectEnvironment(): Partial<EnvironmentContext>;
8
+ declare function getConsoleMethod(level: LogLevel): 'log' | 'error' | 'warn';
25
9
  declare const colors: {
26
10
  readonly reset: "\u001B[0m";
27
11
  readonly bold: "\u001B[1m";
@@ -35,9 +19,6 @@ declare const colors: {
35
19
  readonly white: "\u001B[37m";
36
20
  readonly gray: "\u001B[90m";
37
21
  };
38
- /**
39
- * Get color for log level
40
- */
41
22
  declare function getLevelColor(level: string): string;
42
23
 
43
- export { colors, detectEnvironment, formatDuration, getLevelColor, isClient, isDev, isServer };
24
+ export { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isClient, isDev, isServer };
package/dist/utils.mjs CHANGED
@@ -26,6 +26,9 @@ function detectEnvironment() {
26
26
  region: env.VERCEL_REGION || env.AWS_REGION || env.FLY_REGION || env.CF_REGION
27
27
  };
28
28
  }
29
+ function getConsoleMethod(level) {
30
+ return level === "error" ? "error" : level === "warn" ? "warn" : "log";
31
+ }
29
32
  const colors = {
30
33
  reset: "\x1B[0m",
31
34
  bold: "\x1B[1m",
@@ -54,4 +57,4 @@ function getLevelColor(level) {
54
57
  }
55
58
  }
56
59
 
57
- export { colors, detectEnvironment, formatDuration, getLevelColor, isClient, isDev, isServer };
60
+ export { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isClient, isDev, isServer };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "evlog",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "description": "Wide event logging library with structured error handling. Inspired by LoggingSucks.",
5
5
  "author": "HugoRCD <contact@hrcd.fr>",
6
- "homepage": "https://github.com/HugoRCD/evlog",
6
+ "homepage": "https://evlog.dev",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/HugoRCD/evlog.git"
@@ -57,7 +57,7 @@
57
57
  ],
58
58
  "scripts": {
59
59
  "build": "unbuild",
60
- "dev:prepare": "unbuild --stub",
60
+ "dev:prepare": "unbuild",
61
61
  "release": "bun run lint && bun run test && bun run build && changelogen --release && npm publish && git push --follow-tags",
62
62
  "lint": "eslint .",
63
63
  "lint:fix": "eslint . --fix",
@@ -74,15 +74,16 @@
74
74
  "@nuxt/schema": "^4.3.0",
75
75
  "@nuxt/test-utils": "^3.23.0",
76
76
  "changelogen": "^0.6.2",
77
- "h3": "^1.15.0",
78
- "nitropack": "^2.13.0",
77
+ "h3": "^1.15.5",
78
+ "nitropack": "^2.13.1",
79
79
  "nuxt": "^4.3.0",
80
80
  "typescript": "^5.9.3",
81
- "unbuild": "^3.5.0"
81
+ "unbuild": "^3.6.1"
82
82
  },
83
83
  "peerDependencies": {
84
- "h3": "^1.13.0",
85
- "nitropack": "^2.10.0"
84
+ "h3": "^1.15.5",
85
+ "nitropack": "^2.13.1",
86
+ "ofetch": "^1.5.1"
86
87
  },
87
88
  "peerDependenciesMeta": {
88
89
  "h3": {
@@ -90,6 +91,9 @@
90
91
  },
91
92
  "nitropack": {
92
93
  "optional": true
94
+ },
95
+ "ofetch": {
96
+ "optional": true
93
97
  }
94
98
  }
95
99
  }
@@ -1,4 +0,0 @@
1
- export { initLog, log } from './log.mjs';
2
- export { useLogger } from './useLogger.mjs';
3
- import '../../types.mjs';
4
- import 'h3';
@@ -1,4 +0,0 @@
1
- export { initLog, log } from './log.js';
2
- export { useLogger } from './useLogger.js';
3
- import '../../types.js';
4
- import 'h3';
@@ -1,2 +0,0 @@
1
- export { initLog, log } from './log.mjs';
2
- export { useLogger } from './useLogger.mjs';
@@ -1,18 +0,0 @@
1
- import { Log } from '../../types.mjs';
2
-
3
- declare function initLog(options?: {
4
- pretty?: boolean;
5
- service?: string;
6
- }): void;
7
- /**
8
- * Universal logging API - works on both client and server.
9
- *
10
- * @example
11
- * ```ts
12
- * log.info('auth', 'User logged in')
13
- * log.info({ action: 'checkout', items: 3 })
14
- * ```
15
- */
16
- declare const log: Log;
17
-
18
- export { initLog, log };
@@ -1,18 +0,0 @@
1
- import { Log } from '../../types.js';
2
-
3
- declare function initLog(options?: {
4
- pretty?: boolean;
5
- service?: string;
6
- }): void;
7
- /**
8
- * Universal logging API - works on both client and server.
9
- *
10
- * @example
11
- * ```ts
12
- * log.info('auth', 'User logged in')
13
- * log.info({ action: 'checkout', items: 3 })
14
- * ```
15
- */
16
- declare const log: Log;
17
-
18
- export { initLog, log };
@@ -1,21 +0,0 @@
1
- import { H3Event } from 'h3';
2
- import { RequestLogger } from '../../types.mjs';
3
-
4
- /**
5
- * Get the request-scoped logger from the event context.
6
- * Must be called within a server handler with an H3 event.
7
- *
8
- * @example
9
- * ```ts
10
- * export default defineEventHandler((event) => {
11
- * const logger = useLogger(event)
12
- * logger.set({ userId: '123' })
13
- * logger.set({ action: 'checkout' })
14
- * return { ok: true }
15
- * // emit() is called automatically by the Nitro plugin
16
- * })
17
- * ```
18
- */
19
- declare function useLogger(event: H3Event): RequestLogger;
20
-
21
- export { useLogger };
@@ -1,21 +0,0 @@
1
- import { H3Event } from 'h3';
2
- import { RequestLogger } from '../../types.js';
3
-
4
- /**
5
- * Get the request-scoped logger from the event context.
6
- * Must be called within a server handler with an H3 event.
7
- *
8
- * @example
9
- * ```ts
10
- * export default defineEventHandler((event) => {
11
- * const logger = useLogger(event)
12
- * logger.set({ userId: '123' })
13
- * logger.set({ action: 'checkout' })
14
- * return { ok: true }
15
- * // emit() is called automatically by the Nitro plugin
16
- * })
17
- * ```
18
- */
19
- declare function useLogger(event: H3Event): RequestLogger;
20
-
21
- export { useLogger };