evlog 1.0.1 → 1.2.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 +82 -1
- package/dist/error.d.mts +12 -6
- package/dist/error.d.ts +12 -6
- package/dist/error.mjs +21 -8
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +3 -2
- package/dist/logger.d.mts +7 -2
- package/dist/logger.d.ts +7 -2
- package/dist/logger.mjs +59 -10
- package/dist/nitro/errorHandler.d.mts +13 -0
- package/dist/nitro/errorHandler.d.ts +13 -0
- package/dist/nitro/errorHandler.mjs +41 -0
- package/dist/nitro/plugin.mjs +79 -15
- package/dist/nuxt/module.d.mts +37 -1
- package/dist/nuxt/module.d.ts +37 -1
- package/dist/nuxt/module.mjs +19 -3
- package/dist/runtime/client/log.d.mts +2 -1
- package/dist/runtime/client/log.d.ts +2 -1
- package/dist/runtime/client/log.mjs +37 -21
- package/dist/runtime/client/plugin.mjs +2 -1
- package/dist/runtime/server/routes/_evlog/ingest.post.d.mts +5 -0
- package/dist/runtime/server/routes/_evlog/ingest.post.d.ts +5 -0
- package/dist/runtime/server/routes/_evlog/ingest.post.mjs +87 -0
- package/dist/runtime/server/useLogger.d.mts +14 -0
- package/dist/runtime/server/useLogger.d.ts +14 -0
- package/dist/runtime/utils/parseError.mjs +6 -4
- package/dist/types.d.mts +173 -4
- package/dist/types.d.ts +173 -4
- package/dist/utils.d.mts +6 -1
- package/dist/utils.d.ts +6 -1
- package/dist/utils.mjs +6 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -94,7 +94,6 @@ export default defineNuxtConfig({
|
|
|
94
94
|
evlog: {
|
|
95
95
|
env: {
|
|
96
96
|
service: 'my-app',
|
|
97
|
-
environment: process.env.NODE_ENV,
|
|
98
97
|
},
|
|
99
98
|
// Optional: only log specific routes (supports glob patterns)
|
|
100
99
|
include: ['/api/**'],
|
|
@@ -102,6 +101,17 @@ export default defineNuxtConfig({
|
|
|
102
101
|
})
|
|
103
102
|
```
|
|
104
103
|
|
|
104
|
+
> **Tip:** Use `$production` to enable [sampling](#sampling) only in production:
|
|
105
|
+
> ```typescript
|
|
106
|
+
> export default defineNuxtConfig({
|
|
107
|
+
> modules: ['evlog/nuxt'],
|
|
108
|
+
> evlog: { env: { service: 'my-app' } },
|
|
109
|
+
> $production: {
|
|
110
|
+
> evlog: { sampling: { rates: { info: 10, warn: 50, debug: 0 } } },
|
|
111
|
+
> },
|
|
112
|
+
> })
|
|
113
|
+
> ```
|
|
114
|
+
|
|
105
115
|
That's it. Now use `useLogger(event)` in any API route:
|
|
106
116
|
|
|
107
117
|
```typescript
|
|
@@ -353,6 +363,77 @@ initLogger({
|
|
|
353
363
|
},
|
|
354
364
|
pretty?: boolean // Pretty print (default: true in dev)
|
|
355
365
|
include?: string[] // Route patterns to log (glob), e.g. ['/api/**']
|
|
366
|
+
sampling?: {
|
|
367
|
+
rates?: { // Head sampling (random per level)
|
|
368
|
+
info?: number // 0-100, default 100
|
|
369
|
+
warn?: number // 0-100, default 100
|
|
370
|
+
debug?: number // 0-100, default 100
|
|
371
|
+
error?: number // 0-100, default 100 (always logged unless set to 0)
|
|
372
|
+
}
|
|
373
|
+
keep?: Array<{ // Tail sampling (force keep based on outcome)
|
|
374
|
+
status?: number // Keep if status >= value
|
|
375
|
+
duration?: number // Keep if duration >= value (ms)
|
|
376
|
+
path?: string // Keep if path matches glob pattern
|
|
377
|
+
}>
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Sampling
|
|
383
|
+
|
|
384
|
+
At scale, logging everything can become expensive. evlog supports two sampling strategies:
|
|
385
|
+
|
|
386
|
+
#### Head Sampling (rates)
|
|
387
|
+
|
|
388
|
+
Random sampling based on log level, decided before the request completes:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
initLogger({
|
|
392
|
+
sampling: {
|
|
393
|
+
rates: {
|
|
394
|
+
info: 10, // Keep 10% of info logs
|
|
395
|
+
warn: 50, // Keep 50% of warning logs
|
|
396
|
+
debug: 0, // Disable debug logs
|
|
397
|
+
// error defaults to 100% (always logged)
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### Tail Sampling (keep)
|
|
404
|
+
|
|
405
|
+
Force-keep logs based on request outcome, evaluated after the request completes. Useful to always capture slow requests or critical paths:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// nuxt.config.ts
|
|
409
|
+
export default defineNuxtConfig({
|
|
410
|
+
modules: ['evlog/nuxt'],
|
|
411
|
+
evlog: {
|
|
412
|
+
sampling: {
|
|
413
|
+
rates: { info: 10 }, // Only 10% of info logs
|
|
414
|
+
keep: [
|
|
415
|
+
{ duration: 1000 }, // Always keep if duration >= 1000ms
|
|
416
|
+
{ status: 400 }, // Always keep if status >= 400
|
|
417
|
+
{ path: '/api/critical/**' }, // Always keep critical paths
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
})
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
#### Custom Tail Sampling Hook
|
|
425
|
+
|
|
426
|
+
For business-specific conditions (premium users, feature flags), use the `evlog:emit:keep` Nitro hook:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// server/plugins/evlog-custom.ts
|
|
430
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
431
|
+
nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
|
|
432
|
+
// Always keep logs for premium users
|
|
433
|
+
if (ctx.context.user?.premium) {
|
|
434
|
+
ctx.shouldKeep = true
|
|
435
|
+
}
|
|
436
|
+
})
|
|
356
437
|
})
|
|
357
438
|
```
|
|
358
439
|
|
package/dist/error.d.mts
CHANGED
|
@@ -16,16 +16,23 @@ import { ErrorOptions } from './types.mjs';
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
declare class EvlogError extends Error {
|
|
19
|
+
/** HTTP status code */
|
|
19
20
|
readonly status: number;
|
|
20
21
|
readonly why?: string;
|
|
21
22
|
readonly fix?: string;
|
|
22
23
|
readonly link?: string;
|
|
23
24
|
constructor(options: ErrorOptions | string);
|
|
25
|
+
/** HTTP status text (alias for message) */
|
|
26
|
+
get statusText(): string;
|
|
27
|
+
/** HTTP status code (alias for compatibility) */
|
|
24
28
|
get statusCode(): number;
|
|
29
|
+
/** HTTP status message (alias for compatibility) */
|
|
30
|
+
get statusMessage(): string;
|
|
31
|
+
/** Structured data for serialization */
|
|
25
32
|
get data(): {
|
|
26
|
-
why
|
|
27
|
-
fix
|
|
28
|
-
link
|
|
33
|
+
why?: string;
|
|
34
|
+
fix?: string;
|
|
35
|
+
link?: string;
|
|
29
36
|
} | undefined;
|
|
30
37
|
toString(): string;
|
|
31
38
|
toJSON(): Record<string, unknown>;
|
|
@@ -34,7 +41,7 @@ declare class EvlogError extends Error {
|
|
|
34
41
|
* Create a structured error with context for debugging and user-facing messages.
|
|
35
42
|
*
|
|
36
43
|
* @param options - Error message string or full options object
|
|
37
|
-
* @returns EvlogError
|
|
44
|
+
* @returns EvlogError with HTTP metadata (`status`, `statusText`) and `data`; also includes `statusCode` and `statusMessage` for legacy compatibility
|
|
38
45
|
*
|
|
39
46
|
* @example
|
|
40
47
|
* ```ts
|
|
@@ -52,6 +59,5 @@ declare class EvlogError extends Error {
|
|
|
52
59
|
* ```
|
|
53
60
|
*/
|
|
54
61
|
declare function createError(options: ErrorOptions | string): EvlogError;
|
|
55
|
-
declare const createEvlogError: typeof createError;
|
|
56
62
|
|
|
57
|
-
export { EvlogError, createError, createEvlogError };
|
|
63
|
+
export { EvlogError, createError, createError as createEvlogError };
|
package/dist/error.d.ts
CHANGED
|
@@ -16,16 +16,23 @@ import { ErrorOptions } from './types.js';
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
declare class EvlogError extends Error {
|
|
19
|
+
/** HTTP status code */
|
|
19
20
|
readonly status: number;
|
|
20
21
|
readonly why?: string;
|
|
21
22
|
readonly fix?: string;
|
|
22
23
|
readonly link?: string;
|
|
23
24
|
constructor(options: ErrorOptions | string);
|
|
25
|
+
/** HTTP status text (alias for message) */
|
|
26
|
+
get statusText(): string;
|
|
27
|
+
/** HTTP status code (alias for compatibility) */
|
|
24
28
|
get statusCode(): number;
|
|
29
|
+
/** HTTP status message (alias for compatibility) */
|
|
30
|
+
get statusMessage(): string;
|
|
31
|
+
/** Structured data for serialization */
|
|
25
32
|
get data(): {
|
|
26
|
-
why
|
|
27
|
-
fix
|
|
28
|
-
link
|
|
33
|
+
why?: string;
|
|
34
|
+
fix?: string;
|
|
35
|
+
link?: string;
|
|
29
36
|
} | undefined;
|
|
30
37
|
toString(): string;
|
|
31
38
|
toJSON(): Record<string, unknown>;
|
|
@@ -34,7 +41,7 @@ declare class EvlogError extends Error {
|
|
|
34
41
|
* Create a structured error with context for debugging and user-facing messages.
|
|
35
42
|
*
|
|
36
43
|
* @param options - Error message string or full options object
|
|
37
|
-
* @returns EvlogError
|
|
44
|
+
* @returns EvlogError with HTTP metadata (`status`, `statusText`) and `data`; also includes `statusCode` and `statusMessage` for legacy compatibility
|
|
38
45
|
*
|
|
39
46
|
* @example
|
|
40
47
|
* ```ts
|
|
@@ -52,6 +59,5 @@ declare class EvlogError extends Error {
|
|
|
52
59
|
* ```
|
|
53
60
|
*/
|
|
54
61
|
declare function createError(options: ErrorOptions | string): EvlogError;
|
|
55
|
-
declare const createEvlogError: typeof createError;
|
|
56
62
|
|
|
57
|
-
export { EvlogError, createError, createEvlogError };
|
|
63
|
+
export { EvlogError, createError, createError as 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
|
+
/** HTTP status code */
|
|
4
5
|
status;
|
|
5
6
|
why;
|
|
6
7
|
fix;
|
|
@@ -17,11 +18,24 @@ class EvlogError extends Error {
|
|
|
17
18
|
Error.captureStackTrace(this, EvlogError);
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
/** HTTP status text (alias for message) */
|
|
22
|
+
get statusText() {
|
|
23
|
+
return this.message;
|
|
24
|
+
}
|
|
25
|
+
/** HTTP status code (alias for compatibility) */
|
|
20
26
|
get statusCode() {
|
|
21
27
|
return this.status;
|
|
22
28
|
}
|
|
29
|
+
/** HTTP status message (alias for compatibility) */
|
|
30
|
+
get statusMessage() {
|
|
31
|
+
return this.message;
|
|
32
|
+
}
|
|
33
|
+
/** Structured data for serialization */
|
|
23
34
|
get data() {
|
|
24
|
-
|
|
35
|
+
if (this.why || this.fix || this.link) {
|
|
36
|
+
return { why: this.why, fix: this.fix, link: this.link };
|
|
37
|
+
}
|
|
38
|
+
return void 0;
|
|
25
39
|
}
|
|
26
40
|
toString() {
|
|
27
41
|
const useColors = isServer();
|
|
@@ -48,21 +62,20 @@ class EvlogError extends Error {
|
|
|
48
62
|
return lines.join("\n");
|
|
49
63
|
}
|
|
50
64
|
toJSON() {
|
|
65
|
+
const { data } = this;
|
|
51
66
|
return {
|
|
52
67
|
name: this.name,
|
|
53
68
|
message: this.message,
|
|
54
69
|
status: this.status,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
stack: this.stack
|
|
70
|
+
...data && { data },
|
|
71
|
+
...this.cause instanceof Error && {
|
|
72
|
+
cause: { name: this.cause.name, message: this.cause.message }
|
|
73
|
+
}
|
|
60
74
|
};
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
77
|
function createError(options) {
|
|
64
78
|
return new EvlogError(options);
|
|
65
79
|
}
|
|
66
|
-
const createEvlogError = createError;
|
|
67
80
|
|
|
68
|
-
export { EvlogError, createError, createEvlogError };
|
|
81
|
+
export { EvlogError, createError, createError as createEvlogError };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { EvlogError, createError, createEvlogError } from './error.mjs';
|
|
2
|
-
export { createRequestLogger, getEnvironment, initLogger, log } from './logger.mjs';
|
|
1
|
+
export { EvlogError, createError, createError as createEvlogError } from './error.mjs';
|
|
2
|
+
export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from './logger.mjs';
|
|
3
3
|
export { useLogger } from './runtime/server/useLogger.mjs';
|
|
4
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';
|
|
5
|
+
export { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent } from './types.mjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { EvlogError, createError, createEvlogError } from './error.js';
|
|
2
|
-
export { createRequestLogger, getEnvironment, initLogger, log } from './logger.js';
|
|
1
|
+
export { EvlogError, createError, createError as createEvlogError } from './error.js';
|
|
2
|
+
export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from './logger.js';
|
|
3
3
|
export { useLogger } from './runtime/server/useLogger.js';
|
|
4
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';
|
|
5
|
+
export { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent } from './types.js';
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { EvlogError, createError, createEvlogError } from './error.mjs';
|
|
2
|
-
export { createRequestLogger, getEnvironment, initLogger, log } from './logger.mjs';
|
|
1
|
+
export { EvlogError, createError, createError as createEvlogError } from './error.mjs';
|
|
2
|
+
export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from './logger.mjs';
|
|
3
3
|
export { useLogger } from './runtime/server/useLogger.mjs';
|
|
4
4
|
export { parseError } from './runtime/utils/parseError.mjs';
|
|
5
5
|
import './utils.mjs';
|
|
6
|
+
import 'defu';
|
package/dist/logger.d.mts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { RequestLoggerOptions, RequestLogger, EnvironmentContext, LoggerConfig, Log } from './types.mjs';
|
|
1
|
+
import { RequestLoggerOptions, RequestLogger, EnvironmentContext, LoggerConfig, Log, TailSamplingContext } from './types.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Initialize the logger with configuration.
|
|
5
5
|
* Call this once at application startup.
|
|
6
6
|
*/
|
|
7
7
|
declare function initLogger(config?: LoggerConfig): void;
|
|
8
|
+
/**
|
|
9
|
+
* Evaluate tail sampling conditions to determine if a log should be force-kept.
|
|
10
|
+
* Returns true if ANY condition matches (OR logic).
|
|
11
|
+
*/
|
|
12
|
+
declare function shouldKeep(ctx: TailSamplingContext): boolean;
|
|
8
13
|
/**
|
|
9
14
|
* Simple logging API - as easy as console.log
|
|
10
15
|
*
|
|
@@ -32,4 +37,4 @@ declare function createRequestLogger(options?: RequestLoggerOptions): RequestLog
|
|
|
32
37
|
*/
|
|
33
38
|
declare function getEnvironment(): EnvironmentContext;
|
|
34
39
|
|
|
35
|
-
export { createRequestLogger, getEnvironment, initLogger, log };
|
|
40
|
+
export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep };
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { RequestLoggerOptions, RequestLogger, EnvironmentContext, LoggerConfig, Log } from './types.js';
|
|
1
|
+
import { RequestLoggerOptions, RequestLogger, EnvironmentContext, LoggerConfig, Log, TailSamplingContext } from './types.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Initialize the logger with configuration.
|
|
5
5
|
* Call this once at application startup.
|
|
6
6
|
*/
|
|
7
7
|
declare function initLogger(config?: LoggerConfig): void;
|
|
8
|
+
/**
|
|
9
|
+
* Evaluate tail sampling conditions to determine if a log should be force-kept.
|
|
10
|
+
* Returns true if ANY condition matches (OR logic).
|
|
11
|
+
*/
|
|
12
|
+
declare function shouldKeep(ctx: TailSamplingContext): boolean;
|
|
8
13
|
/**
|
|
9
14
|
* Simple logging API - as easy as console.log
|
|
10
15
|
*
|
|
@@ -32,4 +37,4 @@ declare function createRequestLogger(options?: RequestLoggerOptions): RequestLog
|
|
|
32
37
|
*/
|
|
33
38
|
declare function getEnvironment(): EnvironmentContext;
|
|
34
39
|
|
|
35
|
-
export { createRequestLogger, getEnvironment, initLogger, log };
|
|
40
|
+
export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep };
|
package/dist/logger.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defu } from 'defu';
|
|
2
|
+
import { isDev, detectEnvironment, formatDuration, matchesPattern, getConsoleMethod, colors, getLevelColor } from './utils.mjs';
|
|
2
3
|
|
|
3
4
|
let globalEnv = {
|
|
4
5
|
service: "app",
|
|
5
6
|
environment: "development"
|
|
6
7
|
};
|
|
7
8
|
let globalPretty = isDev();
|
|
9
|
+
let globalSampling = {};
|
|
8
10
|
function initLogger(config = {}) {
|
|
9
11
|
const detected = detectEnvironment();
|
|
10
12
|
globalEnv = {
|
|
@@ -15,8 +17,38 @@ function initLogger(config = {}) {
|
|
|
15
17
|
region: config.env?.region ?? detected.region
|
|
16
18
|
};
|
|
17
19
|
globalPretty = config.pretty ?? isDev();
|
|
20
|
+
globalSampling = config.sampling ?? {};
|
|
18
21
|
}
|
|
19
|
-
function
|
|
22
|
+
function shouldSample(level) {
|
|
23
|
+
const { rates } = globalSampling;
|
|
24
|
+
if (!rates) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const percentage = level === "error" && rates.error === void 0 ? 100 : rates[level] ?? 100;
|
|
28
|
+
if (percentage <= 0) return false;
|
|
29
|
+
if (percentage >= 100) return true;
|
|
30
|
+
return Math.random() * 100 < percentage;
|
|
31
|
+
}
|
|
32
|
+
function shouldKeep(ctx) {
|
|
33
|
+
const { keep } = globalSampling;
|
|
34
|
+
if (!keep?.length) return false;
|
|
35
|
+
return keep.some((condition) => {
|
|
36
|
+
if (condition.status !== void 0 && ctx.status !== void 0 && ctx.status >= condition.status) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (condition.duration !== void 0 && ctx.duration !== void 0 && ctx.duration >= condition.duration) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (condition.path && ctx.path && matchesPattern(ctx.path, condition.path)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function emitWideEvent(level, event, skipSamplingCheck = false) {
|
|
49
|
+
if (!skipSamplingCheck && !shouldSample(level)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
20
52
|
const formatted = {
|
|
21
53
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22
54
|
level,
|
|
@@ -28,9 +60,13 @@ function emitWideEvent(level, event) {
|
|
|
28
60
|
} else {
|
|
29
61
|
console[getConsoleMethod(level)](JSON.stringify(formatted));
|
|
30
62
|
}
|
|
63
|
+
return formatted;
|
|
31
64
|
}
|
|
32
65
|
function emitTaggedLog(level, tag, message) {
|
|
33
66
|
if (globalPretty) {
|
|
67
|
+
if (!shouldSample(level)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
34
70
|
const color = getLevelColor(level);
|
|
35
71
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
36
72
|
console.log(`${colors.dim}${timestamp}${colors.reset} ${color}[${tag}]${colors.reset} ${message}`);
|
|
@@ -114,13 +150,12 @@ function createRequestLogger(options = {}) {
|
|
|
114
150
|
let hasError = false;
|
|
115
151
|
return {
|
|
116
152
|
set(data) {
|
|
117
|
-
context =
|
|
153
|
+
context = defu(data, context);
|
|
118
154
|
},
|
|
119
155
|
error(error, errorContext) {
|
|
120
156
|
hasError = true;
|
|
121
157
|
const err = typeof error === "string" ? new Error(error) : error;
|
|
122
|
-
|
|
123
|
-
...context,
|
|
158
|
+
const errorData = {
|
|
124
159
|
...errorContext,
|
|
125
160
|
error: {
|
|
126
161
|
name: err.name,
|
|
@@ -128,15 +163,29 @@ function createRequestLogger(options = {}) {
|
|
|
128
163
|
stack: err.stack
|
|
129
164
|
}
|
|
130
165
|
};
|
|
166
|
+
context = defu(errorData, context);
|
|
131
167
|
},
|
|
132
168
|
emit(overrides) {
|
|
133
|
-
const
|
|
169
|
+
const durationMs = Date.now() - startTime;
|
|
170
|
+
const duration = formatDuration(durationMs);
|
|
134
171
|
const level = hasError ? "error" : "info";
|
|
135
|
-
|
|
172
|
+
const { _forceKeep, ...restOverrides } = overrides ?? {};
|
|
173
|
+
const tailCtx = {
|
|
174
|
+
status: context.status ?? restOverrides.status,
|
|
175
|
+
duration: durationMs,
|
|
176
|
+
path: context.path,
|
|
177
|
+
method: context.method,
|
|
178
|
+
context: { ...context, ...restOverrides }
|
|
179
|
+
};
|
|
180
|
+
const forceKeep = _forceKeep || shouldKeep(tailCtx);
|
|
181
|
+
if (!forceKeep && !shouldSample(level)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return emitWideEvent(level, {
|
|
136
185
|
...context,
|
|
137
|
-
...
|
|
186
|
+
...restOverrides,
|
|
138
187
|
duration
|
|
139
|
-
});
|
|
188
|
+
}, true);
|
|
140
189
|
},
|
|
141
190
|
getContext() {
|
|
142
191
|
return { ...context };
|
|
@@ -147,4 +196,4 @@ function getEnvironment() {
|
|
|
147
196
|
return { ...globalEnv };
|
|
148
197
|
}
|
|
149
198
|
|
|
150
|
-
export { createRequestLogger, getEnvironment, initLogger, log };
|
|
199
|
+
export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as nitropack from 'nitropack';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom Nitro error handler that properly serializes EvlogError.
|
|
5
|
+
* This ensures that 'data' (containing 'why', 'fix', 'link') is preserved
|
|
6
|
+
* in the JSON response regardless of the underlying HTTP framework.
|
|
7
|
+
*
|
|
8
|
+
* For non-EvlogError, it preserves Nitro's default response shape while
|
|
9
|
+
* sanitizing internal error details in production for 5xx errors.
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: nitropack.NitroErrorHandler;
|
|
12
|
+
|
|
13
|
+
export { _default as default };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as nitropack from 'nitropack';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom Nitro error handler that properly serializes EvlogError.
|
|
5
|
+
* This ensures that 'data' (containing 'why', 'fix', 'link') is preserved
|
|
6
|
+
* in the JSON response regardless of the underlying HTTP framework.
|
|
7
|
+
*
|
|
8
|
+
* For non-EvlogError, it preserves Nitro's default response shape while
|
|
9
|
+
* sanitizing internal error details in production for 5xx errors.
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: nitropack.NitroErrorHandler;
|
|
12
|
+
|
|
13
|
+
export { _default as default };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineNitroErrorHandler } from 'nitropack/runtime';
|
|
2
|
+
import { getRequestURL, setResponseStatus, setResponseHeader, send } from 'h3';
|
|
3
|
+
|
|
4
|
+
const errorHandler = defineNitroErrorHandler((error, event) => {
|
|
5
|
+
const evlogError = error.name === "EvlogError" ? error : error.cause?.name === "EvlogError" ? error.cause : null;
|
|
6
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
7
|
+
const url = getRequestURL(event, { xForwardedHost: true }).pathname;
|
|
8
|
+
if (!evlogError) {
|
|
9
|
+
const status2 = error.statusCode ?? error.status ?? 500;
|
|
10
|
+
const rawMessage = (error.statusText ?? error.statusMessage ?? error.message) || "Internal Server Error";
|
|
11
|
+
const message = isDev ? rawMessage : status2 >= 500 ? "Internal Server Error" : rawMessage;
|
|
12
|
+
setResponseStatus(event, status2);
|
|
13
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
14
|
+
return send(event, JSON.stringify({
|
|
15
|
+
url,
|
|
16
|
+
status: status2,
|
|
17
|
+
statusCode: status2,
|
|
18
|
+
statusText: message,
|
|
19
|
+
statusMessage: message,
|
|
20
|
+
message,
|
|
21
|
+
error: true
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
const status = evlogError.status ?? evlogError.statusCode ?? 500;
|
|
25
|
+
setResponseStatus(event, status);
|
|
26
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
27
|
+
const { data } = evlogError;
|
|
28
|
+
const statusMessage = evlogError.statusMessage || evlogError.message;
|
|
29
|
+
return send(event, JSON.stringify({
|
|
30
|
+
url,
|
|
31
|
+
status,
|
|
32
|
+
statusCode: status,
|
|
33
|
+
statusText: statusMessage,
|
|
34
|
+
statusMessage,
|
|
35
|
+
message: evlogError.message,
|
|
36
|
+
error: true,
|
|
37
|
+
...data !== void 0 && { data }
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export { errorHandler as default };
|
package/dist/nitro/plugin.mjs
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime';
|
|
2
|
+
import { getHeaders } from 'h3';
|
|
2
3
|
import { initLogger, createRequestLogger } from '../logger.mjs';
|
|
3
|
-
import '../utils.mjs';
|
|
4
|
+
import { matchesPattern } from '../utils.mjs';
|
|
5
|
+
import 'defu';
|
|
4
6
|
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
7
|
+
function shouldLog(path, include, exclude) {
|
|
8
|
+
if (exclude && exclude.length > 0) {
|
|
9
|
+
if (exclude.some((pattern) => matchesPattern(path, pattern))) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
11
13
|
if (!include || include.length === 0) {
|
|
12
14
|
return true;
|
|
13
15
|
}
|
|
14
16
|
return include.some((pattern) => matchesPattern(path, pattern));
|
|
15
17
|
}
|
|
18
|
+
const SENSITIVE_HEADERS = [
|
|
19
|
+
"authorization",
|
|
20
|
+
"cookie",
|
|
21
|
+
"set-cookie",
|
|
22
|
+
"x-api-key",
|
|
23
|
+
"x-auth-token",
|
|
24
|
+
"proxy-authorization"
|
|
25
|
+
];
|
|
26
|
+
function getSafeHeaders(event) {
|
|
27
|
+
const allHeaders = getHeaders(event);
|
|
28
|
+
const safeHeaders = {};
|
|
29
|
+
for (const [key, value] of Object.entries(allHeaders)) {
|
|
30
|
+
if (!SENSITIVE_HEADERS.includes(key.toLowerCase())) {
|
|
31
|
+
safeHeaders[key] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return safeHeaders;
|
|
35
|
+
}
|
|
16
36
|
function getResponseStatus(event) {
|
|
17
37
|
if (event.node?.res?.statusCode) {
|
|
18
38
|
return event.node.res.statusCode;
|
|
@@ -25,18 +45,31 @@ function getResponseStatus(event) {
|
|
|
25
45
|
}
|
|
26
46
|
return 200;
|
|
27
47
|
}
|
|
48
|
+
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
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
28
59
|
const plugin = defineNitroPlugin((nitroApp) => {
|
|
29
60
|
const config = useRuntimeConfig();
|
|
30
61
|
const evlogConfig = config.evlog;
|
|
31
62
|
initLogger({
|
|
32
63
|
env: evlogConfig?.env,
|
|
33
|
-
pretty: evlogConfig?.pretty
|
|
64
|
+
pretty: evlogConfig?.pretty,
|
|
65
|
+
sampling: evlogConfig?.sampling
|
|
34
66
|
});
|
|
35
67
|
nitroApp.hooks.hook("request", (event) => {
|
|
36
68
|
const e = event;
|
|
37
|
-
if (!shouldLog(e.path, evlogConfig?.include)) {
|
|
69
|
+
if (!shouldLog(e.path, evlogConfig?.include, evlogConfig?.exclude)) {
|
|
38
70
|
return;
|
|
39
71
|
}
|
|
72
|
+
e.context._evlogStartTime = Date.now();
|
|
40
73
|
const log = createRequestLogger({
|
|
41
74
|
method: e.method,
|
|
42
75
|
path: e.path,
|
|
@@ -44,19 +77,50 @@ const plugin = defineNitroPlugin((nitroApp) => {
|
|
|
44
77
|
});
|
|
45
78
|
e.context.log = log;
|
|
46
79
|
});
|
|
47
|
-
nitroApp.hooks.hook("
|
|
80
|
+
nitroApp.hooks.hook("error", async (error, { event }) => {
|
|
48
81
|
const e = event;
|
|
82
|
+
if (!e) return;
|
|
49
83
|
const log = e.context.log;
|
|
50
84
|
if (log) {
|
|
51
|
-
log.
|
|
52
|
-
|
|
85
|
+
log.error(error);
|
|
86
|
+
const errorStatus = error.statusCode ?? 500;
|
|
87
|
+
log.set({ status: errorStatus });
|
|
88
|
+
const startTime = e.context._evlogStartTime;
|
|
89
|
+
const durationMs = startTime ? Date.now() - startTime : void 0;
|
|
90
|
+
const tailCtx = {
|
|
91
|
+
status: errorStatus,
|
|
92
|
+
duration: durationMs,
|
|
93
|
+
path: e.path,
|
|
94
|
+
method: e.method,
|
|
95
|
+
context: log.getContext(),
|
|
96
|
+
shouldKeep: false
|
|
97
|
+
};
|
|
98
|
+
await nitroApp.hooks.callHook("evlog:emit:keep", tailCtx);
|
|
99
|
+
e.context._evlogEmitted = true;
|
|
100
|
+
const emittedEvent = log.emit({ _forceKeep: tailCtx.shouldKeep });
|
|
101
|
+
callDrainHook(nitroApp, emittedEvent, e);
|
|
53
102
|
}
|
|
54
103
|
});
|
|
55
|
-
nitroApp.hooks.hook("
|
|
104
|
+
nitroApp.hooks.hook("afterResponse", async (event) => {
|
|
56
105
|
const e = event;
|
|
57
|
-
|
|
106
|
+
if (e.context._evlogEmitted) return;
|
|
107
|
+
const log = e.context.log;
|
|
58
108
|
if (log) {
|
|
59
|
-
|
|
109
|
+
const status = getResponseStatus(e);
|
|
110
|
+
log.set({ status });
|
|
111
|
+
const startTime = e.context._evlogStartTime;
|
|
112
|
+
const durationMs = startTime ? Date.now() - startTime : void 0;
|
|
113
|
+
const tailCtx = {
|
|
114
|
+
status,
|
|
115
|
+
duration: durationMs,
|
|
116
|
+
path: e.path,
|
|
117
|
+
method: e.method,
|
|
118
|
+
context: log.getContext(),
|
|
119
|
+
shouldKeep: false
|
|
120
|
+
};
|
|
121
|
+
await nitroApp.hooks.callHook("evlog:emit:keep", tailCtx);
|
|
122
|
+
const emittedEvent = log.emit({ _forceKeep: tailCtx.shouldKeep });
|
|
123
|
+
callDrainHook(nitroApp, emittedEvent, e);
|
|
60
124
|
}
|
|
61
125
|
});
|
|
62
126
|
});
|