evlog 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/error.d.mts +12 -6
- package/dist/error.d.ts +12 -6
- package/dist/error.mjs +21 -8
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +2 -1
- package/dist/logger.mjs +4 -3
- package/dist/nitro/errorHandler.d.mts +13 -0
- package/dist/nitro/errorHandler.d.ts +13 -0
- package/dist/nitro/errorHandler.mjs +41 -0
- package/dist/nitro/plugin.mjs +29 -3
- package/dist/nuxt/module.d.mts +20 -1
- package/dist/nuxt/module.d.ts +20 -1
- package/dist/nuxt/module.mjs +18 -2
- package/dist/runtime/client/log.d.mts +2 -1
- package/dist/runtime/client/log.d.ts +2 -1
- package/dist/runtime/client/log.mjs +25 -0
- package/dist/runtime/client/plugin.mjs +2 -1
- package/dist/runtime/server/routes/_evlog/ingest.post.d.mts +5 -0
- package/dist/runtime/server/routes/_evlog/ingest.post.d.ts +5 -0
- package/dist/runtime/server/routes/_evlog/ingest.post.mjs +87 -0
- package/dist/runtime/utils/parseError.mjs +6 -4
- package/dist/types.d.mts +27 -2
- package/dist/types.d.ts +27 -2
- package/package.json +3 -2
package/dist/error.d.mts
CHANGED
|
@@ -16,16 +16,23 @@ import { ErrorOptions } from './types.mjs';
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
declare class EvlogError extends Error {
|
|
19
|
+
/** HTTP status code */
|
|
19
20
|
readonly status: number;
|
|
20
21
|
readonly why?: string;
|
|
21
22
|
readonly fix?: string;
|
|
22
23
|
readonly link?: string;
|
|
23
24
|
constructor(options: ErrorOptions | string);
|
|
25
|
+
/** HTTP status text (alias for message) */
|
|
26
|
+
get statusText(): string;
|
|
27
|
+
/** HTTP status code (alias for compatibility) */
|
|
24
28
|
get statusCode(): number;
|
|
29
|
+
/** HTTP status message (alias for compatibility) */
|
|
30
|
+
get statusMessage(): string;
|
|
31
|
+
/** Structured data for serialization */
|
|
25
32
|
get data(): {
|
|
26
|
-
why
|
|
27
|
-
fix
|
|
28
|
-
link
|
|
33
|
+
why?: string;
|
|
34
|
+
fix?: string;
|
|
35
|
+
link?: string;
|
|
29
36
|
} | undefined;
|
|
30
37
|
toString(): string;
|
|
31
38
|
toJSON(): Record<string, unknown>;
|
|
@@ -34,7 +41,7 @@ declare class EvlogError extends Error {
|
|
|
34
41
|
* Create a structured error with context for debugging and user-facing messages.
|
|
35
42
|
*
|
|
36
43
|
* @param options - Error message string or full options object
|
|
37
|
-
* @returns EvlogError
|
|
44
|
+
* @returns EvlogError with HTTP metadata (`status`, `statusText`) and `data`; also includes `statusCode` and `statusMessage` for legacy compatibility
|
|
38
45
|
*
|
|
39
46
|
* @example
|
|
40
47
|
* ```ts
|
|
@@ -52,6 +59,5 @@ declare class EvlogError extends Error {
|
|
|
52
59
|
* ```
|
|
53
60
|
*/
|
|
54
61
|
declare function createError(options: ErrorOptions | string): EvlogError;
|
|
55
|
-
declare const createEvlogError: typeof createError;
|
|
56
62
|
|
|
57
|
-
export { EvlogError, createError, createEvlogError };
|
|
63
|
+
export { EvlogError, createError, createError as createEvlogError };
|
package/dist/error.d.ts
CHANGED
|
@@ -16,16 +16,23 @@ import { ErrorOptions } from './types.js';
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
declare class EvlogError extends Error {
|
|
19
|
+
/** HTTP status code */
|
|
19
20
|
readonly status: number;
|
|
20
21
|
readonly why?: string;
|
|
21
22
|
readonly fix?: string;
|
|
22
23
|
readonly link?: string;
|
|
23
24
|
constructor(options: ErrorOptions | string);
|
|
25
|
+
/** HTTP status text (alias for message) */
|
|
26
|
+
get statusText(): string;
|
|
27
|
+
/** HTTP status code (alias for compatibility) */
|
|
24
28
|
get statusCode(): number;
|
|
29
|
+
/** HTTP status message (alias for compatibility) */
|
|
30
|
+
get statusMessage(): string;
|
|
31
|
+
/** Structured data for serialization */
|
|
25
32
|
get data(): {
|
|
26
|
-
why
|
|
27
|
-
fix
|
|
28
|
-
link
|
|
33
|
+
why?: string;
|
|
34
|
+
fix?: string;
|
|
35
|
+
link?: string;
|
|
29
36
|
} | undefined;
|
|
30
37
|
toString(): string;
|
|
31
38
|
toJSON(): Record<string, unknown>;
|
|
@@ -34,7 +41,7 @@ declare class EvlogError extends Error {
|
|
|
34
41
|
* Create a structured error with context for debugging and user-facing messages.
|
|
35
42
|
*
|
|
36
43
|
* @param options - Error message string or full options object
|
|
37
|
-
* @returns EvlogError
|
|
44
|
+
* @returns EvlogError with HTTP metadata (`status`, `statusText`) and `data`; also includes `statusCode` and `statusMessage` for legacy compatibility
|
|
38
45
|
*
|
|
39
46
|
* @example
|
|
40
47
|
* ```ts
|
|
@@ -52,6 +59,5 @@ declare class EvlogError extends Error {
|
|
|
52
59
|
* ```
|
|
53
60
|
*/
|
|
54
61
|
declare function createError(options: ErrorOptions | string): EvlogError;
|
|
55
|
-
declare const createEvlogError: typeof createError;
|
|
56
62
|
|
|
57
|
-
export { EvlogError, createError, createEvlogError };
|
|
63
|
+
export { EvlogError, createError, createError as createEvlogError };
|
package/dist/error.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { colors, isServer } from './utils.mjs';
|
|
2
2
|
|
|
3
3
|
class EvlogError extends Error {
|
|
4
|
+
/** HTTP status code */
|
|
4
5
|
status;
|
|
5
6
|
why;
|
|
6
7
|
fix;
|
|
@@ -17,11 +18,24 @@ class EvlogError extends Error {
|
|
|
17
18
|
Error.captureStackTrace(this, EvlogError);
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
/** HTTP status text (alias for message) */
|
|
22
|
+
get statusText() {
|
|
23
|
+
return this.message;
|
|
24
|
+
}
|
|
25
|
+
/** HTTP status code (alias for compatibility) */
|
|
20
26
|
get statusCode() {
|
|
21
27
|
return this.status;
|
|
22
28
|
}
|
|
29
|
+
/** HTTP status message (alias for compatibility) */
|
|
30
|
+
get statusMessage() {
|
|
31
|
+
return this.message;
|
|
32
|
+
}
|
|
33
|
+
/** Structured data for serialization */
|
|
23
34
|
get data() {
|
|
24
|
-
|
|
35
|
+
if (this.why || this.fix || this.link) {
|
|
36
|
+
return { why: this.why, fix: this.fix, link: this.link };
|
|
37
|
+
}
|
|
38
|
+
return void 0;
|
|
25
39
|
}
|
|
26
40
|
toString() {
|
|
27
41
|
const useColors = isServer();
|
|
@@ -48,21 +62,20 @@ class EvlogError extends Error {
|
|
|
48
62
|
return lines.join("\n");
|
|
49
63
|
}
|
|
50
64
|
toJSON() {
|
|
65
|
+
const { data } = this;
|
|
51
66
|
return {
|
|
52
67
|
name: this.name,
|
|
53
68
|
message: this.message,
|
|
54
69
|
status: this.status,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
stack: this.stack
|
|
70
|
+
...data && { data },
|
|
71
|
+
...this.cause instanceof Error && {
|
|
72
|
+
cause: { name: this.cause.name, message: this.cause.message }
|
|
73
|
+
}
|
|
60
74
|
};
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
77
|
function createError(options) {
|
|
64
78
|
return new EvlogError(options);
|
|
65
79
|
}
|
|
66
|
-
const createEvlogError = createError;
|
|
67
80
|
|
|
68
|
-
export { EvlogError, createError, createEvlogError };
|
|
81
|
+
export { EvlogError, createError, createError as createEvlogError };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { EvlogError, createError, createEvlogError } from './error.mjs';
|
|
1
|
+
export { EvlogError, createError, createError as createEvlogError } from './error.mjs';
|
|
2
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, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent } from './types.mjs';
|
|
5
|
+
export { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent } from './types.mjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { EvlogError, createError, createEvlogError } from './error.js';
|
|
1
|
+
export { EvlogError, createError, createError as createEvlogError } from './error.js';
|
|
2
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, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent } from './types.js';
|
|
5
|
+
export { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent } from './types.js';
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { EvlogError, createError, createEvlogError } from './error.mjs';
|
|
1
|
+
export { EvlogError, createError, createError as createEvlogError } from './error.mjs';
|
|
2
2
|
export { createRequestLogger, getEnvironment, initLogger, log, shouldKeep } from './logger.mjs';
|
|
3
3
|
export { useLogger } from './runtime/server/useLogger.mjs';
|
|
4
4
|
export { parseError } from './runtime/utils/parseError.mjs';
|
|
5
5
|
import './utils.mjs';
|
|
6
|
+
import 'defu';
|
package/dist/logger.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { defu } from 'defu';
|
|
1
2
|
import { isDev, detectEnvironment, formatDuration, matchesPattern, getConsoleMethod, colors, getLevelColor } from './utils.mjs';
|
|
2
3
|
|
|
3
4
|
let globalEnv = {
|
|
@@ -149,13 +150,12 @@ function createRequestLogger(options = {}) {
|
|
|
149
150
|
let hasError = false;
|
|
150
151
|
return {
|
|
151
152
|
set(data) {
|
|
152
|
-
context =
|
|
153
|
+
context = defu(data, context);
|
|
153
154
|
},
|
|
154
155
|
error(error, errorContext) {
|
|
155
156
|
hasError = true;
|
|
156
157
|
const err = typeof error === "string" ? new Error(error) : error;
|
|
157
|
-
|
|
158
|
-
...context,
|
|
158
|
+
const errorData = {
|
|
159
159
|
...errorContext,
|
|
160
160
|
error: {
|
|
161
161
|
name: err.name,
|
|
@@ -163,6 +163,7 @@ function createRequestLogger(options = {}) {
|
|
|
163
163
|
stack: err.stack
|
|
164
164
|
}
|
|
165
165
|
};
|
|
166
|
+
context = defu(errorData, context);
|
|
166
167
|
},
|
|
167
168
|
emit(overrides) {
|
|
168
169
|
const durationMs = Date.now() - startTime;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as nitropack from 'nitropack';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom Nitro error handler that properly serializes EvlogError.
|
|
5
|
+
* This ensures that 'data' (containing 'why', 'fix', 'link') is preserved
|
|
6
|
+
* in the JSON response regardless of the underlying HTTP framework.
|
|
7
|
+
*
|
|
8
|
+
* For non-EvlogError, it preserves Nitro's default response shape while
|
|
9
|
+
* sanitizing internal error details in production for 5xx errors.
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: nitropack.NitroErrorHandler;
|
|
12
|
+
|
|
13
|
+
export { _default as default };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as nitropack from 'nitropack';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom Nitro error handler that properly serializes EvlogError.
|
|
5
|
+
* This ensures that 'data' (containing 'why', 'fix', 'link') is preserved
|
|
6
|
+
* in the JSON response regardless of the underlying HTTP framework.
|
|
7
|
+
*
|
|
8
|
+
* For non-EvlogError, it preserves Nitro's default response shape while
|
|
9
|
+
* sanitizing internal error details in production for 5xx errors.
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: nitropack.NitroErrorHandler;
|
|
12
|
+
|
|
13
|
+
export { _default as default };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineNitroErrorHandler } from 'nitropack/runtime';
|
|
2
|
+
import { getRequestURL, setResponseStatus, setResponseHeader, send } from 'h3';
|
|
3
|
+
|
|
4
|
+
const errorHandler = defineNitroErrorHandler((error, event) => {
|
|
5
|
+
const evlogError = error.name === "EvlogError" ? error : error.cause?.name === "EvlogError" ? error.cause : null;
|
|
6
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
7
|
+
const url = getRequestURL(event, { xForwardedHost: true }).pathname;
|
|
8
|
+
if (!evlogError) {
|
|
9
|
+
const status2 = error.statusCode ?? error.status ?? 500;
|
|
10
|
+
const rawMessage = (error.statusText ?? error.statusMessage ?? error.message) || "Internal Server Error";
|
|
11
|
+
const message = isDev ? rawMessage : status2 >= 500 ? "Internal Server Error" : rawMessage;
|
|
12
|
+
setResponseStatus(event, status2);
|
|
13
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
14
|
+
return send(event, JSON.stringify({
|
|
15
|
+
url,
|
|
16
|
+
status: status2,
|
|
17
|
+
statusCode: status2,
|
|
18
|
+
statusText: message,
|
|
19
|
+
statusMessage: message,
|
|
20
|
+
message,
|
|
21
|
+
error: true
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
const status = evlogError.status ?? evlogError.statusCode ?? 500;
|
|
25
|
+
setResponseStatus(event, status);
|
|
26
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
27
|
+
const { data } = evlogError;
|
|
28
|
+
const statusMessage = evlogError.statusMessage || evlogError.message;
|
|
29
|
+
return send(event, JSON.stringify({
|
|
30
|
+
url,
|
|
31
|
+
status,
|
|
32
|
+
statusCode: status,
|
|
33
|
+
statusText: statusMessage,
|
|
34
|
+
statusMessage,
|
|
35
|
+
message: evlogError.message,
|
|
36
|
+
error: true,
|
|
37
|
+
...data !== void 0 && { data }
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export { errorHandler as default };
|
package/dist/nitro/plugin.mjs
CHANGED
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime';
|
|
2
|
+
import { getHeaders } from 'h3';
|
|
2
3
|
import { initLogger, createRequestLogger } from '../logger.mjs';
|
|
3
4
|
import { matchesPattern } from '../utils.mjs';
|
|
5
|
+
import 'defu';
|
|
4
6
|
|
|
5
|
-
function shouldLog(path, include) {
|
|
7
|
+
function shouldLog(path, include, exclude) {
|
|
8
|
+
if (exclude && exclude.length > 0) {
|
|
9
|
+
if (exclude.some((pattern) => matchesPattern(path, pattern))) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
6
13
|
if (!include || include.length === 0) {
|
|
7
14
|
return true;
|
|
8
15
|
}
|
|
9
16
|
return include.some((pattern) => matchesPattern(path, pattern));
|
|
10
17
|
}
|
|
18
|
+
const SENSITIVE_HEADERS = [
|
|
19
|
+
"authorization",
|
|
20
|
+
"cookie",
|
|
21
|
+
"set-cookie",
|
|
22
|
+
"x-api-key",
|
|
23
|
+
"x-auth-token",
|
|
24
|
+
"proxy-authorization"
|
|
25
|
+
];
|
|
26
|
+
function getSafeHeaders(event) {
|
|
27
|
+
const allHeaders = getHeaders(event);
|
|
28
|
+
const safeHeaders = {};
|
|
29
|
+
for (const [key, value] of Object.entries(allHeaders)) {
|
|
30
|
+
if (!SENSITIVE_HEADERS.includes(key.toLowerCase())) {
|
|
31
|
+
safeHeaders[key] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return safeHeaders;
|
|
35
|
+
}
|
|
11
36
|
function getResponseStatus(event) {
|
|
12
37
|
if (event.node?.res?.statusCode) {
|
|
13
38
|
return event.node.res.statusCode;
|
|
@@ -24,7 +49,8 @@ function callDrainHook(nitroApp, emittedEvent, event) {
|
|
|
24
49
|
if (emittedEvent) {
|
|
25
50
|
nitroApp.hooks.callHook("evlog:drain", {
|
|
26
51
|
event: emittedEvent,
|
|
27
|
-
request: { method: event.method, path: event.path, requestId: event.context.requestId }
|
|
52
|
+
request: { method: event.method, path: event.path, requestId: event.context.requestId },
|
|
53
|
+
headers: getSafeHeaders(event)
|
|
28
54
|
}).catch((err) => {
|
|
29
55
|
console.error("[evlog] drain failed:", err);
|
|
30
56
|
});
|
|
@@ -40,7 +66,7 @@ const plugin = defineNitroPlugin((nitroApp) => {
|
|
|
40
66
|
});
|
|
41
67
|
nitroApp.hooks.hook("request", (event) => {
|
|
42
68
|
const e = event;
|
|
43
|
-
if (!shouldLog(e.path, evlogConfig?.include)) {
|
|
69
|
+
if (!shouldLog(e.path, evlogConfig?.include, evlogConfig?.exclude)) {
|
|
44
70
|
return;
|
|
45
71
|
}
|
|
46
72
|
e.context._evlogStartTime = Date.now();
|
package/dist/nuxt/module.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
-
import { EnvironmentContext, SamplingConfig } from '../types.mjs';
|
|
2
|
+
import { EnvironmentContext, SamplingConfig, TransportConfig } from '../types.mjs';
|
|
3
3
|
|
|
4
4
|
interface ModuleOptions {
|
|
5
5
|
/**
|
|
@@ -18,6 +18,13 @@ interface ModuleOptions {
|
|
|
18
18
|
* @example ['/api/**', '/auth/**']
|
|
19
19
|
*/
|
|
20
20
|
include?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Route patterns to exclude from logging.
|
|
23
|
+
* Supports glob patterns like '/api/_nuxt_icon/**'.
|
|
24
|
+
* Exclusions take precedence over inclusions.
|
|
25
|
+
* @example ['/api/_nuxt_icon/**', '/health']
|
|
26
|
+
*/
|
|
27
|
+
exclude?: string[];
|
|
21
28
|
/**
|
|
22
29
|
* Sampling configuration for filtering logs.
|
|
23
30
|
* Allows configuring what percentage of logs to keep per level.
|
|
@@ -35,6 +42,18 @@ interface ModuleOptions {
|
|
|
35
42
|
* ```
|
|
36
43
|
*/
|
|
37
44
|
sampling?: SamplingConfig;
|
|
45
|
+
/**
|
|
46
|
+
* Transport configuration for sending client logs to the server.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* transport: {
|
|
51
|
+
* enabled: true, // Send logs to server API
|
|
52
|
+
* endpoint: '/api/_evlog/ingest' // Custom endpoint
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
transport?: TransportConfig;
|
|
38
57
|
}
|
|
39
58
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
40
59
|
|
package/dist/nuxt/module.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
-
import { EnvironmentContext, SamplingConfig } from '../types.js';
|
|
2
|
+
import { EnvironmentContext, SamplingConfig, TransportConfig } from '../types.js';
|
|
3
3
|
|
|
4
4
|
interface ModuleOptions {
|
|
5
5
|
/**
|
|
@@ -18,6 +18,13 @@ interface ModuleOptions {
|
|
|
18
18
|
* @example ['/api/**', '/auth/**']
|
|
19
19
|
*/
|
|
20
20
|
include?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Route patterns to exclude from logging.
|
|
23
|
+
* Supports glob patterns like '/api/_nuxt_icon/**'.
|
|
24
|
+
* Exclusions take precedence over inclusions.
|
|
25
|
+
* @example ['/api/_nuxt_icon/**', '/health']
|
|
26
|
+
*/
|
|
27
|
+
exclude?: string[];
|
|
21
28
|
/**
|
|
22
29
|
* Sampling configuration for filtering logs.
|
|
23
30
|
* Allows configuring what percentage of logs to keep per level.
|
|
@@ -35,6 +42,18 @@ interface ModuleOptions {
|
|
|
35
42
|
* ```
|
|
36
43
|
*/
|
|
37
44
|
sampling?: SamplingConfig;
|
|
45
|
+
/**
|
|
46
|
+
* Transport configuration for sending client logs to the server.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* transport: {
|
|
51
|
+
* enabled: true, // Send logs to server API
|
|
52
|
+
* endpoint: '/api/_evlog/ingest' // Custom endpoint
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
transport?: TransportConfig;
|
|
38
57
|
}
|
|
39
58
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
40
59
|
|
package/dist/nuxt/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addServerPlugin, addPlugin, addImports, addServerImports } from '@nuxt/kit';
|
|
1
|
+
import { defineNuxtModule, createResolver, addServerHandler, addServerPlugin, addPlugin, addImports, addServerImports } from '@nuxt/kit';
|
|
2
2
|
|
|
3
3
|
const module$1 = defineNuxtModule({
|
|
4
4
|
meta: {
|
|
@@ -9,10 +9,26 @@ const module$1 = defineNuxtModule({
|
|
|
9
9
|
defaults: {},
|
|
10
10
|
setup(options, nuxt) {
|
|
11
11
|
const resolver = createResolver(import.meta.url);
|
|
12
|
+
const transportEnabled = options.transport?.enabled ?? false;
|
|
13
|
+
const transportEndpoint = options.transport?.endpoint ?? "/api/_evlog/ingest";
|
|
14
|
+
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
15
|
+
nitroConfig.errorHandler = nitroConfig.errorHandler || resolver.resolve("../nitro/errorHandler");
|
|
16
|
+
});
|
|
12
17
|
nuxt.options.runtimeConfig.evlog = options;
|
|
13
18
|
nuxt.options.runtimeConfig.public.evlog = {
|
|
14
|
-
pretty: options.pretty
|
|
19
|
+
pretty: options.pretty,
|
|
20
|
+
transport: {
|
|
21
|
+
enabled: transportEnabled,
|
|
22
|
+
endpoint: transportEndpoint
|
|
23
|
+
}
|
|
15
24
|
};
|
|
25
|
+
if (transportEnabled) {
|
|
26
|
+
addServerHandler({
|
|
27
|
+
route: transportEndpoint,
|
|
28
|
+
method: "post",
|
|
29
|
+
handler: resolver.resolve("../runtime/server/routes/_evlog/ingest.post")
|
|
30
|
+
});
|
|
31
|
+
}
|
|
16
32
|
addServerPlugin(resolver.resolve("../nitro/plugin"));
|
|
17
33
|
addPlugin({
|
|
18
34
|
src: resolver.resolve("../runtime/client/plugin"),
|
|
@@ -3,6 +3,8 @@ import { getConsoleMethod } from '../../utils.mjs';
|
|
|
3
3
|
const isClient = typeof window !== "undefined";
|
|
4
4
|
let clientPretty = true;
|
|
5
5
|
let clientService = "client";
|
|
6
|
+
let transportEnabled = false;
|
|
7
|
+
let transportEndpoint = "/api/_evlog/ingest";
|
|
6
8
|
const LEVEL_COLORS = {
|
|
7
9
|
error: "color: #ef4444; font-weight: bold",
|
|
8
10
|
warn: "color: #f59e0b; font-weight: bold",
|
|
@@ -12,6 +14,21 @@ const LEVEL_COLORS = {
|
|
|
12
14
|
function initLog(options = {}) {
|
|
13
15
|
clientPretty = options.pretty ?? true;
|
|
14
16
|
clientService = options.service ?? "client";
|
|
17
|
+
transportEnabled = options.transport?.enabled ?? false;
|
|
18
|
+
transportEndpoint = options.transport?.endpoint ?? "/api/_evlog/ingest";
|
|
19
|
+
}
|
|
20
|
+
async function sendToServer(event) {
|
|
21
|
+
if (!transportEnabled) return;
|
|
22
|
+
try {
|
|
23
|
+
await fetch(transportEndpoint, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify(event),
|
|
27
|
+
keepalive: true,
|
|
28
|
+
credentials: "same-origin"
|
|
29
|
+
});
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
15
32
|
}
|
|
16
33
|
function emitLog(level, event) {
|
|
17
34
|
const formatted = {
|
|
@@ -27,10 +44,18 @@ function emitLog(level, event) {
|
|
|
27
44
|
} else {
|
|
28
45
|
console[method](JSON.stringify(formatted));
|
|
29
46
|
}
|
|
47
|
+
sendToServer(formatted);
|
|
30
48
|
}
|
|
31
49
|
function emitTaggedLog(level, tag, message) {
|
|
32
50
|
if (clientPretty) {
|
|
33
51
|
console[getConsoleMethod(level)](`%c[${tag}]%c ${message}`, LEVEL_COLORS[level] || "", "color: inherit");
|
|
52
|
+
sendToServer({
|
|
53
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
54
|
+
level,
|
|
55
|
+
service: clientService,
|
|
56
|
+
tag,
|
|
57
|
+
message
|
|
58
|
+
});
|
|
34
59
|
} else {
|
|
35
60
|
emitLog(level, { tag, message });
|
|
36
61
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { defineEventHandler, readBody, setResponseStatus, getHeader, getRequestHost, createError } from 'h3';
|
|
2
|
+
import { useNitroApp } from 'nitropack/runtime';
|
|
3
|
+
import { getEnvironment } from '../../../../logger.mjs';
|
|
4
|
+
import 'defu';
|
|
5
|
+
import '../../../../utils.mjs';
|
|
6
|
+
|
|
7
|
+
const VALID_LEVELS = ["info", "error", "warn", "debug"];
|
|
8
|
+
function validateOrigin(event) {
|
|
9
|
+
const origin = getHeader(event, "origin");
|
|
10
|
+
const referer = getHeader(event, "referer");
|
|
11
|
+
const host = getRequestHost(event);
|
|
12
|
+
const requestOrigin = origin || (referer ? new URL(referer).origin : null);
|
|
13
|
+
if (!requestOrigin) {
|
|
14
|
+
throw createError({ statusCode: 403, message: "Missing origin header" });
|
|
15
|
+
}
|
|
16
|
+
const originHost = new URL(requestOrigin).host;
|
|
17
|
+
if (originHost !== host) {
|
|
18
|
+
throw createError({ statusCode: 403, message: "Invalid origin" });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const ISO_8601_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
|
|
22
|
+
function isValidISOTimestamp(value) {
|
|
23
|
+
if (!ISO_8601_REGEX.test(value)) return false;
|
|
24
|
+
const date = new Date(value);
|
|
25
|
+
return !Number.isNaN(date.getTime());
|
|
26
|
+
}
|
|
27
|
+
function validatePayload(body) {
|
|
28
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
29
|
+
throw createError({ statusCode: 400, message: "Invalid request body" });
|
|
30
|
+
}
|
|
31
|
+
const payload = body;
|
|
32
|
+
if (payload.timestamp === void 0 || payload.timestamp === null) {
|
|
33
|
+
throw createError({ statusCode: 400, message: "Missing required field: timestamp" });
|
|
34
|
+
}
|
|
35
|
+
const { timestamp: rawTimestamp } = payload;
|
|
36
|
+
let timestamp;
|
|
37
|
+
if (typeof rawTimestamp === "number") {
|
|
38
|
+
const minTimestamp = (/* @__PURE__ */ new Date("2000-01-01")).getTime();
|
|
39
|
+
const maxTimestamp = Date.now() + 24 * 60 * 60 * 1e3;
|
|
40
|
+
if (rawTimestamp < minTimestamp || rawTimestamp > maxTimestamp) {
|
|
41
|
+
throw createError({ statusCode: 400, message: "Invalid timestamp: value out of reasonable range" });
|
|
42
|
+
}
|
|
43
|
+
timestamp = new Date(rawTimestamp).toISOString();
|
|
44
|
+
} else if (typeof rawTimestamp === "string") {
|
|
45
|
+
if (!isValidISOTimestamp(rawTimestamp)) {
|
|
46
|
+
throw createError({ statusCode: 400, message: "Invalid timestamp: must be a valid ISO 8601 datetime string" });
|
|
47
|
+
}
|
|
48
|
+
timestamp = rawTimestamp;
|
|
49
|
+
} else {
|
|
50
|
+
throw createError({ statusCode: 400, message: "Invalid timestamp: must be string or number" });
|
|
51
|
+
}
|
|
52
|
+
if (!payload.level || typeof payload.level !== "string") {
|
|
53
|
+
throw createError({ statusCode: 400, message: "Missing required field: level" });
|
|
54
|
+
}
|
|
55
|
+
if (!VALID_LEVELS.includes(payload.level)) {
|
|
56
|
+
throw createError({ statusCode: 400, message: `Invalid level: must be one of ${VALID_LEVELS.join(", ")}` });
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...payload,
|
|
60
|
+
timestamp,
|
|
61
|
+
level: payload.level
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const ingest_post = defineEventHandler(async (event) => {
|
|
65
|
+
validateOrigin(event);
|
|
66
|
+
const body = await readBody(event);
|
|
67
|
+
const payload = validatePayload(body);
|
|
68
|
+
const nitroApp = useNitroApp();
|
|
69
|
+
const env = getEnvironment();
|
|
70
|
+
const { service: _clientService, ...sanitizedPayload } = payload;
|
|
71
|
+
const wideEvent = {
|
|
72
|
+
...sanitizedPayload,
|
|
73
|
+
...env,
|
|
74
|
+
source: "client"
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
await nitroApp.hooks.callHook("evlog:drain", {
|
|
78
|
+
event: wideEvent,
|
|
79
|
+
request: { method: "POST", path: event.path }
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
setResponseStatus(event, 204);
|
|
84
|
+
return null;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export { ingest_post as default };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
function parseError(error) {
|
|
2
2
|
if (error && typeof error === "object" && "data" in error) {
|
|
3
|
-
const { data, message: fetchMessage, statusCode: fetchStatusCode } = error;
|
|
4
|
-
const evlogData = data?.data;
|
|
3
|
+
const { data, message: fetchMessage, statusCode: fetchStatusCode, status: fetchStatus } = error;
|
|
4
|
+
const evlogData = data?.data ?? data;
|
|
5
5
|
return {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// Prefer statusText, then statusMessage (or message) for the error message
|
|
7
|
+
message: data?.statusText || data?.statusMessage || data?.message || fetchMessage || "An error occurred",
|
|
8
|
+
// Prefer status, then statusCode for the status value
|
|
9
|
+
status: data?.status || data?.statusCode || fetchStatus || fetchStatusCode || 500,
|
|
8
10
|
why: evlogData?.why,
|
|
9
11
|
fix: evlogData?.fix,
|
|
10
12
|
link: evlogData?.link,
|
package/dist/types.d.mts
CHANGED
|
@@ -33,6 +33,29 @@ declare module 'nitropack/types' {
|
|
|
33
33
|
'evlog:drain': (ctx: DrainContext) => void | Promise<void>;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Transport configuration for sending client logs to the server
|
|
38
|
+
*/
|
|
39
|
+
interface TransportConfig {
|
|
40
|
+
/**
|
|
41
|
+
* Enable sending logs to the server API
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* API endpoint for log ingestion
|
|
47
|
+
* @default '/api/_evlog/ingest'
|
|
48
|
+
*/
|
|
49
|
+
endpoint?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Payload sent from client to server for log ingestion
|
|
53
|
+
*/
|
|
54
|
+
interface IngestPayload {
|
|
55
|
+
timestamp: string;
|
|
56
|
+
level: 'info' | 'error' | 'warn' | 'debug';
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
36
59
|
/**
|
|
37
60
|
* Sampling rates per log level (0-100 percentage)
|
|
38
61
|
*/
|
|
@@ -92,6 +115,8 @@ interface DrainContext {
|
|
|
92
115
|
path?: string;
|
|
93
116
|
requestId?: string;
|
|
94
117
|
};
|
|
118
|
+
/** HTTP headers from the original request (useful for correlation with external services) */
|
|
119
|
+
headers?: Record<string, string>;
|
|
95
120
|
}
|
|
96
121
|
/**
|
|
97
122
|
* Sampling configuration for filtering logs
|
|
@@ -190,7 +215,7 @@ type WideEvent = BaseWideEvent & Record<string, unknown>;
|
|
|
190
215
|
*/
|
|
191
216
|
interface RequestLogger {
|
|
192
217
|
/**
|
|
193
|
-
* Add context to the wide event (
|
|
218
|
+
* Add context to the wide event (deep merge via defu)
|
|
194
219
|
*/
|
|
195
220
|
set: <T extends Record<string, unknown>>(context: T) => void;
|
|
196
221
|
/**
|
|
@@ -314,4 +339,4 @@ interface ParsedError {
|
|
|
314
339
|
raw: unknown;
|
|
315
340
|
}
|
|
316
341
|
|
|
317
|
-
export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent };
|
|
342
|
+
export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent };
|
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,29 @@ declare module 'nitropack/types' {
|
|
|
33
33
|
'evlog:drain': (ctx: DrainContext) => void | Promise<void>;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Transport configuration for sending client logs to the server
|
|
38
|
+
*/
|
|
39
|
+
interface TransportConfig {
|
|
40
|
+
/**
|
|
41
|
+
* Enable sending logs to the server API
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* API endpoint for log ingestion
|
|
47
|
+
* @default '/api/_evlog/ingest'
|
|
48
|
+
*/
|
|
49
|
+
endpoint?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Payload sent from client to server for log ingestion
|
|
53
|
+
*/
|
|
54
|
+
interface IngestPayload {
|
|
55
|
+
timestamp: string;
|
|
56
|
+
level: 'info' | 'error' | 'warn' | 'debug';
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
36
59
|
/**
|
|
37
60
|
* Sampling rates per log level (0-100 percentage)
|
|
38
61
|
*/
|
|
@@ -92,6 +115,8 @@ interface DrainContext {
|
|
|
92
115
|
path?: string;
|
|
93
116
|
requestId?: string;
|
|
94
117
|
};
|
|
118
|
+
/** HTTP headers from the original request (useful for correlation with external services) */
|
|
119
|
+
headers?: Record<string, string>;
|
|
95
120
|
}
|
|
96
121
|
/**
|
|
97
122
|
* Sampling configuration for filtering logs
|
|
@@ -190,7 +215,7 @@ type WideEvent = BaseWideEvent & Record<string, unknown>;
|
|
|
190
215
|
*/
|
|
191
216
|
interface RequestLogger {
|
|
192
217
|
/**
|
|
193
|
-
* Add context to the wide event (
|
|
218
|
+
* Add context to the wide event (deep merge via defu)
|
|
194
219
|
*/
|
|
195
220
|
set: <T extends Record<string, unknown>>(context: T) => void;
|
|
196
221
|
/**
|
|
@@ -314,4 +339,4 @@ interface ParsedError {
|
|
|
314
339
|
raw: unknown;
|
|
315
340
|
}
|
|
316
341
|
|
|
317
|
-
export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, WideEvent };
|
|
342
|
+
export type { BaseWideEvent, DrainContext, EnvironmentContext, ErrorOptions, H3EventContext, IngestPayload, Log, LogLevel, LoggerConfig, ParsedError, RequestLogger, RequestLoggerOptions, SamplingConfig, SamplingRates, ServerEvent, TailSamplingCondition, TailSamplingContext, TransportConfig, WideEvent };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evlog",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Wide event logging library with structured error handling. Inspired by LoggingSucks.",
|
|
5
5
|
"author": "HugoRCD <contact@hrcd.fr>",
|
|
6
6
|
"homepage": "https://evlog.dev",
|
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
"typecheck": "echo 'Typecheck handled by build'"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@nuxt/kit": "^4.3.0"
|
|
70
|
+
"@nuxt/kit": "^4.3.0",
|
|
71
|
+
"defu": "^6.1.4"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
74
|
"@nuxt/devtools": "^3.1.1",
|