evlog 2.12.0 → 2.14.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 +111 -0
- package/dist/adapters/axiom.d.mts +1 -1
- package/dist/adapters/better-stack.d.mts +1 -1
- package/dist/adapters/datadog.d.mts +1 -1
- package/dist/adapters/fs.d.mts +1 -1
- package/dist/adapters/hyperdx.d.mts +1 -1
- package/dist/adapters/otlp.d.mts +1 -1
- package/dist/adapters/posthog.d.mts +1 -1
- package/dist/adapters/sentry.d.mts +1 -1
- package/dist/ai/index.d.mts +106 -5
- package/dist/ai/index.d.mts.map +1 -1
- package/dist/ai/index.mjs +28 -5
- package/dist/ai/index.mjs.map +1 -1
- package/dist/audit-d9esRZOK.mjs +1440 -0
- package/dist/audit-d9esRZOK.mjs.map +1 -0
- package/dist/audit-mUutdf6A.d.mts +1130 -0
- package/dist/audit-mUutdf6A.d.mts.map +1 -0
- package/dist/better-auth/index.d.mts +220 -0
- package/dist/better-auth/index.d.mts.map +1 -0
- package/dist/better-auth/index.mjs +205 -0
- package/dist/better-auth/index.mjs.map +1 -0
- package/dist/browser.d.mts +1 -1
- package/dist/elysia/index.d.mts +2 -2
- package/dist/elysia/index.d.mts.map +1 -1
- package/dist/elysia/index.mjs +16 -4
- package/dist/elysia/index.mjs.map +1 -1
- package/dist/enrichers.d.mts +1 -1
- package/dist/{error-WRz4_F3W.d.mts → error-D1FZI2Kd.d.mts} +2 -2
- package/dist/{error-WRz4_F3W.d.mts.map → error-D1FZI2Kd.d.mts.map} +1 -1
- package/dist/error.d.mts +1 -1
- package/dist/{errors-J2kt7mZh.d.mts → errors-NIXCyk6I.d.mts} +2 -2
- package/dist/{errors-J2kt7mZh.d.mts.map → errors-NIXCyk6I.d.mts.map} +1 -1
- package/dist/express/index.d.mts +2 -2
- package/dist/express/index.d.mts.map +1 -1
- package/dist/express/index.mjs +8 -4
- package/dist/express/index.mjs.map +1 -1
- package/dist/fastify/index.d.mts +2 -2
- package/dist/fastify/index.d.mts.map +1 -1
- package/dist/fastify/index.mjs +8 -4
- package/dist/fastify/index.mjs.map +1 -1
- package/dist/fork-CTJXnpl8.mjs +72 -0
- package/dist/fork-CTJXnpl8.mjs.map +1 -0
- package/dist/headers-D74M0wsg.mjs +30 -0
- package/dist/headers-D74M0wsg.mjs.map +1 -0
- package/dist/hono/index.d.mts +2 -2
- package/dist/hono/index.mjs +2 -1
- package/dist/hono/index.mjs.map +1 -1
- package/dist/http.d.mts +1 -1
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +2 -2
- package/dist/{logger-Bm0k3Hf3.d.mts → logger-b3epPH0N.d.mts} +8 -4
- package/dist/logger-b3epPH0N.d.mts.map +1 -0
- package/dist/logger.d.mts +1 -1
- package/dist/logger.mjs +1 -1
- package/dist/{headers-ht4yS2mx.mjs → middleware-BWOJ7JI0.mjs} +9 -30
- package/dist/middleware-BWOJ7JI0.mjs.map +1 -0
- package/dist/{middleware-D_igVy93.d.mts → middleware-BYf26Lfu.d.mts} +14 -3
- package/dist/{middleware-D_igVy93.d.mts.map → middleware-BYf26Lfu.d.mts.map} +1 -1
- package/dist/nestjs/index.d.mts +2 -2
- package/dist/nestjs/index.d.mts.map +1 -1
- package/dist/nestjs/index.mjs +8 -4
- package/dist/nestjs/index.mjs.map +1 -1
- package/dist/next/client.d.mts +1 -1
- package/dist/next/index.d.mts +4 -4
- package/dist/next/index.d.mts.map +1 -1
- package/dist/next/index.mjs +15 -1
- package/dist/next/index.mjs.map +1 -1
- package/dist/next/instrumentation.d.mts +1 -1
- package/dist/next/instrumentation.mjs +1 -1
- package/dist/nitro/module.d.mts +2 -2
- package/dist/nitro/plugin.mjs +1 -1
- package/dist/nitro/v3/index.d.mts +2 -2
- package/dist/nitro/v3/module.d.mts +1 -1
- package/dist/nitro/v3/plugin.mjs +1 -1
- package/dist/nitro/v3/useLogger.d.mts +1 -1
- package/dist/{nitro-BeRXZcBd.d.mts → nitro-DenB86W6.d.mts} +2 -2
- package/dist/{nitro-BeRXZcBd.d.mts.map → nitro-DenB86W6.d.mts.map} +1 -1
- package/dist/nuxt/module.d.mts +1 -1
- package/dist/nuxt/module.mjs +1 -1
- package/dist/{parseError-DhXS_vzM.d.mts → parseError-BR9pocvY.d.mts} +2 -2
- package/dist/parseError-BR9pocvY.d.mts.map +1 -0
- package/dist/react-router/index.d.mts +2 -2
- package/dist/react-router/index.d.mts.map +1 -1
- package/dist/react-router/index.mjs +8 -4
- package/dist/react-router/index.mjs.map +1 -1
- package/dist/runtime/client/log.d.mts +1 -1
- package/dist/runtime/server/routes/_evlog/ingest.post.mjs +1 -1
- package/dist/runtime/server/useLogger.d.mts +1 -1
- package/dist/runtime/utils/parseError.d.mts +2 -2
- package/dist/{storage-DpLJYMoc.mjs → storage-CFGTn37X.mjs} +1 -1
- package/dist/{storage-DpLJYMoc.mjs.map → storage-CFGTn37X.mjs.map} +1 -1
- package/dist/sveltekit/index.d.mts +2 -2
- package/dist/sveltekit/index.d.mts.map +1 -1
- package/dist/sveltekit/index.mjs +8 -4
- package/dist/sveltekit/index.mjs.map +1 -1
- package/dist/toolkit.d.mts +41 -4
- package/dist/toolkit.d.mts.map +1 -1
- package/dist/toolkit.mjs +5 -3
- package/dist/types.d.mts +2 -2
- package/dist/{useLogger-Dcj1Nrsa.d.mts → useLogger-C56tDPwf.d.mts} +2 -2
- package/dist/{useLogger-Dcj1Nrsa.d.mts.map → useLogger-C56tDPwf.d.mts.map} +1 -1
- package/dist/{utils-Bnc95-VC.d.mts → utils-DzGCLRFe.d.mts} +2 -2
- package/dist/{utils-Bnc95-VC.d.mts.map → utils-DzGCLRFe.d.mts.map} +1 -1
- package/dist/utils.d.mts +1 -1
- package/dist/vite/index.d.mts +1 -1
- package/dist/workers.d.mts +1 -1
- package/dist/workers.mjs +1 -1
- package/package.json +16 -3
- package/dist/headers-ht4yS2mx.mjs.map +0 -1
- package/dist/logger-Bm0k3Hf3.d.mts.map +0 -1
- package/dist/logger-DY0X5oQd.mjs +0 -704
- package/dist/logger-DY0X5oQd.mjs.map +0 -1
- package/dist/parseError-DhXS_vzM.d.mts.map +0 -1
- package/dist/types-D5OwxZCw.d.mts +0 -587
- package/dist/types-D5OwxZCw.d.mts.map +0 -1
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
declare module 'nitropack/types' {
|
|
3
|
+
interface NitroRuntimeHooks {
|
|
4
|
+
/**
|
|
5
|
+
* Tail sampling hook - called before emitting a log.
|
|
6
|
+
* Set `ctx.shouldKeep = true` to force-keep the log regardless of head sampling.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
|
|
11
|
+
* if (ctx.context.user?.premium) {
|
|
12
|
+
* ctx.shouldKeep = true
|
|
13
|
+
* }
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
'evlog:emit:keep': (ctx: TailSamplingContext) => void | Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Enrichment hook - called after emit, before drain.
|
|
20
|
+
* Use this to enrich the event with derived context (e.g. geo, user agent).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* nitroApp.hooks.hook('evlog:enrich', (ctx) => {
|
|
25
|
+
* ctx.event.deploymentId = process.env.DEPLOYMENT_ID
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
'evlog:enrich': (ctx: EnrichContext) => void | Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Drain hook - called after emitting a log (fire-and-forget).
|
|
32
|
+
* Use this to send logs to external services like Axiom, Loki, or custom endpoints.
|
|
33
|
+
* Errors are logged but never block the request.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* nitroApp.hooks.hook('evlog:drain', async (ctx) => {
|
|
38
|
+
* await fetch('https://api.axiom.co/v1/datasets/logs/ingest', {
|
|
39
|
+
* method: 'POST',
|
|
40
|
+
* headers: { Authorization: `Bearer ${process.env.AXIOM_TOKEN}` },
|
|
41
|
+
* body: JSON.stringify([ctx.event])
|
|
42
|
+
* })
|
|
43
|
+
* })
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
'evlog:drain': (ctx: DrainContext) => void | Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
declare module 'nitro/types' {
|
|
50
|
+
interface NitroRuntimeHooks {
|
|
51
|
+
'evlog:emit:keep': (ctx: TailSamplingContext) => void | Promise<void>;
|
|
52
|
+
'evlog:enrich': (ctx: EnrichContext) => void | Promise<void>;
|
|
53
|
+
'evlog:drain': (ctx: DrainContext) => void | Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Transport configuration for sending client logs to the server
|
|
58
|
+
*/
|
|
59
|
+
interface TransportConfig {
|
|
60
|
+
/**
|
|
61
|
+
* Enable sending logs to the server API
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* API endpoint for log ingestion
|
|
67
|
+
* @default '/api/_evlog/ingest'
|
|
68
|
+
*/
|
|
69
|
+
endpoint?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Fetch credentials mode
|
|
72
|
+
* @default 'same-origin'
|
|
73
|
+
*/
|
|
74
|
+
credentials?: RequestCredentials;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Payload sent from client to server for log ingestion
|
|
78
|
+
*/
|
|
79
|
+
interface IngestPayload {
|
|
80
|
+
timestamp: string;
|
|
81
|
+
level: 'info' | 'error' | 'warn' | 'debug';
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Auto-redaction configuration for PII protection.
|
|
86
|
+
* Scrubs sensitive data from wide events before console output and draining.
|
|
87
|
+
*
|
|
88
|
+
* Built-in patterns are included by default. Opt out with `builtins: false`
|
|
89
|
+
* or select specific ones with `builtins: ['email', 'creditCard']`.
|
|
90
|
+
*/
|
|
91
|
+
interface RedactConfig {
|
|
92
|
+
/** Dot-notation paths to redact (e.g., 'user.email', 'headers.x-forwarded-for') */
|
|
93
|
+
paths?: string[];
|
|
94
|
+
/** Additional regex patterns to match and replace string values anywhere in the event */
|
|
95
|
+
patterns?: RegExp[];
|
|
96
|
+
/**
|
|
97
|
+
* Control built-in PII patterns.
|
|
98
|
+
* - `undefined` / omitted → all built-ins enabled (default)
|
|
99
|
+
* - `false` → no built-ins, only custom `paths`/`patterns`
|
|
100
|
+
* - `['email', 'creditCard', ...]` → only the listed built-ins
|
|
101
|
+
*
|
|
102
|
+
* Available: `'creditCard'`, `'email'`, `'ipv4'`, `'phone'`, `'jwt'`, `'bearer'`, `'iban'`
|
|
103
|
+
*/
|
|
104
|
+
builtins?: false | Array<'creditCard' | 'email' | 'ipv4' | 'phone' | 'jwt' | 'bearer' | 'iban'>;
|
|
105
|
+
/**
|
|
106
|
+
* Replacement string used for path-based and custom pattern redaction.
|
|
107
|
+
* Built-in patterns use smart partial masking instead (e.g. `****1111` for credit cards).
|
|
108
|
+
* @default '[REDACTED]'
|
|
109
|
+
*/
|
|
110
|
+
replacement?: string;
|
|
111
|
+
/** @internal Resolved masker functions from built-in patterns. Not user-facing. */
|
|
112
|
+
_maskers?: Array<[RegExp, (match: string) => string]>;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Sampling rates per log level (0-100 percentage)
|
|
116
|
+
*/
|
|
117
|
+
interface SamplingRates {
|
|
118
|
+
/** Percentage of info logs to keep (0-100). Default: 100 */
|
|
119
|
+
info?: number;
|
|
120
|
+
/** Percentage of warn logs to keep (0-100). Default: 100 */
|
|
121
|
+
warn?: number;
|
|
122
|
+
/** Percentage of debug logs to keep (0-100). Default: 100 */
|
|
123
|
+
debug?: number;
|
|
124
|
+
/** Percentage of error logs to keep (0-100). Default: 100 */
|
|
125
|
+
error?: number;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Tail sampling condition for forcing log retention based on request outcome.
|
|
129
|
+
* All conditions use >= comparison (e.g., status: 400 means status >= 400).
|
|
130
|
+
*/
|
|
131
|
+
interface TailSamplingCondition {
|
|
132
|
+
/** Keep if HTTP status >= this value (e.g., 400 for all errors) */
|
|
133
|
+
status?: number;
|
|
134
|
+
/** Keep if request duration >= this value in milliseconds */
|
|
135
|
+
duration?: number;
|
|
136
|
+
/** Keep if path matches this glob pattern (e.g., '/api/critical/**') */
|
|
137
|
+
path?: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Context passed to tail sampling evaluation and hooks.
|
|
141
|
+
* Contains request outcome information for sampling decisions.
|
|
142
|
+
*/
|
|
143
|
+
interface TailSamplingContext {
|
|
144
|
+
/** HTTP response status code */
|
|
145
|
+
status?: number;
|
|
146
|
+
/** Request duration in milliseconds (raw number) */
|
|
147
|
+
duration?: number;
|
|
148
|
+
/** Request path */
|
|
149
|
+
path?: string;
|
|
150
|
+
/** HTTP method */
|
|
151
|
+
method?: string;
|
|
152
|
+
/** Full accumulated context from the request logger */
|
|
153
|
+
context: Record<string, unknown>;
|
|
154
|
+
/**
|
|
155
|
+
* Set to true in evlog:emit:keep hook to force keep this log.
|
|
156
|
+
* Multiple hooks can set this - if any sets it to true, the log is kept.
|
|
157
|
+
*/
|
|
158
|
+
shouldKeep?: boolean;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Context passed to the evlog:enrich hook.
|
|
162
|
+
* Called after emit, before drain.
|
|
163
|
+
*/
|
|
164
|
+
interface EnrichContext {
|
|
165
|
+
/** The emitted wide event (mutable). */
|
|
166
|
+
event: WideEvent;
|
|
167
|
+
/** Request metadata (if available) */
|
|
168
|
+
request?: {
|
|
169
|
+
method?: string;
|
|
170
|
+
path: string;
|
|
171
|
+
requestId?: string;
|
|
172
|
+
};
|
|
173
|
+
/** Safe HTTP request headers (sensitive headers filtered out) */
|
|
174
|
+
headers?: Record<string, string>;
|
|
175
|
+
/** Optional response metadata */
|
|
176
|
+
response?: {
|
|
177
|
+
status?: number;
|
|
178
|
+
headers?: Record<string, string>;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Context passed to the evlog:drain hook.
|
|
183
|
+
* Contains the complete wide event and request metadata for external transport.
|
|
184
|
+
*/
|
|
185
|
+
interface DrainContext {
|
|
186
|
+
/** The complete wide event to drain */
|
|
187
|
+
event: WideEvent;
|
|
188
|
+
/** Request metadata (if available) */
|
|
189
|
+
request?: {
|
|
190
|
+
method?: string;
|
|
191
|
+
path?: string;
|
|
192
|
+
requestId?: string;
|
|
193
|
+
};
|
|
194
|
+
/** HTTP headers from the original request (useful for correlation with external services) */
|
|
195
|
+
headers?: Record<string, string>;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Sampling configuration for filtering logs
|
|
199
|
+
*/
|
|
200
|
+
interface SamplingConfig {
|
|
201
|
+
/**
|
|
202
|
+
* Sampling rates per log level (head sampling).
|
|
203
|
+
* Values are percentages from 0 to 100.
|
|
204
|
+
* Default: 100 for all levels (log everything).
|
|
205
|
+
* Error defaults to 100 even if not specified.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* sampling: {
|
|
210
|
+
* rates: {
|
|
211
|
+
* info: 10, // Keep 10% of info logs
|
|
212
|
+
* warn: 50, // Keep 50% of warning logs
|
|
213
|
+
* debug: 5, // Keep 5% of debug logs
|
|
214
|
+
* error: 100, // Always keep errors (default)
|
|
215
|
+
* }
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
rates?: SamplingRates;
|
|
220
|
+
/**
|
|
221
|
+
* Tail sampling conditions for forcing log retention (OR logic).
|
|
222
|
+
* If ANY condition matches, the log is kept regardless of head sampling.
|
|
223
|
+
* Use the `evlog:emit:keep` Nitro hook for custom conditions.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* sampling: {
|
|
228
|
+
* rates: { info: 10 }, // Head sampling: keep 10% of info logs
|
|
229
|
+
* keep: [
|
|
230
|
+
* { status: 400 }, // Always keep if status >= 400
|
|
231
|
+
* { duration: 1000 }, // Always keep if duration >= 1000ms
|
|
232
|
+
* { path: '/api/critical/**' }, // Always keep critical paths
|
|
233
|
+
* ]
|
|
234
|
+
* }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
keep?: TailSamplingCondition[];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Route-based service configuration
|
|
241
|
+
*/
|
|
242
|
+
interface RouteConfig {
|
|
243
|
+
/** Service name to use for routes matching this pattern */
|
|
244
|
+
service: string;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Environment context automatically included in every log event
|
|
248
|
+
*/
|
|
249
|
+
interface EnvironmentContext {
|
|
250
|
+
/** Service name (auto-detected from package.json or configurable) */
|
|
251
|
+
service: string;
|
|
252
|
+
/** Environment: 'development', 'production', 'test', etc. */
|
|
253
|
+
environment: 'development' | 'production' | 'test' | string;
|
|
254
|
+
/** Application version (auto-detected from package.json) */
|
|
255
|
+
version?: string;
|
|
256
|
+
/** Git commit hash (auto-detected from CI/CD env vars) */
|
|
257
|
+
commitHash?: string;
|
|
258
|
+
/** Deployment region (auto-detected from cloud provider env vars) */
|
|
259
|
+
region?: string;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Logger configuration options
|
|
263
|
+
*/
|
|
264
|
+
interface LoggerConfig {
|
|
265
|
+
/**
|
|
266
|
+
* Enable or disable all logging globally.
|
|
267
|
+
* When false, all emits, tagged logs, and request logger operations become no-ops.
|
|
268
|
+
* @default true
|
|
269
|
+
*/
|
|
270
|
+
enabled?: boolean;
|
|
271
|
+
/** Environment context overrides */
|
|
272
|
+
env?: Partial<EnvironmentContext>;
|
|
273
|
+
/** Enable pretty printing (auto-detected: true in dev, false in prod) */
|
|
274
|
+
pretty?: boolean;
|
|
275
|
+
/** Sampling configuration for filtering logs */
|
|
276
|
+
sampling?: SamplingConfig;
|
|
277
|
+
/**
|
|
278
|
+
* Minimum severity for the global `log` API (tagged and object form).
|
|
279
|
+
* Does not apply to `createLogger().emit()` / request wide events (use `sampling` for volume).
|
|
280
|
+
* Order: debug < info < warn < error.
|
|
281
|
+
* @default 'debug' (all levels)
|
|
282
|
+
*/
|
|
283
|
+
minLevel?: LogLevel;
|
|
284
|
+
/**
|
|
285
|
+
* When pretty is disabled, emit JSON strings (default) or raw objects.
|
|
286
|
+
* Set to false for environments like Cloudflare Workers that expect objects.
|
|
287
|
+
* @default true
|
|
288
|
+
*/
|
|
289
|
+
stringify?: boolean;
|
|
290
|
+
/**
|
|
291
|
+
* Suppress built-in console output.
|
|
292
|
+
* When true, events are still built, sampled, and passed to drains,
|
|
293
|
+
* but nothing is written to console. Use when drains own the output
|
|
294
|
+
* channel (e.g., stdout-based platforms like GCP Cloud Run, AWS Lambda).
|
|
295
|
+
* @default false
|
|
296
|
+
*/
|
|
297
|
+
silent?: boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Auto-redaction configuration for PII protection.
|
|
300
|
+
* Scrubs sensitive data from wide events before console output and before any drain.
|
|
301
|
+
*
|
|
302
|
+
* - `true` → enable with all built-in patterns (email, credit card, IPv4, phone, JWT, Bearer, IBAN)
|
|
303
|
+
* - `{ paths, patterns, builtins }` → fine-grained control
|
|
304
|
+
* - `false` → explicitly disable redaction
|
|
305
|
+
*
|
|
306
|
+
* @default true in production, false in development
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* // Disable in production
|
|
311
|
+
* initLogger({ redact: false })
|
|
312
|
+
*
|
|
313
|
+
* // With custom paths on top of built-ins
|
|
314
|
+
* initLogger({
|
|
315
|
+
* redact: {
|
|
316
|
+
* paths: ['user.password', 'headers.authorization'],
|
|
317
|
+
* },
|
|
318
|
+
* })
|
|
319
|
+
*
|
|
320
|
+
* // Only specific built-ins + custom patterns
|
|
321
|
+
* initLogger({
|
|
322
|
+
* redact: {
|
|
323
|
+
* builtins: ['email', 'creditCard'],
|
|
324
|
+
* patterns: [/SECRET_\w+/g],
|
|
325
|
+
* },
|
|
326
|
+
* })
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
redact?: boolean | RedactConfig;
|
|
330
|
+
/**
|
|
331
|
+
* Drain callback called with every emitted event (fire-and-forget).
|
|
332
|
+
* Use this to send logs to external services outside of Nitro.
|
|
333
|
+
* Compatible with drain adapters (`createAxiomDrain()`) and pipeline-wrapped drains.
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```ts
|
|
337
|
+
* import { initLogger, log } from 'evlog'
|
|
338
|
+
* import { createAxiomDrain } from 'evlog/axiom'
|
|
339
|
+
*
|
|
340
|
+
* initLogger({
|
|
341
|
+
* drain: createAxiomDrain({ dataset: 'logs', token: '...' }),
|
|
342
|
+
* })
|
|
343
|
+
*
|
|
344
|
+
* log.info({ action: 'user_login' }) // automatically drained
|
|
345
|
+
* ```
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* // With pipeline for batching and retry
|
|
350
|
+
* import { createDrainPipeline } from 'evlog/pipeline'
|
|
351
|
+
*
|
|
352
|
+
* const pipeline = createDrainPipeline({ batch: { size: 25 } })
|
|
353
|
+
* const drain = pipeline(createAxiomDrain({ dataset: 'logs', token: '...' }))
|
|
354
|
+
*
|
|
355
|
+
* initLogger({ drain })
|
|
356
|
+
*
|
|
357
|
+
* // Flush on shutdown
|
|
358
|
+
* process.on('beforeExit', () => drain.flush())
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
drain?: (ctx: DrainContext) => void | Promise<void>;
|
|
362
|
+
/** @internal Suppress the "silent without drain" warning (used by hook-based frameworks like Nitro/Nuxt) */
|
|
363
|
+
_suppressDrainWarning?: boolean;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Audit actor — who initiated the action.
|
|
367
|
+
*
|
|
368
|
+
* `type` covers the most common actor families. `id` is required and should be
|
|
369
|
+
* a stable identifier (user id, service name, API key id, agent id). `model`,
|
|
370
|
+
* `tools`, `reason`, and `promptId` are filled when `type === 'agent'` and
|
|
371
|
+
* mirror the AI SDK fields already used by `evlog/ai`.
|
|
372
|
+
*/
|
|
373
|
+
interface AuditActor {
|
|
374
|
+
type: 'user' | 'system' | 'api' | 'agent';
|
|
375
|
+
id: string;
|
|
376
|
+
displayName?: string;
|
|
377
|
+
email?: string;
|
|
378
|
+
model?: string;
|
|
379
|
+
tools?: string[];
|
|
380
|
+
reason?: string;
|
|
381
|
+
promptId?: string;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Audit target — the resource the action was performed on.
|
|
385
|
+
*
|
|
386
|
+
* `type` is a free-form string (e.g. `'invoice'`, `'user'`, `'subscription'`)
|
|
387
|
+
* narrowed by {@link defineAuditAction}. Additional fields are allowed for
|
|
388
|
+
* resource-specific metadata (e.g. `tenantId`, `path`, `previousOwnerId`).
|
|
389
|
+
*/
|
|
390
|
+
interface AuditTarget {
|
|
391
|
+
type: string;
|
|
392
|
+
id: string;
|
|
393
|
+
[key: string]: unknown;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Reserved audit fields on the wide event.
|
|
397
|
+
*
|
|
398
|
+
* Set via `log.audit({ ... })`, `log.set({ audit: { ... } })`, or the
|
|
399
|
+
* standalone `audit({ ... })` helper. Downstream filters on `audit IS NOT NULL`.
|
|
400
|
+
*
|
|
401
|
+
* - `outcome` — `'success' | 'failure' | 'denied'`. `'denied'` records an
|
|
402
|
+
* AuthZ-denied action (often forgotten but exactly what auditors want).
|
|
403
|
+
* - `changes.before/after` — the diff for mutating actions. Use
|
|
404
|
+
* {@link auditDiff} to produce a redact-aware compact JSON Patch.
|
|
405
|
+
* - `causationId` / `correlationId` — chain related actions (admin action →
|
|
406
|
+
* system reactions). Set by callers, propagated by `auditEnricher` when
|
|
407
|
+
* available on the request.
|
|
408
|
+
* - `signature` / `prevHash` — populated by the {@link signed} drain wrapper.
|
|
409
|
+
* Never set by application code.
|
|
410
|
+
* - `idempotencyKey` — derived deterministically by `log.audit()` so retries
|
|
411
|
+
* across drains are safe.
|
|
412
|
+
* - `context` — request/runtime context auto-populated by {@link auditEnricher}
|
|
413
|
+
* (`requestId`, `traceId`, `ip`, `userAgent`, `tenantId`).
|
|
414
|
+
*/
|
|
415
|
+
interface AuditFields {
|
|
416
|
+
/** Action name. Convention: `'<resource>.<verb>'`, e.g. `'invoice.refund'`. */
|
|
417
|
+
action: string;
|
|
418
|
+
actor: AuditActor;
|
|
419
|
+
target?: AuditTarget;
|
|
420
|
+
outcome: 'success' | 'failure' | 'denied';
|
|
421
|
+
/** Human-readable explanation, especially required for `outcome: 'denied'`. */
|
|
422
|
+
reason?: string;
|
|
423
|
+
/** Before/after snapshots for mutating actions. */
|
|
424
|
+
changes?: {
|
|
425
|
+
before?: unknown;
|
|
426
|
+
after?: unknown;
|
|
427
|
+
};
|
|
428
|
+
/** ID of the action that caused this one. */
|
|
429
|
+
causationId?: string;
|
|
430
|
+
/** ID shared by every action in the same logical operation. */
|
|
431
|
+
correlationId?: string;
|
|
432
|
+
/** Schema version of the audit envelope. Defaults to `1` when omitted by the caller. */
|
|
433
|
+
version?: number;
|
|
434
|
+
/** Set by `log.audit()` as a stable hash for safe retries across drains. */
|
|
435
|
+
idempotencyKey?: string;
|
|
436
|
+
/** Request/runtime context — populated by `auditEnricher`. */
|
|
437
|
+
context?: {
|
|
438
|
+
requestId?: string;
|
|
439
|
+
traceId?: string;
|
|
440
|
+
ip?: string;
|
|
441
|
+
userAgent?: string;
|
|
442
|
+
tenantId?: string;
|
|
443
|
+
[key: string]: unknown;
|
|
444
|
+
};
|
|
445
|
+
/** HMAC signature of the event when wrapped with `signed({ strategy: 'hmac' })`. */
|
|
446
|
+
signature?: string;
|
|
447
|
+
/** Previous event hash when wrapped with `signed({ strategy: 'hash-chain' })`. */
|
|
448
|
+
prevHash?: string;
|
|
449
|
+
/** Hash of the current event when wrapped with `signed({ strategy: 'hash-chain' })`. */
|
|
450
|
+
hash?: string;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Base structure for all wide events.
|
|
454
|
+
*
|
|
455
|
+
* Augment via `declare module 'evlog'` to add app-specific top-level fields.
|
|
456
|
+
* `audit` is reserved for {@link AuditFields}.
|
|
457
|
+
*/
|
|
458
|
+
interface BaseWideEvent {
|
|
459
|
+
timestamp: string;
|
|
460
|
+
level: 'info' | 'error' | 'warn' | 'debug';
|
|
461
|
+
service: string;
|
|
462
|
+
environment: string;
|
|
463
|
+
version?: string;
|
|
464
|
+
commitHash?: string;
|
|
465
|
+
region?: string;
|
|
466
|
+
audit?: AuditFields;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Wide event with arbitrary additional fields
|
|
470
|
+
*/
|
|
471
|
+
type WideEvent = BaseWideEvent & Record<string, unknown>;
|
|
472
|
+
/**
|
|
473
|
+
* Recursively makes all properties optional.
|
|
474
|
+
* Arrays are kept as-is (not deeply partial).
|
|
475
|
+
*/
|
|
476
|
+
type DeepPartial<T> = T extends Array<unknown> ? T : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
|
|
477
|
+
/**
|
|
478
|
+
* Fields set internally by the evlog plugin (status, service, etc.).
|
|
479
|
+
* These are always accepted by `set()` regardless of the user-defined field type.
|
|
480
|
+
*/
|
|
481
|
+
interface InternalFields {
|
|
482
|
+
status?: number;
|
|
483
|
+
service?: string;
|
|
484
|
+
requestLogs?: RequestLogEntry[];
|
|
485
|
+
/** Label for a forked background wide event (child operation name). */
|
|
486
|
+
operation?: string;
|
|
487
|
+
/** Parent request's `requestId` when this event was produced by `log.fork()`. */
|
|
488
|
+
_parentRequestId?: string;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Request-scoped log entry captured during a request lifecycle.
|
|
492
|
+
*/
|
|
493
|
+
interface RequestLogEntry {
|
|
494
|
+
level: 'info' | 'warn';
|
|
495
|
+
message: string;
|
|
496
|
+
timestamp: string;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Resolved context type for logger methods.
|
|
500
|
+
* User fields are deeply partial (matching deep merge behavior) with internal
|
|
501
|
+
* field keys omitted to avoid intersection conflicts, then internal fields
|
|
502
|
+
* are added back with their canonical types.
|
|
503
|
+
*/
|
|
504
|
+
type FieldContext<T extends object = Record<string, unknown>> = DeepPartial<Omit<T, keyof InternalFields>> & InternalFields;
|
|
505
|
+
/**
|
|
506
|
+
* Request-scoped logger for building wide events
|
|
507
|
+
*
|
|
508
|
+
* After {@link RequestLogger.emit} runs (including when head sampling drops the event),
|
|
509
|
+
* the logger is **sealed**: further `set`, `error`, `info`, and `warn` calls log a
|
|
510
|
+
* console warning and do not mutate the wide event. A second `emit` is ignored with
|
|
511
|
+
* a warning. Use {@link RequestLogger.fork} on supported integrations for intentional
|
|
512
|
+
* background work that needs its own wide event.
|
|
513
|
+
*
|
|
514
|
+
* `fork` is only present on request loggers from integrations that attach it (not on
|
|
515
|
+
* standalone `createLogger()` instances).
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```ts
|
|
519
|
+
* const logger = useLogger(event)
|
|
520
|
+
* logger.set({ user: { id: '123' } })
|
|
521
|
+
* logger.set({ cart: { items: 3 } })
|
|
522
|
+
* // emit() is called automatically by the plugin
|
|
523
|
+
* ```
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```ts
|
|
527
|
+
* // With typed fields for compile-time safety
|
|
528
|
+
* interface MyFields {
|
|
529
|
+
* user: { id: string; plan: string }
|
|
530
|
+
* action: string
|
|
531
|
+
* }
|
|
532
|
+
* const logger = useLogger<MyFields>(event)
|
|
533
|
+
* logger.set({ user: { id: '123', plan: 'pro' } }) // OK
|
|
534
|
+
* logger.set({ user: { id: '123' } }) // OK (deep partial)
|
|
535
|
+
* logger.set({ action: 'checkout' }) // OK
|
|
536
|
+
* logger.set({ status: 200 }) // OK (internal field)
|
|
537
|
+
* logger.set({ account: '...' }) // TS error
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
interface RequestLogger<T extends object = Record<string, unknown>> {
|
|
541
|
+
/**
|
|
542
|
+
* Add context to the wide event. Plain objects are merged recursively.
|
|
543
|
+
* When both the existing and incoming values for a key are arrays, elements are
|
|
544
|
+
* concatenated (existing order preserved, new elements appended). Otherwise the
|
|
545
|
+
* new value replaces the old one (including when only one side is an array).
|
|
546
|
+
*
|
|
547
|
+
* No-ops with a console warning after the wide event has been emitted.
|
|
548
|
+
*/
|
|
549
|
+
set: (context: FieldContext<T>) => void;
|
|
550
|
+
/**
|
|
551
|
+
* Log an error and capture its details.
|
|
552
|
+
*
|
|
553
|
+
* No-ops with a console warning after the wide event has been emitted.
|
|
554
|
+
*/
|
|
555
|
+
error: (error: Error | string, context?: FieldContext<T>) => void;
|
|
556
|
+
/**
|
|
557
|
+
* Capture an informational message inside the request wide event.
|
|
558
|
+
*
|
|
559
|
+
* No-ops with a console warning after the wide event has been emitted.
|
|
560
|
+
*/
|
|
561
|
+
info: (message: string, context?: FieldContext<T>) => void;
|
|
562
|
+
/**
|
|
563
|
+
* Capture a warning message inside the request wide event.
|
|
564
|
+
*
|
|
565
|
+
* No-ops with a console warning after the wide event has been emitted.
|
|
566
|
+
*/
|
|
567
|
+
warn: (message: string, context?: FieldContext<T>) => void;
|
|
568
|
+
/**
|
|
569
|
+
* Emit the final wide event with all accumulated context.
|
|
570
|
+
* Returns the emitted WideEvent, or null if the log was sampled out.
|
|
571
|
+
*
|
|
572
|
+
* Seals the logger: after this returns (including when the return value is `null`
|
|
573
|
+
* due to sampling), further mutations are ignored with warnings.
|
|
574
|
+
*/
|
|
575
|
+
emit: (overrides?: FieldContext<T> & {
|
|
576
|
+
_forceKeep?: boolean;
|
|
577
|
+
}) => WideEvent | null;
|
|
578
|
+
/**
|
|
579
|
+
* Get the current accumulated context
|
|
580
|
+
*/
|
|
581
|
+
getContext: () => FieldContext<T> & Record<string, unknown>;
|
|
582
|
+
/**
|
|
583
|
+
* Run async (or sync) work in a **child** request logger scope so `useLogger()`
|
|
584
|
+
* resolves to the child logger while `fn` runs. The child emits its own wide event
|
|
585
|
+
* when `fn` settles, with `operation` and `_parentRequestId` set for correlation.
|
|
586
|
+
* Only available on integrations that attach this method (Express, Fastify, NestJS,
|
|
587
|
+
* SvelteKit, React Router, Next.js `withEvlog`, Elysia — see docs).
|
|
588
|
+
*
|
|
589
|
+
* @param label - Value stored as `operation` on the child wide event.
|
|
590
|
+
* @param fn - Function to run; may return a Promise. Errors are captured on the
|
|
591
|
+
* child logger and emitted.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```ts
|
|
595
|
+
* log.fork('process_order', async () => {
|
|
596
|
+
* const log = useLogger()
|
|
597
|
+
* log.set({ step: 'charged' })
|
|
598
|
+
* })
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
fork?: (label: string, fn: () => void | Promise<void>) => void;
|
|
602
|
+
/**
|
|
603
|
+
* Record an audit event on this wide event. Strictly equivalent to
|
|
604
|
+
* `log.set({ audit: { ... } })` plus tail-sample force-keep.
|
|
605
|
+
*
|
|
606
|
+
* Use `log.audit.deny(reason, fields)` for AuthZ-denied actions — most teams
|
|
607
|
+
* forget to log denials but they are exactly what auditors and security teams
|
|
608
|
+
* ask for.
|
|
609
|
+
*
|
|
610
|
+
* Available on every logger returned by `createLogger()` / `createRequestLogger()`
|
|
611
|
+
* and on framework loggers exposed via `useLogger()` / `c.get('log')` etc.
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```ts
|
|
615
|
+
* log.audit({
|
|
616
|
+
* action: 'invoice.refund',
|
|
617
|
+
* actor: { type: 'user', id: user.id, email: user.email },
|
|
618
|
+
* target: { type: 'invoice', id: 'inv_889' },
|
|
619
|
+
* outcome: 'success',
|
|
620
|
+
* reason: 'Customer requested refund',
|
|
621
|
+
* })
|
|
622
|
+
* ```
|
|
623
|
+
*/
|
|
624
|
+
audit?: AuditLoggerMethod;
|
|
625
|
+
}
|
|
626
|
+
/** @internal Forward-declaration to avoid a circular import with `audit.ts`. */
|
|
627
|
+
interface AuditLoggerMethod {
|
|
628
|
+
(input: AuditInput): void;
|
|
629
|
+
deny: (reason: string, input: Omit<AuditInput, 'outcome' | 'reason'>) => void;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Log level type
|
|
633
|
+
*/
|
|
634
|
+
type LogLevel = 'info' | 'error' | 'warn' | 'debug';
|
|
635
|
+
/**
|
|
636
|
+
* Simple logging API - as easy as console.log
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* ```ts
|
|
640
|
+
* log.info('auth', 'User logged in')
|
|
641
|
+
* log.error({ action: 'payment', error: 'failed' })
|
|
642
|
+
* ```
|
|
643
|
+
*/
|
|
644
|
+
interface Log {
|
|
645
|
+
/**
|
|
646
|
+
* Log an info message or wide event
|
|
647
|
+
* @example log.info('auth', 'User logged in')
|
|
648
|
+
* @example log.info({ action: 'login', userId: '123' })
|
|
649
|
+
*/
|
|
650
|
+
info(tag: string, message: string): void;
|
|
651
|
+
info(event: Record<string, unknown>): void;
|
|
652
|
+
/**
|
|
653
|
+
* Log an error message or wide event
|
|
654
|
+
* @example log.error('payment', 'Payment failed')
|
|
655
|
+
* @example log.error({ action: 'payment', error: 'declined' })
|
|
656
|
+
*/
|
|
657
|
+
error(tag: string, message: string): void;
|
|
658
|
+
error(event: Record<string, unknown>): void;
|
|
659
|
+
/**
|
|
660
|
+
* Log a warning message or wide event
|
|
661
|
+
* @example log.warn('api', 'Rate limit approaching')
|
|
662
|
+
* @example log.warn({ action: 'api', remaining: 10 })
|
|
663
|
+
*/
|
|
664
|
+
warn(tag: string, message: string): void;
|
|
665
|
+
warn(event: Record<string, unknown>): void;
|
|
666
|
+
/**
|
|
667
|
+
* Log a debug message or wide event
|
|
668
|
+
* @example log.debug('cache', 'Cache miss')
|
|
669
|
+
* @example log.debug({ action: 'cache', key: 'user_123' })
|
|
670
|
+
*/
|
|
671
|
+
debug(tag: string, message: string): void;
|
|
672
|
+
debug(event: Record<string, unknown>): void;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Error options for creating structured errors
|
|
676
|
+
*/
|
|
677
|
+
interface ErrorOptions {
|
|
678
|
+
/** What actually happened */
|
|
679
|
+
message: string;
|
|
680
|
+
/** HTTP status code (default: 500) */
|
|
681
|
+
status?: number;
|
|
682
|
+
/** Why this error occurred */
|
|
683
|
+
why?: string;
|
|
684
|
+
/** How to fix this issue */
|
|
685
|
+
fix?: string;
|
|
686
|
+
/** Link to documentation or more information */
|
|
687
|
+
link?: string;
|
|
688
|
+
/** The original error that caused this */
|
|
689
|
+
cause?: Error;
|
|
690
|
+
/**
|
|
691
|
+
* Backend-only diagnostic context (auditing, support, debugging).
|
|
692
|
+
* Never included in HTTP responses or `EvlogError#toJSON`; included in wide events when the error is passed to `log.error()`.
|
|
693
|
+
*/
|
|
694
|
+
internal?: Record<string, unknown>;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Options for creating a request logger
|
|
698
|
+
*/
|
|
699
|
+
interface RequestLoggerOptions {
|
|
700
|
+
method?: string;
|
|
701
|
+
path?: string;
|
|
702
|
+
requestId?: string;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* H3 event context with evlog logger attached
|
|
706
|
+
*/
|
|
707
|
+
interface H3EventContext {
|
|
708
|
+
log?: RequestLogger;
|
|
709
|
+
requestId?: string;
|
|
710
|
+
status?: number;
|
|
711
|
+
/** Internal: start time for duration calculation in tail sampling */
|
|
712
|
+
_evlogStartTime?: number;
|
|
713
|
+
/** Internal: flag to prevent double emission on errors */
|
|
714
|
+
_evlogEmitted?: boolean;
|
|
715
|
+
/** Internal: whether the route matched shouldLog filtering (emit-time guard) */
|
|
716
|
+
_evlogShouldEmit?: boolean;
|
|
717
|
+
[key: string]: unknown;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Server event type for Nitro/h3 handlers
|
|
721
|
+
*/
|
|
722
|
+
interface ServerEvent {
|
|
723
|
+
method: string;
|
|
724
|
+
path: string;
|
|
725
|
+
context: H3EventContext & {
|
|
726
|
+
/** Cloudflare Workers context (available when deployed to CF Workers) */cloudflare?: {
|
|
727
|
+
context: {
|
|
728
|
+
waitUntil: (promise: Promise<unknown>) => void;
|
|
729
|
+
};
|
|
730
|
+
}; /** Vercel Edge context (available when deployed to Vercel Edge) */
|
|
731
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
732
|
+
};
|
|
733
|
+
node?: {
|
|
734
|
+
res?: {
|
|
735
|
+
statusCode?: number;
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
response?: Response;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Parsed evlog error with all fields at the top level
|
|
742
|
+
*/
|
|
743
|
+
interface ParsedError {
|
|
744
|
+
message: string;
|
|
745
|
+
status: number;
|
|
746
|
+
why?: string;
|
|
747
|
+
fix?: string;
|
|
748
|
+
link?: string;
|
|
749
|
+
raw: unknown;
|
|
750
|
+
}
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region src/audit.d.ts
|
|
753
|
+
/**
|
|
754
|
+
* Current version of the audit envelope. Bumped when `AuditFields` evolves
|
|
755
|
+
* in a backward-incompatible way so downstream pipelines can branch on it.
|
|
756
|
+
*/
|
|
757
|
+
declare const AUDIT_SCHEMA_VERSION = 1;
|
|
758
|
+
/**
|
|
759
|
+
* Input accepted by `log.audit()`, `audit()`, and `withAudit()`.
|
|
760
|
+
*
|
|
761
|
+
* `outcome` defaults to `'success'`. Internal fields populated by the audit
|
|
762
|
+
* pipeline (`idempotencyKey`, `context`, `signature`, `prevHash`, `hash`) are
|
|
763
|
+
* stripped — pass them through `log.set({ audit })` if you really need to.
|
|
764
|
+
*/
|
|
765
|
+
interface AuditInput {
|
|
766
|
+
action: string;
|
|
767
|
+
actor: AuditActor;
|
|
768
|
+
target?: AuditTarget;
|
|
769
|
+
outcome?: AuditFields['outcome'];
|
|
770
|
+
reason?: string;
|
|
771
|
+
changes?: AuditFields['changes'];
|
|
772
|
+
causationId?: string;
|
|
773
|
+
correlationId?: string;
|
|
774
|
+
version?: number;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Build a normalised {@link AuditFields} from caller input. Defaults:
|
|
778
|
+
* - `outcome` → `'success'`
|
|
779
|
+
* - `version` → {@link AUDIT_SCHEMA_VERSION}
|
|
780
|
+
*
|
|
781
|
+
* `idempotencyKey` is filled at emit time with the event timestamp so retries
|
|
782
|
+
* stay deterministic.
|
|
783
|
+
*/
|
|
784
|
+
declare function buildAuditFields(input: AuditInput): AuditFields;
|
|
785
|
+
/**
|
|
786
|
+
* Add audit semantics to an existing {@link RequestLogger}.
|
|
787
|
+
*
|
|
788
|
+
* Mutates the logger in place by adding an `audit` method (with a `.deny()`
|
|
789
|
+
* sub-method). Strictly equivalent to calling `log.set({ audit: ... })` plus
|
|
790
|
+
* `_forceKeep` on emit. Idempotent: calling twice on the same logger only
|
|
791
|
+
* attaches the methods once.
|
|
792
|
+
*
|
|
793
|
+
* @example
|
|
794
|
+
* ```ts
|
|
795
|
+
* const log = withAuditMethods(createLogger())
|
|
796
|
+
* log.audit({
|
|
797
|
+
* action: 'invoice.refund',
|
|
798
|
+
* actor: { type: 'user', id: user.id },
|
|
799
|
+
* target: { type: 'invoice', id: 'inv_889' },
|
|
800
|
+
* })
|
|
801
|
+
* ```
|
|
802
|
+
*/
|
|
803
|
+
declare function withAuditMethods<T extends object = Record<string, unknown>>(logger: RequestLogger<T>): AuditableLogger<T>;
|
|
804
|
+
/**
|
|
805
|
+
* Logger augmented with `.audit()` / `.audit.deny()` helpers.
|
|
806
|
+
*/
|
|
807
|
+
type AuditableLogger<T extends object = Record<string, unknown>> = RequestLogger<T> & {
|
|
808
|
+
audit: AuditMethod<T>;
|
|
809
|
+
};
|
|
810
|
+
/** Method shape attached to {@link AuditableLogger}. */
|
|
811
|
+
interface AuditMethod<T extends object = Record<string, unknown>> {
|
|
812
|
+
(input: AuditInput): void;
|
|
813
|
+
/**
|
|
814
|
+
* Record an AuthZ-denied action. Forces `outcome: 'denied'` and requires
|
|
815
|
+
* a human-readable `reason`. Most teams forget to log denials — they are
|
|
816
|
+
* exactly what auditors and security teams ask for.
|
|
817
|
+
*/
|
|
818
|
+
deny: (reason: string, input: Omit<AuditInput, 'outcome' | 'reason'>) => void;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Standalone audit emitter for non-request contexts (jobs, scripts, CLIs).
|
|
822
|
+
*
|
|
823
|
+
* Creates a one-shot logger, sets the audit fields, and emits immediately.
|
|
824
|
+
* The event is force-kept past tail sampling. Returns the emitted wide event,
|
|
825
|
+
* or `null` if logging is globally disabled.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```ts
|
|
829
|
+
* import { audit } from 'evlog'
|
|
830
|
+
*
|
|
831
|
+
* audit({
|
|
832
|
+
* action: 'cron.cleanup',
|
|
833
|
+
* actor: { type: 'system', id: 'cron' },
|
|
834
|
+
* target: { type: 'job', id: 'cleanup-stale-sessions' },
|
|
835
|
+
* outcome: 'success',
|
|
836
|
+
* })
|
|
837
|
+
* ```
|
|
838
|
+
*/
|
|
839
|
+
declare function audit(input: AuditInput): WideEvent | null;
|
|
840
|
+
/**
|
|
841
|
+
* Wrap a function so its outcome (success / failure / denied) is automatically
|
|
842
|
+
* audited.
|
|
843
|
+
*
|
|
844
|
+
* Behaviour:
|
|
845
|
+
* - If `fn` resolves, an audit event with `outcome: 'success'` is emitted.
|
|
846
|
+
* - If `fn` throws an `EvlogError` (or any error) with `status === 403`, the
|
|
847
|
+
* audit event is recorded as `'denied'` with the error message as `reason`.
|
|
848
|
+
* - Any other thrown error produces `outcome: 'failure'` and re-throws.
|
|
849
|
+
*
|
|
850
|
+
* Use {@link AuditDeniedError} to signal denial without an HTTP status.
|
|
851
|
+
*
|
|
852
|
+
* @example
|
|
853
|
+
* ```ts
|
|
854
|
+
* const refundInvoice = withAudit(
|
|
855
|
+
* { action: 'invoice.refund', target: (input) => ({ type: 'invoice', id: input.id }) },
|
|
856
|
+
* async (input: { id: string }, ctx: { actor: AuditActor }) => {
|
|
857
|
+
* await db.invoices.refund(input.id)
|
|
858
|
+
* }
|
|
859
|
+
* )
|
|
860
|
+
*
|
|
861
|
+
* await refundInvoice({ id: 'inv_889' }, { actor: { type: 'user', id: user.id } })
|
|
862
|
+
* ```
|
|
863
|
+
*/
|
|
864
|
+
declare function withAudit<TInput, TOutput>(options: WithAuditOptions<TInput>, fn: (input: TInput, ctx: WithAuditContext) => Promise<TOutput> | TOutput): (input: TInput, ctx: WithAuditContext) => Promise<TOutput>;
|
|
865
|
+
/**
|
|
866
|
+
* Throw inside a {@link withAudit} body to mark the action as `outcome: 'denied'`
|
|
867
|
+
* regardless of the underlying HTTP status. The constructor message becomes the
|
|
868
|
+
* audit `reason`.
|
|
869
|
+
*/
|
|
870
|
+
declare class AuditDeniedError extends Error {
|
|
871
|
+
constructor(reason: string);
|
|
872
|
+
}
|
|
873
|
+
/** Options for {@link withAudit}. `target` may be derived from the input. */
|
|
874
|
+
interface WithAuditOptions<TInput> {
|
|
875
|
+
action: string;
|
|
876
|
+
target?: AuditTarget | ((input: TInput) => AuditTarget | undefined);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Runtime context required by a {@link withAudit}-wrapped function.
|
|
880
|
+
* The actor is always required; correlation IDs are optional.
|
|
881
|
+
*/
|
|
882
|
+
interface WithAuditContext {
|
|
883
|
+
actor: AuditActor;
|
|
884
|
+
causationId?: string;
|
|
885
|
+
correlationId?: string;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Compute a compact, redact-aware diff between two objects for the
|
|
889
|
+
* `changes` field. Output is a JSON Patch-style array (RFC 6902 subset:
|
|
890
|
+
* `add`, `remove`, `replace`) — small enough to ship over the wire.
|
|
891
|
+
*
|
|
892
|
+
* Object keys whose name matches one of the `redactPaths` (dot-notation, e.g.
|
|
893
|
+
* `'user.password'`, `'card.cvv'`) are replaced with `'[REDACTED]'` so PII
|
|
894
|
+
* never leaks through the diff.
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* ```ts
|
|
898
|
+
* log.audit({
|
|
899
|
+
* action: 'user.update',
|
|
900
|
+
* actor: { type: 'user', id: user.id },
|
|
901
|
+
* target: { type: 'user', id: 'usr_42' },
|
|
902
|
+
* changes: auditDiff(before, after, { redactPaths: ['password'] }),
|
|
903
|
+
* })
|
|
904
|
+
* ```
|
|
905
|
+
*/
|
|
906
|
+
declare function auditDiff(before: unknown, after: unknown, options?: AuditDiffOptions): {
|
|
907
|
+
before?: unknown;
|
|
908
|
+
after?: unknown;
|
|
909
|
+
patch: AuditPatchOp[];
|
|
910
|
+
};
|
|
911
|
+
/** Single JSON Patch operation produced by {@link auditDiff}. */
|
|
912
|
+
interface AuditPatchOp {
|
|
913
|
+
op: 'add' | 'remove' | 'replace';
|
|
914
|
+
path: string;
|
|
915
|
+
value?: unknown;
|
|
916
|
+
}
|
|
917
|
+
/** Options for {@link auditDiff}. */
|
|
918
|
+
interface AuditDiffOptions {
|
|
919
|
+
/** Object keys (dot-notation) whose values should be replaced with `[REDACTED]`. */
|
|
920
|
+
redactPaths?: string[];
|
|
921
|
+
/** Custom replacement string. @default '[REDACTED]' */
|
|
922
|
+
replacement?: string;
|
|
923
|
+
/** Include the full redacted `before` snapshot alongside the patch. */
|
|
924
|
+
includeBefore?: boolean;
|
|
925
|
+
/** Include the full redacted `after` snapshot alongside the patch. */
|
|
926
|
+
includeAfter?: boolean;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Define a typed audit action with an optional fixed target type.
|
|
930
|
+
*
|
|
931
|
+
* Returns a curried helper that fills in the action name (and target shape
|
|
932
|
+
* if provided) so call sites stay terse and the action set is discoverable
|
|
933
|
+
* in one place.
|
|
934
|
+
*
|
|
935
|
+
* @example
|
|
936
|
+
* ```ts
|
|
937
|
+
* const refund = defineAuditAction('invoice.refund', { target: 'invoice' })
|
|
938
|
+
*
|
|
939
|
+
* log.audit(refund({
|
|
940
|
+
* actor: { type: 'user', id: user.id },
|
|
941
|
+
* target: { id: 'inv_889' }, // type inferred as 'invoice'
|
|
942
|
+
* outcome: 'success',
|
|
943
|
+
* }))
|
|
944
|
+
* ```
|
|
945
|
+
*/
|
|
946
|
+
declare function defineAuditAction<TTargetType extends string | undefined = undefined>(action: string, options?: {
|
|
947
|
+
target?: TTargetType;
|
|
948
|
+
}): DefinedAuditAction<TTargetType>;
|
|
949
|
+
/**
|
|
950
|
+
* Return type of {@link defineAuditAction}. Accepts a partial input (no
|
|
951
|
+
* `action`, target type pre-filled when provided).
|
|
952
|
+
*/
|
|
953
|
+
type DefinedAuditAction<TTargetType extends string | undefined> = (input: TTargetType extends string ? Omit<AuditInput, 'action' | 'target'> & {
|
|
954
|
+
target?: Omit<AuditTarget, 'type'> & {
|
|
955
|
+
type?: TTargetType;
|
|
956
|
+
};
|
|
957
|
+
} : Omit<AuditInput, 'action'>) => AuditInput;
|
|
958
|
+
/**
|
|
959
|
+
* Test helper that captures every audit event emitted while it is active.
|
|
960
|
+
*
|
|
961
|
+
* Returns `{ events, restore, expect }`:
|
|
962
|
+
* - `events` — live array of captured `AuditFields`, populated as audits fire.
|
|
963
|
+
* - `restore()` — uninstall the collector. Call from `afterEach()`.
|
|
964
|
+
* - `expect.toIncludeAuditOf(matcher)` — assertion helper used inside `expect`
|
|
965
|
+
* blocks, returns `true` if at least one captured event matches.
|
|
966
|
+
*
|
|
967
|
+
* Only captures audits going through `log.audit()` and the standalone
|
|
968
|
+
* `audit()` function. Events emitted via raw `log.set({ audit })` skip the
|
|
969
|
+
* collector by design — wrap them with `log.audit()` to make them visible to
|
|
970
|
+
* tests.
|
|
971
|
+
*
|
|
972
|
+
* @example
|
|
973
|
+
* ```ts
|
|
974
|
+
* const captured = mockAudit()
|
|
975
|
+
* await refundInvoice('inv_889')
|
|
976
|
+
* expect(captured.events).toHaveLength(1)
|
|
977
|
+
* expect(captured.toIncludeAuditOf({ action: 'invoice.refund' })).toBe(true)
|
|
978
|
+
* captured.restore()
|
|
979
|
+
* ```
|
|
980
|
+
*/
|
|
981
|
+
declare function mockAudit(): MockAudit;
|
|
982
|
+
/** Result of {@link mockAudit}. */
|
|
983
|
+
interface MockAudit {
|
|
984
|
+
events: AuditFields[];
|
|
985
|
+
restore: () => void;
|
|
986
|
+
toIncludeAuditOf: (matcher: AuditMatcher) => boolean;
|
|
987
|
+
}
|
|
988
|
+
/** Partial structural matcher for {@link MockAudit.toIncludeAuditOf}. */
|
|
989
|
+
interface AuditMatcher {
|
|
990
|
+
action?: string | RegExp;
|
|
991
|
+
outcome?: AuditFields['outcome'];
|
|
992
|
+
actor?: Partial<AuditActor>;
|
|
993
|
+
target?: Partial<AuditTarget>;
|
|
994
|
+
}
|
|
995
|
+
/** Shape of the optional better-auth bridge for the audit enricher. */
|
|
996
|
+
interface AuditEnricherBetterAuthBridge {
|
|
997
|
+
/** Read the current authenticated session for this request, if any. */
|
|
998
|
+
getSession: (ctx: EnrichContext) => Promise<AuditActor | null | undefined> | AuditActor | null | undefined;
|
|
999
|
+
}
|
|
1000
|
+
/** Options for {@link auditEnricher}. */
|
|
1001
|
+
interface AuditEnricherOptions {
|
|
1002
|
+
/**
|
|
1003
|
+
* Resolve the tenant id for the current request. The result is stored at
|
|
1004
|
+
* `event.audit.context.tenantId`. Multi-tenant SaaS gets isolation by default.
|
|
1005
|
+
*/
|
|
1006
|
+
tenantId?: (ctx: EnrichContext) => string | undefined;
|
|
1007
|
+
/**
|
|
1008
|
+
* Bridge to populate `event.audit.actor` from the authenticated session.
|
|
1009
|
+
* Only used when the application has not already filled `actor`.
|
|
1010
|
+
*/
|
|
1011
|
+
bridge?: AuditEnricherBetterAuthBridge;
|
|
1012
|
+
/** When true, overwrite existing context fields. @default false */
|
|
1013
|
+
overwrite?: boolean;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Enrich audit-bearing wide events with request, runtime, and tenant context.
|
|
1017
|
+
*
|
|
1018
|
+
* Runs only when `event.audit` is present — every other event passes through
|
|
1019
|
+
* untouched. Populates:
|
|
1020
|
+
* - `event.audit.context.requestId` from `ctx.request.requestId`
|
|
1021
|
+
* - `event.audit.context.traceId` from `event.traceId`
|
|
1022
|
+
* - `event.audit.context.ip` from `x-forwarded-for` / `x-real-ip`
|
|
1023
|
+
* - `event.audit.context.userAgent` from `user-agent`
|
|
1024
|
+
* - `event.audit.context.tenantId` from `options.tenantId(ctx)`
|
|
1025
|
+
*
|
|
1026
|
+
* Optionally fills `event.audit.actor` from the better-auth bridge when the
|
|
1027
|
+
* caller did not provide one. Anything else (custom actor strategies,
|
|
1028
|
+
* extra context) belongs in a custom enricher — replace this one entirely.
|
|
1029
|
+
*/
|
|
1030
|
+
declare function auditEnricher(options?: AuditEnricherOptions): (ctx: EnrichContext) => void | Promise<void>;
|
|
1031
|
+
/** Options accepted by {@link auditOnly}. */
|
|
1032
|
+
interface AuditOnlyOptions {
|
|
1033
|
+
/**
|
|
1034
|
+
* When true, the wrapper awaits the wrapped drain so the event is flushed
|
|
1035
|
+
* before the request resolves. Use for crash-safe audit storage.
|
|
1036
|
+
* @default false
|
|
1037
|
+
*/
|
|
1038
|
+
await?: boolean;
|
|
1039
|
+
}
|
|
1040
|
+
/** Drain function signature accepted by all wrappers. Matches `LoggerConfig['drain']`. */
|
|
1041
|
+
type DrainFn = (ctx: DrainContext) => void | Promise<void>;
|
|
1042
|
+
/**
|
|
1043
|
+
* Wrap any drain so it only receives events that carry an `audit` field.
|
|
1044
|
+
*
|
|
1045
|
+
* Use to route audit events to dedicated storage (separate Axiom dataset,
|
|
1046
|
+
* append-only Postgres table, FS journal) without affecting your main drain.
|
|
1047
|
+
*
|
|
1048
|
+
* Per-sink failure isolation comes from `initLogger({ drain: [...] })`: each
|
|
1049
|
+
* drain in the array is invoked independently, so a crashed Axiom call never
|
|
1050
|
+
* blocks the FS audit drain.
|
|
1051
|
+
*
|
|
1052
|
+
* @example
|
|
1053
|
+
* ```ts
|
|
1054
|
+
* import { initLogger, auditOnly } from 'evlog'
|
|
1055
|
+
* import { createAxiomDrain } from 'evlog/axiom'
|
|
1056
|
+
* import { createFsDrain } from 'evlog/fs'
|
|
1057
|
+
*
|
|
1058
|
+
* initLogger({
|
|
1059
|
+
* drain: [
|
|
1060
|
+
* createAxiomDrain({ dataset: 'logs' }),
|
|
1061
|
+
* auditOnly(createFsDrain({ dir: '.audit' }), { await: true }),
|
|
1062
|
+
* ],
|
|
1063
|
+
* })
|
|
1064
|
+
* ```
|
|
1065
|
+
*/
|
|
1066
|
+
declare function auditOnly(drain: DrainFn, options?: AuditOnlyOptions): DrainFn;
|
|
1067
|
+
/** Pluggable persistence for the hash-chain state. */
|
|
1068
|
+
interface SignedChainState {
|
|
1069
|
+
/** Load the previous hash from durable storage, or `null` on first run. */
|
|
1070
|
+
load: () => Promise<string | null> | string | null;
|
|
1071
|
+
/** Persist the latest hash so the chain survives process restarts. */
|
|
1072
|
+
save: (hash: string) => Promise<void> | void;
|
|
1073
|
+
}
|
|
1074
|
+
/** Options for {@link signed}. Pick a strategy at construction time. */
|
|
1075
|
+
type SignedOptions = {
|
|
1076
|
+
strategy: 'hmac';
|
|
1077
|
+
secret: string;
|
|
1078
|
+
algorithm?: 'sha256' | 'sha512';
|
|
1079
|
+
} | {
|
|
1080
|
+
strategy: 'hash-chain';
|
|
1081
|
+
state?: SignedChainState;
|
|
1082
|
+
algorithm?: 'sha256' | 'sha512';
|
|
1083
|
+
};
|
|
1084
|
+
/**
|
|
1085
|
+
* Wrap a drain so every event passing through gains tamper-evident integrity.
|
|
1086
|
+
*
|
|
1087
|
+
* - `'hmac'` — adds `event.audit.signature` (HMAC of the canonical event).
|
|
1088
|
+
* - `'hash-chain'` — adds `event.audit.prevHash` and `event.audit.hash` so the
|
|
1089
|
+
* sequence of events forms a verifiable chain. State persists in memory
|
|
1090
|
+
* by default; pass a `state: { load, save }` for cross-process / durable
|
|
1091
|
+
* chains (Redis, file, Postgres).
|
|
1092
|
+
*
|
|
1093
|
+
* The signature is computed before the event is forwarded to the wrapped
|
|
1094
|
+
* drain — combine with {@link auditOnly} when you only want integrity for
|
|
1095
|
+
* audit events.
|
|
1096
|
+
*
|
|
1097
|
+
* @example
|
|
1098
|
+
* ```ts
|
|
1099
|
+
* import { initLogger, auditOnly, signed } from 'evlog'
|
|
1100
|
+
* import { createFsDrain } from 'evlog/fs'
|
|
1101
|
+
*
|
|
1102
|
+
* initLogger({
|
|
1103
|
+
* drain: auditOnly(
|
|
1104
|
+
* signed(createFsDrain({ dir: '.audit' }), { strategy: 'hash-chain' }),
|
|
1105
|
+
* { await: true },
|
|
1106
|
+
* ),
|
|
1107
|
+
* })
|
|
1108
|
+
* ```
|
|
1109
|
+
*/
|
|
1110
|
+
declare function signed(drain: DrainFn, options: SignedOptions): DrainFn;
|
|
1111
|
+
/**
|
|
1112
|
+
* Strict redact preset for audit events.
|
|
1113
|
+
*
|
|
1114
|
+
* Combine with the user's existing redact configuration via spread:
|
|
1115
|
+
* `initLogger({ redact: { paths: [...auditRedactPreset.paths!, ...mine] } })`.
|
|
1116
|
+
*
|
|
1117
|
+
* Hardens PII handling:
|
|
1118
|
+
* - Drops `Authorization` and `Cookie` headers anywhere they appear.
|
|
1119
|
+
* - Drops common credential field names (`password`, `passwordHash`, `token`,
|
|
1120
|
+
* `apiKey`, `secret`, `accessToken`, `refreshToken`, `cardNumber`, `cvv`,
|
|
1121
|
+
* `ssn`).
|
|
1122
|
+
*
|
|
1123
|
+
* Built-in pattern maskers (email, credit card, …) keep their default
|
|
1124
|
+
* behaviour — partial masking, not full redaction — so audit trails retain
|
|
1125
|
+
* enough signal to be useful.
|
|
1126
|
+
*/
|
|
1127
|
+
declare const auditRedactPreset: RedactConfig;
|
|
1128
|
+
//#endregion
|
|
1129
|
+
export { SamplingRates as $, AuditFields as A, H3EventContext as B, buildAuditFields as C, withAudit as D, signed as E, DrainContext as F, LoggerConfig as G, InternalFields as H, EnrichContext as I, RequestLogEntry as J, ParsedError as K, EnvironmentContext as L, AuditTarget as M, BaseWideEvent as N, withAuditMethods as O, DeepPartial as P, SamplingConfig as Q, ErrorOptions as R, auditRedactPreset as S, mockAudit as T, Log as U, IngestPayload as V, LogLevel as W, RequestLoggerOptions as X, RequestLogger as Y, RouteConfig as Z, WithAuditOptions as _, AuditInput as a, auditEnricher as b, AuditOnlyOptions as c, DefinedAuditAction as d, ServerEvent as et, DrainFn as f, WithAuditContext as g, SignedOptions as h, AuditEnricherOptions as i, WideEvent as it, AuditLoggerMethod as j, AuditActor as k, AuditPatchOp as l, SignedChainState as m, AuditDeniedError as n, TailSamplingContext as nt, AuditMatcher as o, MockAudit as p, RedactConfig as q, AuditDiffOptions as r, TransportConfig as rt, AuditMethod as s, AUDIT_SCHEMA_VERSION as t, TailSamplingCondition as tt, AuditableLogger as u, audit as v, defineAuditAction as w, auditOnly as x, auditDiff as y, FieldContext as z };
|
|
1130
|
+
//# sourceMappingURL=audit-mUutdf6A.d.mts.map
|