nuxt-server-log 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hamid Niakan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,248 @@
1
+ <p align="center">
2
+ <img src="./assets/logo.png" alt="Nuxt Server Log" width="140" height="140">
3
+ </p>
4
+
5
+ <h1 align="center">Nuxt Server Log</h1>
6
+
7
+ [![npm version][npm-version-src]][npm-version-href]
8
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
9
+ [![License][license-src]][license-href]
10
+ [![Nuxt][nuxt-src]][nuxt-href]
11
+
12
+ Structured, per-request server-side logging for Nuxt (Nitro). For every incoming
13
+ request it emits a single JSON log line that bundles together:
14
+
15
+ - the **request** itself (method, path, status, duration, client IP, request id),
16
+ - every **outgoing API call** made on the server while handling that request, and
17
+ - every **error** thrown during that request.
18
+
19
+ This works for both your API routes **and your server-rendered (SSR) pages**: when
20
+ a page is rendered on the server, every data-fetching call your app makes during
21
+ that render is captured under the page's request โ€” so you can see exactly which
22
+ upstream calls contributed to a page, how long each took, and which one made the
23
+ page slow.
24
+
25
+ This gives you one correlated, machine-readable record per request โ€” ready to ship
26
+ to Elasticsearch, Loki, Datadog, or any log pipeline that ingests JSON.
27
+
28
+ - [โœจ &nbsp;Release Notes](/CHANGELOG.md)
29
+
30
+ ## Features
31
+
32
+ - ๐Ÿ“ฆ &nbsp;**One JSON log per request** โ€” request, API calls, and errors correlated by a shared `requestId`.
33
+ - ๐Ÿ–ฅ๏ธ &nbsp;**SSR page insight** โ€” see every upstream call a server-rendered page made during its render, and exactly how long each took.
34
+ - ๐ŸŒ &nbsp;**Automatic outbound API tracking** โ€” server-side `fetch` calls are captured with URL, status, method, and duration.
35
+ - ๐Ÿงจ &nbsp;**Automatic error capture** โ€” unhandled Nitro errors are recorded against the request that caused them.
36
+ - ๐Ÿข &nbsp;**Slow-call warnings** โ€” configurable thresholds warn on slow responses and slow API calls.
37
+ - ๐Ÿ” &nbsp;**Query redaction** โ€” sensitive query params (tokens, passwords, โ€ฆ) are redacted by default.
38
+ - ๐ŸŽš๏ธ &nbsp;**Sampling & filtering** โ€” log a fraction of traffic and exclude noisy paths.
39
+ - ๐Ÿชช &nbsp;**`X-Request-ID` header** โ€” added to every response for end-to-end tracing.
40
+ - ๐Ÿงท &nbsp;**Crash-safe** โ€” circular references and BigInt never break logging.
41
+
42
+ ## Quick Setup
43
+
44
+ Install the module:
45
+
46
+ ```bash
47
+ npx nuxt module add nuxt-server-log
48
+ ```
49
+
50
+ Or manually:
51
+
52
+ ```bash
53
+ npm install nuxt-server-log
54
+ ```
55
+
56
+ ```ts
57
+ // nuxt.config.ts
58
+ export default defineNuxtConfig({
59
+ modules: ["nuxt-server-log"],
60
+ });
61
+ ```
62
+
63
+ That's it. Requests are now logged to the server console as JSON. โœจ
64
+
65
+ ## Configuration
66
+
67
+ Configure the module under the `serverLog` key in `nuxt.config.ts`:
68
+
69
+ ```ts
70
+ export default defineNuxtConfig({
71
+ modules: ["nuxt-server-log"],
72
+ serverLog: {
73
+ logLevel: "info",
74
+ sampleRate: 1,
75
+ excludePaths: ["/__nuxt_error", "/_nuxt"],
76
+ apiDurationWarning: 1500,
77
+ responseDurationWarning: 3000,
78
+ remoteAddressHeader: "x-real-ip",
79
+ redactQueryKeys: ["token", "password", "secret"],
80
+ traceDepth: 10,
81
+ },
82
+ });
83
+ ```
84
+
85
+ | Option | Type | Default | Description |
86
+ | ------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
87
+ | `enabled` | `boolean` | `true` | Master switch. When `false`, the module registers nothing. |
88
+ | `logLevel` | `"debug" \| "info" \| "warn" \| "error"` | `"info"` | Minimum level for the module's own log messages (e.g. slow-call warnings). The per-request log line is always emitted. |
89
+ | `sampleRate` | `number` | `1` | Fraction of requests to log, from `0` (none) to `1` (all). E.g. `0.1` logs ~10% of requests. |
90
+ | `excludePaths` | `string[]` | `["/__nuxt_error"]` | Requests whose path **starts with** any of these are not logged. |
91
+ | `apiDurationWarning` | `number` | `1500` | Emit a `warn` when a captured API call exceeds this many milliseconds. |
92
+ | `responseDurationWarning` | `number` | `3000` | Emit a `warn` when total request duration exceeds this many milliseconds. |
93
+ | `remoteAddressHeader` | `string` | `undefined` | Header to read the client IP from (e.g. behind a proxy). Falls back to `X-Forwarded-For` / socket address. |
94
+ | `redactQueryKeys` | `string[]` | `["token", "password", "secret", "apiKey", "api_key", "auth", "authorization", "access_token", "refresh_token"]` | Query-string keys whose values are replaced with `[REDACTED]` in logs (case-insensitive). |
95
+ | `traceDepth` | `number` | `10` | Maximum number of stack-trace frames recorded per error. |
96
+
97
+ > [!NOTE]
98
+ > `remoteAddressHeader` and `X-Forwarded-For` are client-controllable and can be
99
+ > spoofed unless your app sits behind a trusted proxy that overwrites them.
100
+
101
+ ## How it works
102
+
103
+ The module registers three pieces of Nitro runtime:
104
+
105
+ 1. **A server middleware** that opens a per-request context (via `AsyncLocalStorage`),
106
+ assigns a `requestId`, sets the `X-Request-ID` response header, and writes the
107
+ final JSON log line once the response finishes (or the connection closes).
108
+ 2. **A `fetch` interceptor** that wraps `globalThis.fetch`, so any server-side
109
+ API call is recorded into the current request's context.
110
+ 3. **An error hook** that records unhandled Nitro errors against the active request.
111
+
112
+ Because everything is tied together through the request context, a single log line
113
+ tells the full story of what happened during that request.
114
+
115
+ ## Example output
116
+
117
+ A **server-rendered page** (`GET /`) whose render fetched data from four upstream
118
+ APIs. The whole page took ~2s, and you can immediately see that one categories
119
+ call (1259ms) dominated the render time:
120
+
121
+ ```json
122
+ {
123
+ "@timestamp": "2026-06-19T11:32:03.271Z",
124
+ "requestId": "2ce40ef3-a8d0-4642-82d7-e15bcbf418dc",
125
+ "userAgent": "unknown",
126
+ "statusCode": 200,
127
+ "method": "GET",
128
+ "path": "/",
129
+ "query": "",
130
+ "remoteAddress": "::ffff:127.0.0.1",
131
+ "duration": 2012,
132
+ "apiCalls": [
133
+ {
134
+ "@timestamp": "2026-06-19T11:32:01.482Z",
135
+ "statusCode": 200,
136
+ "url": "https://api.example.com/v1/categories/",
137
+ "method": "GET",
138
+ "duration": 1259
139
+ },
140
+ {
141
+ "@timestamp": "2026-06-19T11:32:02.801Z",
142
+ "statusCode": 200,
143
+ "url": "https://api.example.com/v1/sliders/",
144
+ "method": "GET",
145
+ "duration": 91
146
+ }
147
+ ],
148
+ "errors": []
149
+ }
150
+ ```
151
+
152
+ An API route that made one upstream API call:
153
+
154
+ ```json
155
+ {
156
+ "@timestamp": "2026-06-19T11:34:41.959Z",
157
+ "requestId": "ffbc6274-a7f0-40d6-86fe-1e4154ab1f1b",
158
+ "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
159
+ "statusCode": 200,
160
+ "method": "GET",
161
+ "path": "/api/products",
162
+ "query": "?token=%5BREDACTED%5D&page=2",
163
+ "remoteAddress": "203.0.113.7",
164
+ "duration": 142,
165
+ "apiCalls": [
166
+ {
167
+ "@timestamp": "2026-06-19T11:34:41.820Z",
168
+ "statusCode": 200,
169
+ "url": "https://api.example.com/products?page=2",
170
+ "method": "GET",
171
+ "duration": 98
172
+ }
173
+ ],
174
+ "errors": []
175
+ }
176
+ ```
177
+
178
+ A request where an upstream call failed and an error was thrown:
179
+
180
+ ```json
181
+ {
182
+ "@timestamp": "2026-06-19T11:35:02.114Z",
183
+ "requestId": "a18d2f90-1c4e-4b2a-9a77-2f0b5c9d11aa",
184
+ "userAgent": "node",
185
+ "statusCode": 500,
186
+ "method": "POST",
187
+ "path": "/api/checkout",
188
+ "query": "",
189
+ "remoteAddress": "203.0.113.7",
190
+ "duration": 233,
191
+ "apiCalls": [
192
+ {
193
+ "@timestamp": "2026-06-19T11:35:02.020Z",
194
+ "statusCode": 503,
195
+ "url": "https://payments.example.com/charge",
196
+ "method": "POST",
197
+ "duration": 180,
198
+ "error": "HTTP Error 503: Service Unavailable"
199
+ }
200
+ ],
201
+ "errors": [
202
+ {
203
+ "@timestamp": "2026-06-19T11:35:02.110Z",
204
+ "error": "Payment provider unavailable",
205
+ "type": "Error",
206
+ "trace": [
207
+ "at chargeCard (server/api/checkout.post.ts:24:11)",
208
+ "at handler (server/api/checkout.post.ts:10:3)"
209
+ ],
210
+ "action": "unhandled"
211
+ }
212
+ ]
213
+ }
214
+ ```
215
+
216
+ In addition to the per-request line, slow requests/calls produce standalone `warn`
217
+ entries, e.g.:
218
+
219
+ ```json
220
+ {
221
+ "@timestamp": "โ€ฆ",
222
+ "level": "warn",
223
+ "message": "Slow API call detected",
224
+ "requestId": "โ€ฆ",
225
+ "url": "https://api.example.com/products",
226
+ "duration": 1820
227
+ }
228
+ ```
229
+
230
+ ## Contribution
231
+
232
+ Contributions are welcome! The flow is the standard GitHub one: fork the repo,
233
+ create a branch, and open a pull request.
234
+
235
+ ## License
236
+
237
+ [MIT](./LICENSE)
238
+
239
+ <!-- Badges -->
240
+
241
+ [npm-version-src]: https://img.shields.io/npm/v/nuxt-server-log/latest.svg?style=flat&colorA=020420&colorB=00DC82
242
+ [npm-version-href]: https://npmjs.com/package/nuxt-server-log
243
+ [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-server-log.svg?style=flat&colorA=020420&colorB=00DC82
244
+ [npm-downloads-href]: https://npm.chart.dev/nuxt-server-log
245
+ [license-src]: https://img.shields.io/npm/l/nuxt-server-log.svg?style=flat&colorA=020420&colorB=00DC82
246
+ [license-href]: https://npmjs.com/package/nuxt-server-log
247
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt
248
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1,23 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import { LoggerRuntimeConfig } from '../dist/runtime/types.js';
3
+
4
+ interface ModuleOptions {
5
+ enabled?: boolean;
6
+ logLevel?: "debug" | "info" | "warn" | "error";
7
+ sampleRate?: number;
8
+ excludePaths?: string[];
9
+ apiDurationWarning?: number;
10
+ responseDurationWarning?: number;
11
+ remoteAddressHeader?: string;
12
+ redactQueryKeys?: string[];
13
+ traceDepth?: number;
14
+ }
15
+ declare module "@nuxt/schema" {
16
+ interface RuntimeConfig {
17
+ logger: LoggerRuntimeConfig;
18
+ }
19
+ }
20
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
21
+
22
+ export { _default as default };
23
+ export type { ModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "nuxt-server-log",
3
+ "configKey": "serverLog",
4
+ "version": "0.1.1",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "3.6.1"
8
+ }
9
+ }
@@ -0,0 +1,43 @@
1
+ import { defineNuxtModule, createResolver, addServerHandler, addServerPlugin } from 'nuxt/kit';
2
+
3
+ const module$1 = defineNuxtModule({
4
+ meta: {
5
+ name: "nuxt-server-log",
6
+ configKey: "serverLog"
7
+ },
8
+ defaults: {
9
+ enabled: true,
10
+ logLevel: "info",
11
+ sampleRate: 1,
12
+ excludePaths: ["/__nuxt_error"],
13
+ apiDurationWarning: 1500,
14
+ responseDurationWarning: 3e3,
15
+ redactQueryKeys: [
16
+ "token",
17
+ "password",
18
+ "secret",
19
+ "apiKey",
20
+ "api_key",
21
+ "auth",
22
+ "authorization",
23
+ "access_token",
24
+ "refresh_token"
25
+ ],
26
+ traceDepth: 10
27
+ },
28
+ setup(options, nuxt) {
29
+ if (!options.enabled) return;
30
+ const resolver = createResolver(import.meta.url);
31
+ nuxt.options.runtimeConfig.logger = { ...options };
32
+ addServerHandler({
33
+ middleware: true,
34
+ handler: resolver.resolve("./runtime/server/middleware/logger")
35
+ });
36
+ addServerPlugin(
37
+ resolver.resolve("./runtime/server/plugins/apiInterceptor")
38
+ );
39
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/errorLogger"));
40
+ }
41
+ });
42
+
43
+ export { module$1 as default };
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,71 @@
1
+ import {
2
+ defineEventHandler,
3
+ getRequestURL,
4
+ getHeaders,
5
+ getRequestIP
6
+ } from "h3";
7
+ import { requestContext } from "../utils/context.js";
8
+ import { logger } from "../utils/logger.js";
9
+ import { redactQueryString } from "../utils/helpers.js";
10
+ import { randomUUID } from "node:crypto";
11
+ import { useRuntimeConfig } from "#imports";
12
+ export default defineEventHandler(async (event) => {
13
+ const config = useRuntimeConfig().logger;
14
+ const url = getRequestURL(event);
15
+ if (config.excludePaths?.some((path) => url.pathname.startsWith(path)))
16
+ return;
17
+ if (Math.random() > (config.sampleRate ?? 1)) return;
18
+ const requestId = getHeaders(event)["x-request-id"] || randomUUID();
19
+ const startTime = performance.now();
20
+ const ctx = {
21
+ requestId,
22
+ startTime,
23
+ event,
24
+ apiCalls: [],
25
+ errors: [],
26
+ metaData: {}
27
+ };
28
+ requestContext.enterWith(ctx);
29
+ event.node.res.setHeader("X-Request-ID", requestId);
30
+ let logged = false;
31
+ const writeRequestLog = () => {
32
+ if (logged) return;
33
+ logged = true;
34
+ const duration = performance.now() - startTime;
35
+ const headers = getHeaders(event);
36
+ const logEntry = {
37
+ "@timestamp": (/* @__PURE__ */ new Date()).toISOString(),
38
+ requestId,
39
+ userAgent: headers["user-agent"] || "unknown",
40
+ statusCode: event.node.res.statusCode,
41
+ method: event.method,
42
+ path: url.pathname,
43
+ query: redactQueryString(url.search, config.redactQueryKeys ?? []),
44
+ // `xForwardedFor` trusts the X-Forwarded-For header, which is spoofable
45
+ // unless requests pass through a trusted proxy. Set `remoteAddressHeader`
46
+ // to read the client IP from a header your proxy controls instead.
47
+ remoteAddress: (config.remoteAddressHeader ? headers[config.remoteAddressHeader] : void 0) || getRequestIP(event, { xForwardedFor: true }),
48
+ duration: Math.round(duration),
49
+ apiCalls: ctx.apiCalls,
50
+ errors: ctx.errors,
51
+ metadata: Object.keys(ctx.metaData).length > 0 ? ctx.metaData : void 0
52
+ };
53
+ if (duration > config.responseDurationWarning) {
54
+ logger.warn("Slow response detected", {
55
+ duration: Math.round(duration),
56
+ path: url.pathname
57
+ });
58
+ }
59
+ ctx.apiCalls.forEach((call) => {
60
+ if (call.duration > config.apiDurationWarning) {
61
+ logger.warn("Slow API call detected", {
62
+ url: call.url,
63
+ duration: Math.round(call.duration)
64
+ });
65
+ }
66
+ });
67
+ logger.logRequest(logEntry);
68
+ };
69
+ event.node.res.on("finish", writeRequestLog);
70
+ event.node.res.on("close", writeRequestLog);
71
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nitropack").NitroAppPlugin;
2
+ export default _default;
@@ -0,0 +1,43 @@
1
+ import { addApiCall } from "../utils/context.js";
2
+ import { defineNitroPlugin } from "nitropack/runtime";
3
+ const FETCH_PATCHED = Symbol.for("nuxt-server-log:fetch-patched");
4
+ export default defineNitroPlugin(() => {
5
+ const globalScope = globalThis;
6
+ if (globalScope[FETCH_PATCHED]) return;
7
+ globalScope[FETCH_PATCHED] = true;
8
+ const originalNativeFetch = globalThis.fetch;
9
+ globalThis.fetch = new Proxy(originalNativeFetch, {
10
+ apply: async (target, thisArg, args) => {
11
+ const [input, init = {}] = args;
12
+ const startTime = performance.now();
13
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
14
+ const method = init.method || (typeof input === "object" && "method" in input ? input.method : "GET");
15
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
16
+ let status = null;
17
+ let error;
18
+ try {
19
+ const response = await Reflect.apply(target, thisArg, args);
20
+ status = response.status;
21
+ if (!response.ok)
22
+ error = `HTTP Error ${status}: ${response.statusText || "Unknown Error"}`;
23
+ return response;
24
+ } catch (err) {
25
+ if (err instanceof Error) error = err.message;
26
+ else error = String(err);
27
+ status = null;
28
+ throw err;
29
+ } finally {
30
+ const duration = performance.now() - startTime;
31
+ const apiCall = {
32
+ "@timestamp": timestamp,
33
+ statusCode: status,
34
+ url,
35
+ method: method.toUpperCase(),
36
+ duration: Math.round(duration),
37
+ error
38
+ };
39
+ addApiCall(apiCall);
40
+ }
41
+ }
42
+ });
43
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nitropack").NitroAppPlugin;
2
+ export default _default;
@@ -0,0 +1,10 @@
1
+ import { logger } from "../utils/logger.js";
2
+ import { defineNitroPlugin } from "nitropack/runtime";
3
+ export default defineNitroPlugin((nitroApp) => {
4
+ nitroApp.hooks.hook("error", (error, { event }) => {
5
+ logger.error("Nitro Error!", error, {
6
+ path: event?.path,
7
+ method: event?.method
8
+ });
9
+ });
10
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json"
3
+ }
@@ -0,0 +1,31 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { H3Event } from "h3";
3
+ export interface ApiCall {
4
+ "@timestamp": string;
5
+ statusCode: number | null;
6
+ url: string;
7
+ method: string;
8
+ duration: number;
9
+ error?: string;
10
+ }
11
+ export interface ErrorLog {
12
+ "@timestamp": string;
13
+ error: string;
14
+ type: string;
15
+ trace: string[];
16
+ action: "unhandled" | "caught" | "fatal";
17
+ context?: Record<string, unknown>;
18
+ }
19
+ export interface RequestContext {
20
+ requestId: string;
21
+ startTime: number;
22
+ event: H3Event;
23
+ apiCalls: ApiCall[];
24
+ errors: ErrorLog[];
25
+ metaData: Record<string, unknown>;
26
+ }
27
+ export declare const requestContext: AsyncLocalStorage<RequestContext>;
28
+ export declare function getRequestContext(): RequestContext | undefined;
29
+ export declare function addApiCall(call: ApiCall): void;
30
+ export declare function addError(error: ErrorLog): void;
31
+ export declare function setMetaData(key: string, value: unknown): void;
@@ -0,0 +1,17 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export const requestContext = new AsyncLocalStorage();
3
+ export function getRequestContext() {
4
+ return requestContext.getStore();
5
+ }
6
+ export function addApiCall(call) {
7
+ const ctx = getRequestContext();
8
+ ctx?.apiCalls.push(call);
9
+ }
10
+ export function addError(error) {
11
+ const ctx = getRequestContext();
12
+ ctx?.errors.push(error);
13
+ }
14
+ export function setMetaData(key, value) {
15
+ const ctx = getRequestContext();
16
+ if (ctx) ctx.metaData[key] = value;
17
+ }
@@ -0,0 +1,3 @@
1
+ export declare function parseStackTrace(stack?: string, depth?: number): string[];
2
+ export declare function safeStringify(value: unknown): string;
3
+ export declare function redactQueryString(search: string, redactKeys: string[]): string;
@@ -0,0 +1,35 @@
1
+ export function parseStackTrace(stack, depth = 10) {
2
+ if (!stack) return [];
3
+ return stack.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("at ")).slice(0, depth);
4
+ }
5
+ export function safeStringify(value) {
6
+ const seen = /* @__PURE__ */ new WeakSet();
7
+ try {
8
+ return JSON.stringify(value, (_key, val) => {
9
+ if (typeof val === "bigint") return val.toString();
10
+ if (typeof val === "object" && val !== null) {
11
+ if (seen.has(val)) return "[Circular]";
12
+ seen.add(val);
13
+ }
14
+ return val;
15
+ });
16
+ } catch (err) {
17
+ return JSON.stringify({
18
+ logError: "Failed to serialize log entry",
19
+ reason: err instanceof Error ? err.message : String(err)
20
+ });
21
+ }
22
+ }
23
+ export function redactQueryString(search, redactKeys) {
24
+ if (!search || redactKeys.length === 0) return search;
25
+ const params = new URLSearchParams(search);
26
+ const lowered = redactKeys.map((key) => key.toLowerCase());
27
+ let changed = false;
28
+ for (const key of [...params.keys()]) {
29
+ if (lowered.includes(key.toLowerCase())) {
30
+ params.set(key, "[REDACTED]");
31
+ changed = true;
32
+ }
33
+ }
34
+ return changed ? `?${params.toString()}` : search;
35
+ }
@@ -0,0 +1,18 @@
1
+ import type { ErrorLog } from "./context.js";
2
+ declare class Logger {
3
+ private static instance;
4
+ private logLevel?;
5
+ private levels;
6
+ private constructor();
7
+ static getInstance(): Logger;
8
+ private resolveLogLevel;
9
+ private shouldLog;
10
+ private formatLog;
11
+ debug(message: string, data?: Record<string, unknown>): void;
12
+ info(message: string, data?: Record<string, unknown>): void;
13
+ warn(message: string, data?: Record<string, unknown>): void;
14
+ error(message: string, error?: Error, data?: Record<string, unknown>, action?: ErrorLog["action"]): void;
15
+ logRequest(data: Record<string, unknown>): void;
16
+ }
17
+ export declare const logger: Logger;
18
+ export {};
@@ -0,0 +1,69 @@
1
+ import { getRequestContext } from "./context.js";
2
+ import { parseStackTrace, safeStringify } from "./helpers.js";
3
+ import { useRuntimeConfig } from "#imports";
4
+ class Logger {
5
+ static instance;
6
+ logLevel;
7
+ levels = { debug: 0, info: 1, warn: 2, error: 3 };
8
+ constructor() {
9
+ }
10
+ static getInstance() {
11
+ if (!Logger.instance) Logger.instance = new Logger();
12
+ return Logger.instance;
13
+ }
14
+ resolveLogLevel() {
15
+ if (this.logLevel === void 0) {
16
+ const level = useRuntimeConfig().logger?.logLevel;
17
+ this.logLevel = level !== void 0 ? this.levels[level] ?? 1 : 1;
18
+ }
19
+ return this.logLevel;
20
+ }
21
+ shouldLog(level) {
22
+ return this.levels[level] >= this.resolveLogLevel();
23
+ }
24
+ formatLog(level, message, data) {
25
+ const ctx = getRequestContext();
26
+ const baseLog = {
27
+ "@timestamp": (/* @__PURE__ */ new Date()).toISOString(),
28
+ level,
29
+ message,
30
+ requestId: ctx?.requestId,
31
+ ...data
32
+ };
33
+ return safeStringify(baseLog);
34
+ }
35
+ debug(message, data) {
36
+ if (this.shouldLog("debug"))
37
+ console.log(this.formatLog("debug", message, data));
38
+ }
39
+ info(message, data) {
40
+ if (this.shouldLog("info"))
41
+ console.log(this.formatLog("info", message, data));
42
+ }
43
+ warn(message, data) {
44
+ if (this.shouldLog("warn"))
45
+ console.warn(this.formatLog("warn", message, data));
46
+ }
47
+ error(message, error, data, action = "unhandled") {
48
+ if (this.shouldLog("error")) {
49
+ const config = useRuntimeConfig().logger;
50
+ const errorLog = {
51
+ "@timestamp": (/* @__PURE__ */ new Date()).toISOString(),
52
+ error: error?.message || message,
53
+ type: error?.name || "Error",
54
+ trace: parseStackTrace(error?.stack, config?.traceDepth),
55
+ action,
56
+ context: error?.cause ? { cause: error.cause } : void 0
57
+ };
58
+ const ctx = getRequestContext();
59
+ if (ctx) ctx.errors.push(errorLog);
60
+ console.error(
61
+ this.formatLog("error", message, { error: errorLog, data })
62
+ );
63
+ }
64
+ }
65
+ logRequest(data) {
66
+ console.log(safeStringify(data));
67
+ }
68
+ }
69
+ export const logger = Logger.getInstance();
@@ -0,0 +1,11 @@
1
+ export interface LoggerRuntimeConfig {
2
+ enabled: boolean;
3
+ logLevel: "debug" | "info" | "warn" | "error";
4
+ sampleRate: number;
5
+ excludePaths: string[];
6
+ apiDurationWarning: number;
7
+ responseDurationWarning: number;
8
+ remoteAddressHeader?: string;
9
+ redactQueryKeys: string[];
10
+ traceDepth: number;
11
+ }
File without changes
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "nuxt-server-log",
3
+ "version": "0.1.1",
4
+ "description": "A Nuxt module for structured server-side logging",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Hamid-Niakan/nuxt-server-log.git"
8
+ },
9
+ "packageManager": "npm@10.9.2",
10
+ "engines": {
11
+ "node": "^20.19.0 || >=22.12.0"
12
+ },
13
+ "author": {
14
+ "email": "hniakan@gmail.com",
15
+ "name": "Hamid Niakan"
16
+ },
17
+ "keywords": [
18
+ "nuxt",
19
+ "nuxt-module",
20
+ "logging",
21
+ "logger",
22
+ "structured-logging",
23
+ "request-logging",
24
+ "observability",
25
+ "nitro",
26
+ "SSR",
27
+ "server"
28
+ ],
29
+ "homepage": "https://github.com/Hamid-Niakan/nuxt-server-log#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/Hamid-Niakan/nuxt-server-log/issues"
32
+ },
33
+ "license": "MIT",
34
+ "type": "module",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/types.d.mts",
38
+ "import": "./dist/module.mjs"
39
+ }
40
+ },
41
+ "main": "./dist/module.mjs",
42
+ "typesVersions": {
43
+ "*": {
44
+ ".": [
45
+ "./dist/types.d.mts"
46
+ ]
47
+ }
48
+ },
49
+ "files": [
50
+ "dist"
51
+ ],
52
+ "workspaces": [
53
+ "playground"
54
+ ],
55
+ "scripts": {
56
+ "prepack": "nuxt-module-build build",
57
+ "dev": "npm run dev:prepare && nuxt dev playground",
58
+ "dev:build": "nuxt build playground",
59
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
60
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
61
+ "lint": "eslint .",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest watch",
64
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
65
+ },
66
+ "peerDependencies": {
67
+ "nuxt": "^4.0.0"
68
+ },
69
+ "dependencies": {
70
+ "@nuxt/kit": "^4.4.5"
71
+ },
72
+ "devDependencies": {
73
+ "@nuxt/devtools": "^3.2.4",
74
+ "@nuxt/eslint-config": "^1.15.2",
75
+ "@nuxt/module-builder": "^1.0.2",
76
+ "@nuxt/schema": "^4.4.5",
77
+ "@nuxt/test-utils": "^4.0.3",
78
+ "@types/node": "latest",
79
+ "changelogen": "^0.6.2",
80
+ "eslint": "^10.3.0",
81
+ "nuxt": "^4.4.5",
82
+ "typescript": "~6.0.3",
83
+ "vitest": "^4.1.5",
84
+ "vue-tsc": "^3.2.8"
85
+ }
86
+ }