evlog 1.0.1 → 1.1.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/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/logger.d.mts +7 -2
- package/dist/logger.d.ts +7 -2
- package/dist/logger.mjs +55 -7
- package/dist/nitro/plugin.mjs +51 -13
- package/dist/nuxt/module.d.mts +18 -1
- package/dist/nuxt/module.d.ts +18 -1
- package/dist/nuxt/module.mjs +1 -1
- package/dist/runtime/client/log.mjs +12 -21
- package/dist/runtime/server/useLogger.d.mts +14 -0
- package/dist/runtime/server/useLogger.d.ts +14 -0
- package/dist/types.d.mts +147 -3
- package/dist/types.d.ts +147 -3
- package/dist/utils.d.mts +6 -1
- package/dist/utils.d.ts +6 -1
- package/dist/utils.mjs +6 -1
- package/package.json +1 -1
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/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { EvlogError, createError, createEvlogError } from './error.mjs';
|
|
2
|
-
export { createRequestLogger, getEnvironment, initLogger, log } from './logger.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, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent } from './types.mjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { EvlogError, createError, createEvlogError } from './error.js';
|
|
2
|
-
export { createRequestLogger, getEnvironment, initLogger, log } from './logger.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, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent } from './types.js';
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { EvlogError, createError, createEvlogError } from './error.mjs';
|
|
2
|
-
export { createRequestLogger, getEnvironment, initLogger, log } from './logger.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';
|
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,11 @@
|
|
|
1
|
-
import { isDev, detectEnvironment, formatDuration, getConsoleMethod, colors, getLevelColor } from './utils.mjs';
|
|
1
|
+
import { isDev, detectEnvironment, formatDuration, matchesPattern, getConsoleMethod, colors, getLevelColor } from './utils.mjs';
|
|
2
2
|
|
|
3
3
|
let globalEnv = {
|
|
4
4
|
service: "app",
|
|
5
5
|
environment: "development"
|
|
6
6
|
};
|
|
7
7
|
let globalPretty = isDev();
|
|
8
|
+
let globalSampling = {};
|
|
8
9
|
function initLogger(config = {}) {
|
|
9
10
|
const detected = detectEnvironment();
|
|
10
11
|
globalEnv = {
|
|
@@ -15,8 +16,38 @@ function initLogger(config = {}) {
|
|
|
15
16
|
region: config.env?.region ?? detected.region
|
|
16
17
|
};
|
|
17
18
|
globalPretty = config.pretty ?? isDev();
|
|
19
|
+
globalSampling = config.sampling ?? {};
|
|
18
20
|
}
|
|
19
|
-
function
|
|
21
|
+
function shouldSample(level) {
|
|
22
|
+
const { rates } = globalSampling;
|
|
23
|
+
if (!rates) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const percentage = level === "error" && rates.error === void 0 ? 100 : rates[level] ?? 100;
|
|
27
|
+
if (percentage <= 0) return false;
|
|
28
|
+
if (percentage >= 100) return true;
|
|
29
|
+
return Math.random() * 100 < percentage;
|
|
30
|
+
}
|
|
31
|
+
function shouldKeep(ctx) {
|
|
32
|
+
const { keep } = globalSampling;
|
|
33
|
+
if (!keep?.length) return false;
|
|
34
|
+
return keep.some((condition) => {
|
|
35
|
+
if (condition.status !== void 0 && ctx.status !== void 0 && ctx.status >= condition.status) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (condition.duration !== void 0 && ctx.duration !== void 0 && ctx.duration >= condition.duration) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (condition.path && ctx.path && matchesPattern(ctx.path, condition.path)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function emitWideEvent(level, event, skipSamplingCheck = false) {
|
|
48
|
+
if (!skipSamplingCheck && !shouldSample(level)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
20
51
|
const formatted = {
|
|
21
52
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22
53
|
level,
|
|
@@ -28,9 +59,13 @@ function emitWideEvent(level, event) {
|
|
|
28
59
|
} else {
|
|
29
60
|
console[getConsoleMethod(level)](JSON.stringify(formatted));
|
|
30
61
|
}
|
|
62
|
+
return formatted;
|
|
31
63
|
}
|
|
32
64
|
function emitTaggedLog(level, tag, message) {
|
|
33
65
|
if (globalPretty) {
|
|
66
|
+
if (!shouldSample(level)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
34
69
|
const color = getLevelColor(level);
|
|
35
70
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
36
71
|
console.log(`${colors.dim}${timestamp}${colors.reset} ${color}[${tag}]${colors.reset} ${message}`);
|
|
@@ -130,13 +165,26 @@ function createRequestLogger(options = {}) {
|
|
|
130
165
|
};
|
|
131
166
|
},
|
|
132
167
|
emit(overrides) {
|
|
133
|
-
const
|
|
168
|
+
const durationMs = Date.now() - startTime;
|
|
169
|
+
const duration = formatDuration(durationMs);
|
|
134
170
|
const level = hasError ? "error" : "info";
|
|
135
|
-
|
|
171
|
+
const { _forceKeep, ...restOverrides } = overrides ?? {};
|
|
172
|
+
const tailCtx = {
|
|
173
|
+
status: context.status ?? restOverrides.status,
|
|
174
|
+
duration: durationMs,
|
|
175
|
+
path: context.path,
|
|
176
|
+
method: context.method,
|
|
177
|
+
context: { ...context, ...restOverrides }
|
|
178
|
+
};
|
|
179
|
+
const forceKeep = _forceKeep || shouldKeep(tailCtx);
|
|
180
|
+
if (!forceKeep && !shouldSample(level)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return emitWideEvent(level, {
|
|
136
184
|
...context,
|
|
137
|
-
...
|
|
185
|
+
...restOverrides,
|
|
138
186
|
duration
|
|
139
|
-
});
|
|
187
|
+
}, true);
|
|
140
188
|
},
|
|
141
189
|
getContext() {
|
|
142
190
|
return { ...context };
|
|
@@ -147,4 +195,4 @@ function getEnvironment() {
|
|
|
147
195
|
return { ...globalEnv };
|
|
148
196
|
}
|
|
149
197
|
|
|
150
|
-
export { createRequestLogger, getEnvironment, initLogger, log };
|
|
198
|
+
export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep };
|
package/dist/nitro/plugin.mjs
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime';
|
|
2
2
|
import { initLogger, createRequestLogger } from '../logger.mjs';
|
|
3
|
-
import '../utils.mjs';
|
|
3
|
+
import { matchesPattern } from '../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
5
|
function shouldLog(path, include) {
|
|
11
6
|
if (!include || include.length === 0) {
|
|
12
7
|
return true;
|
|
@@ -25,18 +20,30 @@ function getResponseStatus(event) {
|
|
|
25
20
|
}
|
|
26
21
|
return 200;
|
|
27
22
|
}
|
|
23
|
+
function callDrainHook(nitroApp, emittedEvent, event) {
|
|
24
|
+
if (emittedEvent) {
|
|
25
|
+
nitroApp.hooks.callHook("evlog:drain", {
|
|
26
|
+
event: emittedEvent,
|
|
27
|
+
request: { method: event.method, path: event.path, requestId: event.context.requestId }
|
|
28
|
+
}).catch((err) => {
|
|
29
|
+
console.error("[evlog] drain failed:", err);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
28
33
|
const plugin = defineNitroPlugin((nitroApp) => {
|
|
29
34
|
const config = useRuntimeConfig();
|
|
30
35
|
const evlogConfig = config.evlog;
|
|
31
36
|
initLogger({
|
|
32
37
|
env: evlogConfig?.env,
|
|
33
|
-
pretty: evlogConfig?.pretty
|
|
38
|
+
pretty: evlogConfig?.pretty,
|
|
39
|
+
sampling: evlogConfig?.sampling
|
|
34
40
|
});
|
|
35
41
|
nitroApp.hooks.hook("request", (event) => {
|
|
36
42
|
const e = event;
|
|
37
43
|
if (!shouldLog(e.path, evlogConfig?.include)) {
|
|
38
44
|
return;
|
|
39
45
|
}
|
|
46
|
+
e.context._evlogStartTime = Date.now();
|
|
40
47
|
const log = createRequestLogger({
|
|
41
48
|
method: e.method,
|
|
42
49
|
path: e.path,
|
|
@@ -44,19 +51,50 @@ const plugin = defineNitroPlugin((nitroApp) => {
|
|
|
44
51
|
});
|
|
45
52
|
e.context.log = log;
|
|
46
53
|
});
|
|
47
|
-
nitroApp.hooks.hook("
|
|
54
|
+
nitroApp.hooks.hook("error", async (error, { event }) => {
|
|
48
55
|
const e = event;
|
|
56
|
+
if (!e) return;
|
|
49
57
|
const log = e.context.log;
|
|
50
58
|
if (log) {
|
|
51
|
-
log.
|
|
52
|
-
|
|
59
|
+
log.error(error);
|
|
60
|
+
const errorStatus = error.statusCode ?? 500;
|
|
61
|
+
log.set({ status: errorStatus });
|
|
62
|
+
const startTime = e.context._evlogStartTime;
|
|
63
|
+
const durationMs = startTime ? Date.now() - startTime : void 0;
|
|
64
|
+
const tailCtx = {
|
|
65
|
+
status: errorStatus,
|
|
66
|
+
duration: durationMs,
|
|
67
|
+
path: e.path,
|
|
68
|
+
method: e.method,
|
|
69
|
+
context: log.getContext(),
|
|
70
|
+
shouldKeep: false
|
|
71
|
+
};
|
|
72
|
+
await nitroApp.hooks.callHook("evlog:emit:keep", tailCtx);
|
|
73
|
+
e.context._evlogEmitted = true;
|
|
74
|
+
const emittedEvent = log.emit({ _forceKeep: tailCtx.shouldKeep });
|
|
75
|
+
callDrainHook(nitroApp, emittedEvent, e);
|
|
53
76
|
}
|
|
54
77
|
});
|
|
55
|
-
nitroApp.hooks.hook("
|
|
78
|
+
nitroApp.hooks.hook("afterResponse", async (event) => {
|
|
56
79
|
const e = event;
|
|
57
|
-
|
|
80
|
+
if (e.context._evlogEmitted) return;
|
|
81
|
+
const log = e.context.log;
|
|
58
82
|
if (log) {
|
|
59
|
-
|
|
83
|
+
const status = getResponseStatus(e);
|
|
84
|
+
log.set({ status });
|
|
85
|
+
const startTime = e.context._evlogStartTime;
|
|
86
|
+
const durationMs = startTime ? Date.now() - startTime : void 0;
|
|
87
|
+
const tailCtx = {
|
|
88
|
+
status,
|
|
89
|
+
duration: durationMs,
|
|
90
|
+
path: e.path,
|
|
91
|
+
method: e.method,
|
|
92
|
+
context: log.getContext(),
|
|
93
|
+
shouldKeep: false
|
|
94
|
+
};
|
|
95
|
+
await nitroApp.hooks.callHook("evlog:emit:keep", tailCtx);
|
|
96
|
+
const emittedEvent = log.emit({ _forceKeep: tailCtx.shouldKeep });
|
|
97
|
+
callDrainHook(nitroApp, emittedEvent, e);
|
|
60
98
|
}
|
|
61
99
|
});
|
|
62
100
|
});
|
package/dist/nuxt/module.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
-
import { EnvironmentContext } from '../types.mjs';
|
|
2
|
+
import { EnvironmentContext, SamplingConfig } from '../types.mjs';
|
|
3
3
|
|
|
4
4
|
interface ModuleOptions {
|
|
5
5
|
/**
|
|
@@ -18,6 +18,23 @@ interface ModuleOptions {
|
|
|
18
18
|
* @example ['/api/**', '/auth/**']
|
|
19
19
|
*/
|
|
20
20
|
include?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Sampling configuration for filtering logs.
|
|
23
|
+
* Allows configuring what percentage of logs to keep per level.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* sampling: {
|
|
28
|
+
* rates: {
|
|
29
|
+
* info: 10, // Keep 10% of info logs
|
|
30
|
+
* warn: 50, // Keep 50% of warning logs
|
|
31
|
+
* debug: 5, // Keep 5% of debug logs
|
|
32
|
+
* error: 100, // Always keep errors (default)
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
sampling?: SamplingConfig;
|
|
21
38
|
}
|
|
22
39
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
23
40
|
|
package/dist/nuxt/module.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
-
import { EnvironmentContext } from '../types.js';
|
|
2
|
+
import { EnvironmentContext, SamplingConfig } from '../types.js';
|
|
3
3
|
|
|
4
4
|
interface ModuleOptions {
|
|
5
5
|
/**
|
|
@@ -18,6 +18,23 @@ interface ModuleOptions {
|
|
|
18
18
|
* @example ['/api/**', '/auth/**']
|
|
19
19
|
*/
|
|
20
20
|
include?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Sampling configuration for filtering logs.
|
|
23
|
+
* Allows configuring what percentage of logs to keep per level.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* sampling: {
|
|
28
|
+
* rates: {
|
|
29
|
+
* info: 10, // Keep 10% of info logs
|
|
30
|
+
* warn: 50, // Keep 50% of warning logs
|
|
31
|
+
* debug: 5, // Keep 5% of debug logs
|
|
32
|
+
* error: 100, // Always keep errors (default)
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
sampling?: SamplingConfig;
|
|
21
38
|
}
|
|
22
39
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
23
40
|
|
package/dist/nuxt/module.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getConsoleMethod } from '../../utils.mjs';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const isClient = typeof window !== "undefined";
|
|
4
4
|
let clientPretty = true;
|
|
5
5
|
let clientService = "client";
|
|
6
6
|
const LEVEL_COLORS = {
|
|
@@ -13,7 +13,7 @@ function initLog(options = {}) {
|
|
|
13
13
|
clientPretty = options.pretty ?? true;
|
|
14
14
|
clientService = options.service ?? "client";
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
function emitLog(level, event) {
|
|
17
17
|
const formatted = {
|
|
18
18
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19
19
|
level,
|
|
@@ -28,33 +28,24 @@ function emitClientWideEvent(level, event) {
|
|
|
28
28
|
console[method](JSON.stringify(formatted));
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
function
|
|
31
|
+
function emitTaggedLog(level, tag, message) {
|
|
32
32
|
if (clientPretty) {
|
|
33
33
|
console[getConsoleMethod(level)](`%c[${tag}]%c ${message}`, LEVEL_COLORS[level] || "", "color: inherit");
|
|
34
34
|
} else {
|
|
35
|
-
|
|
35
|
+
emitLog(level, { tag, message });
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function createLogMethod(level) {
|
|
39
39
|
return function logMethod(tagOrEvent, message) {
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
40
|
+
if (!(import.meta.client ?? isClient)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (typeof tagOrEvent === "string" && message !== void 0) {
|
|
44
|
+
emitTaggedLog(level, tagOrEvent, message);
|
|
45
|
+
} else if (typeof tagOrEvent === "object") {
|
|
46
|
+
emitLog(level, tagOrEvent);
|
|
48
47
|
} else {
|
|
49
|
-
|
|
50
|
-
if (typeof tagOrEvent === "string" && message !== void 0) {
|
|
51
|
-
serverLog[level](tagOrEvent, message);
|
|
52
|
-
} else if (typeof tagOrEvent === "object") {
|
|
53
|
-
serverLog[level](tagOrEvent);
|
|
54
|
-
} else {
|
|
55
|
-
serverLog[level]("log", String(tagOrEvent));
|
|
56
|
-
}
|
|
57
|
-
});
|
|
48
|
+
emitTaggedLog(level, "log", String(tagOrEvent));
|
|
58
49
|
}
|
|
59
50
|
};
|
|
60
51
|
}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { ServerEvent, RequestLogger } from '../../types.mjs';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Returns the request logger attached to the given server event.
|
|
5
|
+
*
|
|
6
|
+
* @param event - The current server event containing the context with the logger.
|
|
7
|
+
* @returns The request-scoped logger.
|
|
8
|
+
* @throws Error if the logger is not initialized on the event context.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* export default defineEventHandler((event) => {
|
|
12
|
+
* const log = useLogger(event)
|
|
13
|
+
* log.set({ foo: 'bar' })
|
|
14
|
+
* // ...
|
|
15
|
+
* })
|
|
16
|
+
*/
|
|
3
17
|
declare function useLogger(event: ServerEvent): RequestLogger;
|
|
4
18
|
|
|
5
19
|
export { useLogger };
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { ServerEvent, RequestLogger } from '../../types.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Returns the request logger attached to the given server event.
|
|
5
|
+
*
|
|
6
|
+
* @param event - The current server event containing the context with the logger.
|
|
7
|
+
* @returns The request-scoped logger.
|
|
8
|
+
* @throws Error if the logger is not initialized on the event context.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* export default defineEventHandler((event) => {
|
|
12
|
+
* const log = useLogger(event)
|
|
13
|
+
* log.set({ foo: 'bar' })
|
|
14
|
+
* // ...
|
|
15
|
+
* })
|
|
16
|
+
*/
|
|
3
17
|
declare function useLogger(event: ServerEvent): RequestLogger;
|
|
4
18
|
|
|
5
19
|
export { useLogger };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,3 +1,140 @@
|
|
|
1
|
+
declare module 'nitropack/types' {
|
|
2
|
+
interface NitroRuntimeHooks {
|
|
3
|
+
/**
|
|
4
|
+
* Tail sampling hook - called before emitting a log.
|
|
5
|
+
* Set `ctx.shouldKeep = true` to force-keep the log regardless of head sampling.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
|
|
10
|
+
* if (ctx.context.user?.premium) {
|
|
11
|
+
* ctx.shouldKeep = true
|
|
12
|
+
* }
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
'evlog:emit:keep': (ctx: TailSamplingContext) => void | Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Drain hook - called after emitting a log (fire-and-forget).
|
|
19
|
+
* Use this to send logs to external services like Axiom, Loki, or custom endpoints.
|
|
20
|
+
* Errors are logged but never block the request.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* nitroApp.hooks.hook('evlog:drain', async (ctx) => {
|
|
25
|
+
* await fetch('https://api.axiom.co/v1/datasets/logs/ingest', {
|
|
26
|
+
* method: 'POST',
|
|
27
|
+
* headers: { Authorization: `Bearer ${process.env.AXIOM_TOKEN}` },
|
|
28
|
+
* body: JSON.stringify([ctx.event])
|
|
29
|
+
* })
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
'evlog:drain': (ctx: DrainContext) => void | Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sampling rates per log level (0-100 percentage)
|
|
38
|
+
*/
|
|
39
|
+
interface SamplingRates {
|
|
40
|
+
/** Percentage of info logs to keep (0-100). Default: 100 */
|
|
41
|
+
info?: number;
|
|
42
|
+
/** Percentage of warn logs to keep (0-100). Default: 100 */
|
|
43
|
+
warn?: number;
|
|
44
|
+
/** Percentage of debug logs to keep (0-100). Default: 100 */
|
|
45
|
+
debug?: number;
|
|
46
|
+
/** Percentage of error logs to keep (0-100). Default: 100 */
|
|
47
|
+
error?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Tail sampling condition for forcing log retention based on request outcome.
|
|
51
|
+
* All conditions use >= comparison (e.g., status: 400 means status >= 400).
|
|
52
|
+
*/
|
|
53
|
+
interface TailSamplingCondition {
|
|
54
|
+
/** Keep if HTTP status >= this value (e.g., 400 for all errors) */
|
|
55
|
+
status?: number;
|
|
56
|
+
/** Keep if request duration >= this value in milliseconds */
|
|
57
|
+
duration?: number;
|
|
58
|
+
/** Keep if path matches this glob pattern (e.g., '/api/critical/**') */
|
|
59
|
+
path?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Context passed to tail sampling evaluation and hooks.
|
|
63
|
+
* Contains request outcome information for sampling decisions.
|
|
64
|
+
*/
|
|
65
|
+
interface TailSamplingContext {
|
|
66
|
+
/** HTTP response status code */
|
|
67
|
+
status?: number;
|
|
68
|
+
/** Request duration in milliseconds (raw number) */
|
|
69
|
+
duration?: number;
|
|
70
|
+
/** Request path */
|
|
71
|
+
path?: string;
|
|
72
|
+
/** HTTP method */
|
|
73
|
+
method?: string;
|
|
74
|
+
/** Full accumulated context from the request logger */
|
|
75
|
+
context: Record<string, unknown>;
|
|
76
|
+
/**
|
|
77
|
+
* Set to true in evlog:emit:keep hook to force keep this log.
|
|
78
|
+
* Multiple hooks can set this - if any sets it to true, the log is kept.
|
|
79
|
+
*/
|
|
80
|
+
shouldKeep?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Context passed to the evlog:drain hook.
|
|
84
|
+
* Contains the complete wide event and request metadata for external transport.
|
|
85
|
+
*/
|
|
86
|
+
interface DrainContext {
|
|
87
|
+
/** The complete wide event to drain */
|
|
88
|
+
event: WideEvent;
|
|
89
|
+
/** Request metadata (if available) */
|
|
90
|
+
request?: {
|
|
91
|
+
method?: string;
|
|
92
|
+
path?: string;
|
|
93
|
+
requestId?: string;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Sampling configuration for filtering logs
|
|
98
|
+
*/
|
|
99
|
+
interface SamplingConfig {
|
|
100
|
+
/**
|
|
101
|
+
* Sampling rates per log level (head sampling).
|
|
102
|
+
* Values are percentages from 0 to 100.
|
|
103
|
+
* Default: 100 for all levels (log everything).
|
|
104
|
+
* Error defaults to 100 even if not specified.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* sampling: {
|
|
109
|
+
* rates: {
|
|
110
|
+
* info: 10, // Keep 10% of info logs
|
|
111
|
+
* warn: 50, // Keep 50% of warning logs
|
|
112
|
+
* debug: 5, // Keep 5% of debug logs
|
|
113
|
+
* error: 100, // Always keep errors (default)
|
|
114
|
+
* }
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
rates?: SamplingRates;
|
|
119
|
+
/**
|
|
120
|
+
* Tail sampling conditions for forcing log retention (OR logic).
|
|
121
|
+
* If ANY condition matches, the log is kept regardless of head sampling.
|
|
122
|
+
* Use the `evlog:emit:keep` Nitro hook for custom conditions.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* sampling: {
|
|
127
|
+
* rates: { info: 10 }, // Head sampling: keep 10% of info logs
|
|
128
|
+
* keep: [
|
|
129
|
+
* { status: 400 }, // Always keep if status >= 400
|
|
130
|
+
* { duration: 1000 }, // Always keep if duration >= 1000ms
|
|
131
|
+
* { path: '/api/critical/**' }, // Always keep critical paths
|
|
132
|
+
* ]
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
keep?: TailSamplingCondition[];
|
|
137
|
+
}
|
|
1
138
|
/**
|
|
2
139
|
* Environment context automatically included in every log event
|
|
3
140
|
*/
|
|
@@ -21,6 +158,8 @@ interface LoggerConfig {
|
|
|
21
158
|
env?: Partial<EnvironmentContext>;
|
|
22
159
|
/** Enable pretty printing (auto-detected: true in dev, false in prod) */
|
|
23
160
|
pretty?: boolean;
|
|
161
|
+
/** Sampling configuration for filtering logs */
|
|
162
|
+
sampling?: SamplingConfig;
|
|
24
163
|
}
|
|
25
164
|
/**
|
|
26
165
|
* Base structure for all wide events
|
|
@@ -59,9 +198,10 @@ interface RequestLogger {
|
|
|
59
198
|
*/
|
|
60
199
|
error: (error: Error | string, context?: Record<string, unknown>) => void;
|
|
61
200
|
/**
|
|
62
|
-
* Emit the final wide event with all accumulated context
|
|
201
|
+
* Emit the final wide event with all accumulated context.
|
|
202
|
+
* Returns the emitted WideEvent, or null if the log was sampled out.
|
|
63
203
|
*/
|
|
64
|
-
emit: (overrides?: Record<string, unknown>) =>
|
|
204
|
+
emit: (overrides?: Record<string, unknown>) => WideEvent | null;
|
|
65
205
|
/**
|
|
66
206
|
* Get the current accumulated context
|
|
67
207
|
*/
|
|
@@ -142,6 +282,10 @@ interface H3EventContext {
|
|
|
142
282
|
log?: RequestLogger;
|
|
143
283
|
requestId?: string;
|
|
144
284
|
status?: number;
|
|
285
|
+
/** Internal: start time for duration calculation in tail sampling */
|
|
286
|
+
_evlogStartTime?: number;
|
|
287
|
+
/** Internal: flag to prevent double emission on errors */
|
|
288
|
+
_evlogEmitted?: boolean;
|
|
145
289
|
[key: string]: unknown;
|
|
146
290
|
}
|
|
147
291
|
/**
|
|
@@ -170,4 +314,4 @@ interface ParsedError {
|
|
|
170
314
|
raw: unknown;
|
|
171
315
|
}
|
|
172
316
|
|
|
173
|
-
export type { BaseWideEvent, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, ServerEvent, WideEvent };
|
|
317
|
+
export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,140 @@
|
|
|
1
|
+
declare module 'nitropack/types' {
|
|
2
|
+
interface NitroRuntimeHooks {
|
|
3
|
+
/**
|
|
4
|
+
* Tail sampling hook - called before emitting a log.
|
|
5
|
+
* Set `ctx.shouldKeep = true` to force-keep the log regardless of head sampling.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
|
|
10
|
+
* if (ctx.context.user?.premium) {
|
|
11
|
+
* ctx.shouldKeep = true
|
|
12
|
+
* }
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
'evlog:emit:keep': (ctx: TailSamplingContext) => void | Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Drain hook - called after emitting a log (fire-and-forget).
|
|
19
|
+
* Use this to send logs to external services like Axiom, Loki, or custom endpoints.
|
|
20
|
+
* Errors are logged but never block the request.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* nitroApp.hooks.hook('evlog:drain', async (ctx) => {
|
|
25
|
+
* await fetch('https://api.axiom.co/v1/datasets/logs/ingest', {
|
|
26
|
+
* method: 'POST',
|
|
27
|
+
* headers: { Authorization: `Bearer ${process.env.AXIOM_TOKEN}` },
|
|
28
|
+
* body: JSON.stringify([ctx.event])
|
|
29
|
+
* })
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
'evlog:drain': (ctx: DrainContext) => void | Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sampling rates per log level (0-100 percentage)
|
|
38
|
+
*/
|
|
39
|
+
interface SamplingRates {
|
|
40
|
+
/** Percentage of info logs to keep (0-100). Default: 100 */
|
|
41
|
+
info?: number;
|
|
42
|
+
/** Percentage of warn logs to keep (0-100). Default: 100 */
|
|
43
|
+
warn?: number;
|
|
44
|
+
/** Percentage of debug logs to keep (0-100). Default: 100 */
|
|
45
|
+
debug?: number;
|
|
46
|
+
/** Percentage of error logs to keep (0-100). Default: 100 */
|
|
47
|
+
error?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Tail sampling condition for forcing log retention based on request outcome.
|
|
51
|
+
* All conditions use >= comparison (e.g., status: 400 means status >= 400).
|
|
52
|
+
*/
|
|
53
|
+
interface TailSamplingCondition {
|
|
54
|
+
/** Keep if HTTP status >= this value (e.g., 400 for all errors) */
|
|
55
|
+
status?: number;
|
|
56
|
+
/** Keep if request duration >= this value in milliseconds */
|
|
57
|
+
duration?: number;
|
|
58
|
+
/** Keep if path matches this glob pattern (e.g., '/api/critical/**') */
|
|
59
|
+
path?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Context passed to tail sampling evaluation and hooks.
|
|
63
|
+
* Contains request outcome information for sampling decisions.
|
|
64
|
+
*/
|
|
65
|
+
interface TailSamplingContext {
|
|
66
|
+
/** HTTP response status code */
|
|
67
|
+
status?: number;
|
|
68
|
+
/** Request duration in milliseconds (raw number) */
|
|
69
|
+
duration?: number;
|
|
70
|
+
/** Request path */
|
|
71
|
+
path?: string;
|
|
72
|
+
/** HTTP method */
|
|
73
|
+
method?: string;
|
|
74
|
+
/** Full accumulated context from the request logger */
|
|
75
|
+
context: Record<string, unknown>;
|
|
76
|
+
/**
|
|
77
|
+
* Set to true in evlog:emit:keep hook to force keep this log.
|
|
78
|
+
* Multiple hooks can set this - if any sets it to true, the log is kept.
|
|
79
|
+
*/
|
|
80
|
+
shouldKeep?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Context passed to the evlog:drain hook.
|
|
84
|
+
* Contains the complete wide event and request metadata for external transport.
|
|
85
|
+
*/
|
|
86
|
+
interface DrainContext {
|
|
87
|
+
/** The complete wide event to drain */
|
|
88
|
+
event: WideEvent;
|
|
89
|
+
/** Request metadata (if available) */
|
|
90
|
+
request?: {
|
|
91
|
+
method?: string;
|
|
92
|
+
path?: string;
|
|
93
|
+
requestId?: string;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Sampling configuration for filtering logs
|
|
98
|
+
*/
|
|
99
|
+
interface SamplingConfig {
|
|
100
|
+
/**
|
|
101
|
+
* Sampling rates per log level (head sampling).
|
|
102
|
+
* Values are percentages from 0 to 100.
|
|
103
|
+
* Default: 100 for all levels (log everything).
|
|
104
|
+
* Error defaults to 100 even if not specified.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* sampling: {
|
|
109
|
+
* rates: {
|
|
110
|
+
* info: 10, // Keep 10% of info logs
|
|
111
|
+
* warn: 50, // Keep 50% of warning logs
|
|
112
|
+
* debug: 5, // Keep 5% of debug logs
|
|
113
|
+
* error: 100, // Always keep errors (default)
|
|
114
|
+
* }
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
rates?: SamplingRates;
|
|
119
|
+
/**
|
|
120
|
+
* Tail sampling conditions for forcing log retention (OR logic).
|
|
121
|
+
* If ANY condition matches, the log is kept regardless of head sampling.
|
|
122
|
+
* Use the `evlog:emit:keep` Nitro hook for custom conditions.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* sampling: {
|
|
127
|
+
* rates: { info: 10 }, // Head sampling: keep 10% of info logs
|
|
128
|
+
* keep: [
|
|
129
|
+
* { status: 400 }, // Always keep if status >= 400
|
|
130
|
+
* { duration: 1000 }, // Always keep if duration >= 1000ms
|
|
131
|
+
* { path: '/api/critical/**' }, // Always keep critical paths
|
|
132
|
+
* ]
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
keep?: TailSamplingCondition[];
|
|
137
|
+
}
|
|
1
138
|
/**
|
|
2
139
|
* Environment context automatically included in every log event
|
|
3
140
|
*/
|
|
@@ -21,6 +158,8 @@ interface LoggerConfig {
|
|
|
21
158
|
env?: Partial<EnvironmentContext>;
|
|
22
159
|
/** Enable pretty printing (auto-detected: true in dev, false in prod) */
|
|
23
160
|
pretty?: boolean;
|
|
161
|
+
/** Sampling configuration for filtering logs */
|
|
162
|
+
sampling?: SamplingConfig;
|
|
24
163
|
}
|
|
25
164
|
/**
|
|
26
165
|
* Base structure for all wide events
|
|
@@ -59,9 +198,10 @@ interface RequestLogger {
|
|
|
59
198
|
*/
|
|
60
199
|
error: (error: Error | string, context?: Record<string, unknown>) => void;
|
|
61
200
|
/**
|
|
62
|
-
* Emit the final wide event with all accumulated context
|
|
201
|
+
* Emit the final wide event with all accumulated context.
|
|
202
|
+
* Returns the emitted WideEvent, or null if the log was sampled out.
|
|
63
203
|
*/
|
|
64
|
-
emit: (overrides?: Record<string, unknown>) =>
|
|
204
|
+
emit: (overrides?: Record<string, unknown>) => WideEvent | null;
|
|
65
205
|
/**
|
|
66
206
|
* Get the current accumulated context
|
|
67
207
|
*/
|
|
@@ -142,6 +282,10 @@ interface H3EventContext {
|
|
|
142
282
|
log?: RequestLogger;
|
|
143
283
|
requestId?: string;
|
|
144
284
|
status?: number;
|
|
285
|
+
/** Internal: start time for duration calculation in tail sampling */
|
|
286
|
+
_evlogStartTime?: number;
|
|
287
|
+
/** Internal: flag to prevent double emission on errors */
|
|
288
|
+
_evlogEmitted?: boolean;
|
|
145
289
|
[key: string]: unknown;
|
|
146
290
|
}
|
|
147
291
|
/**
|
|
@@ -170,4 +314,4 @@ interface ParsedError {
|
|
|
170
314
|
raw: unknown;
|
|
171
315
|
}
|
|
172
316
|
|
|
173
|
-
export type { BaseWideEvent, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, ServerEvent, WideEvent };
|
|
317
|
+
export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent };
|
package/dist/utils.d.mts
CHANGED
|
@@ -20,5 +20,10 @@ declare const colors: {
|
|
|
20
20
|
readonly gray: "\u001B[90m";
|
|
21
21
|
};
|
|
22
22
|
declare function getLevelColor(level: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Match a path against a glob pattern.
|
|
25
|
+
* Supports * (any chars except /) and ** (any chars including /).
|
|
26
|
+
*/
|
|
27
|
+
declare function matchesPattern(path: string, pattern: string): boolean;
|
|
23
28
|
|
|
24
|
-
export { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isClient, isDev, isServer };
|
|
29
|
+
export { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isClient, isDev, isServer, matchesPattern };
|
package/dist/utils.d.ts
CHANGED
|
@@ -20,5 +20,10 @@ declare const colors: {
|
|
|
20
20
|
readonly gray: "\u001B[90m";
|
|
21
21
|
};
|
|
22
22
|
declare function getLevelColor(level: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Match a path against a glob pattern.
|
|
25
|
+
* Supports * (any chars except /) and ** (any chars including /).
|
|
26
|
+
*/
|
|
27
|
+
declare function matchesPattern(path: string, pattern: string): boolean;
|
|
23
28
|
|
|
24
|
-
export { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isClient, isDev, isServer };
|
|
29
|
+
export { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isClient, isDev, isServer, matchesPattern };
|
package/dist/utils.mjs
CHANGED
|
@@ -56,5 +56,10 @@ function getLevelColor(level) {
|
|
|
56
56
|
return colors.white;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
function matchesPattern(path, pattern) {
|
|
60
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*").replace(/\?/g, "[^/]");
|
|
61
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
62
|
+
return regex.test(path);
|
|
63
|
+
}
|
|
59
64
|
|
|
60
|
-
export { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isClient, isDev, isServer };
|
|
65
|
+
export { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isClient, isDev, isServer, matchesPattern };
|
package/package.json
CHANGED