hotelme-nuxt-logger 1.2.1 → 1.4.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 CHANGED
@@ -70,6 +70,8 @@ export default defineNuxtConfig({
70
70
  enableRequestLogging: true
71
71
  },
72
72
 
73
+ // Назва потрапляє в runtimeConfig.public.projectName і використовується для всіх
74
+ // записів (запити, помилки Nitro, SSR, клієнтські помилки, ручні logger.*).
73
75
  // Альтернативно через env
74
76
  runtimeConfig: {
75
77
  public: {
@@ -115,6 +117,8 @@ logger.warn('Rate limit approaching', { remaining: 10 })
115
117
 
116
118
  ### В API handlers
117
119
 
120
+ `logger` та `getRequestId` auto-import'яться і в серверному коді Nitro (з 1.4.0). Явний імпорт, якщо треба: `import { logger } from 'hotelme-nuxt-logger/runtime/logger'`.
121
+
118
122
  ```typescript
119
123
  // server/api/hotels.get.ts
120
124
  export default defineEventHandler((event) => {
@@ -169,6 +173,19 @@ export default defineEventHandler((event) => {
169
173
  })
170
174
  ```
171
175
 
176
+ ## Рівні для помилок сервера
177
+
178
+ Nitro `error` hook (модульна опція `enableErrorLogging`):
179
+
180
+ | Що сталося | msg | level | stack |
181
+ |---|---|---|---|
182
+ | `createError({ statusCode: 4xx })`, відсутній `/_nuxt/*` chunk, валідація | `request_error` | Warn | ні |
183
+ | 5xx або не-HTTP виняток у handler/middleware | `unhandled_error` | Error | так |
184
+ | `unhandledRejection` / `uncaughtException` процесу | `unhandled_rejection` / `uncaught_exception` | Fatal | так |
185
+
186
+ 4xx не є помилкою застосунку, тому не засмічує рівень Error; сам запит при цьому все одно
187
+ логується request-middleware як `stats` / `api request` з кодом відповіді.
188
+
172
189
  ## Формат логів
173
190
 
174
191
  | Поле | Тип | Опис |
package/dist/module.cjs CHANGED
@@ -26,6 +26,10 @@ const module$1 = kit.defineNuxtModule({
26
26
  const clientErrorEndpoint = options.clientErrorEndpoint || "/_logger/client-error";
27
27
  nuxt.options.runtimeConfig.public.loggerClientErrorEndpoint = clientErrorEndpoint;
28
28
  kit.addImportsDir(resolver.resolve("./runtime"));
29
+ kit.addServerImports([
30
+ { name: "logger", from: resolver.resolve("./runtime/logger") },
31
+ { name: "getRequestId", from: resolver.resolve("./runtime/logger") }
32
+ ]);
29
33
  if (options.enableRequestLogging) {
30
34
  kit.addServerHandler({
31
35
  handler: resolver.resolve("./runtime/middleware/logger.server"),
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "1.2.1",
7
+ "version": "1.4.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addImportsDir, addServerHandler, addServerPlugin, addPlugin } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addImportsDir, addServerImports, addServerHandler, addServerPlugin, addPlugin } from '@nuxt/kit';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname } from 'path';
4
4
 
@@ -32,6 +32,10 @@ const module = defineNuxtModule({
32
32
  const clientErrorEndpoint = options.clientErrorEndpoint || "/_logger/client-error";
33
33
  nuxt.options.runtimeConfig.public.loggerClientErrorEndpoint = clientErrorEndpoint;
34
34
  addImportsDir(resolver.resolve("./runtime"));
35
+ addServerImports([
36
+ { name: "logger", from: resolver.resolve("./runtime/logger") },
37
+ { name: "getRequestId", from: resolver.resolve("./runtime/logger") }
38
+ ]);
35
39
  if (options.enableRequestLogging) {
36
40
  addServerHandler({
37
41
  handler: resolver.resolve("./runtime/middleware/logger.server"),
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Чисті хелпери для класифікації помилок у Nitro `error` hook.
3
+ * Без імпортів із Nuxt/Nitro, щоб їх можна було перевірити звичайним Node.
4
+ */
5
+ /**
6
+ * HTTP-статус помилки. h3 `createError()` кладе його в `statusCode`,
7
+ * деякі бібліотеки — у `status`. Якщо на помилці статусу немає,
8
+ * повертаємо `fallback` (зазвичай `res.statusCode`).
9
+ */
10
+ export declare function httpStatusOf(error: unknown, fallback?: number): number | undefined;
11
+ /** 4xx: помилка на боці клієнта (не знайдено, невалідний запит, заборонено). */
12
+ export declare function isClientError(status: number | undefined): boolean;
13
+ /**
14
+ * Текст помилки для логу: `message`, а якщо порожній — `statusMessage`
15
+ * (h3 для 404 на статику дає порожній message і "Not Found" у statusMessage).
16
+ */
17
+ export declare function errorText(error: unknown): string;
@@ -0,0 +1,18 @@
1
+ export function httpStatusOf(error, fallback) {
2
+ const e = error;
3
+ for (const candidate of [e?.statusCode, e?.status]) {
4
+ if (typeof candidate === "number" && candidate >= 100 && candidate <= 599) {
5
+ return candidate;
6
+ }
7
+ }
8
+ return fallback;
9
+ }
10
+ export function isClientError(status) {
11
+ return status !== void 0 && status >= 400 && status < 500;
12
+ }
13
+ export function errorText(error) {
14
+ const e = error;
15
+ if (e && typeof e.message === "string" && e.message) return e.message;
16
+ if (e && typeof e.statusMessage === "string" && e.statusMessage) return e.statusMessage;
17
+ return String(error);
18
+ }
@@ -4,11 +4,11 @@
4
4
  * Виводить логи в stdout у JSON-форматі, сумісному з бекенд-логами.
5
5
  *
6
6
  * Обов'язкові поля в кожному лозі:
7
- * - ts: ISO timestamp з timezone
7
+ * - ts: timestamp у локальному часі процесу з зсувом зони
8
8
  * - level: рівень логування (Trace, Debug, Info, Warn, Error, Fatal)
9
9
  * - reqid: унікальний ID запиту (з header X-Request-ID)
10
10
  * - msg: текст повідомлення
11
- * - project: назва проекту (з env або 'hotelme' за замовчуванням)
11
+ * - project: назва проекту (див. resolveProjectName)
12
12
  * - log_type: 'frontend' для розрізнення з бекенд-логами
13
13
  * - pid: process ID
14
14
  *
@@ -20,7 +20,7 @@
20
20
  * "project": "hotelme",
21
21
  * "log_type": "frontend",
22
22
  * "pid": 53,
23
- * "ts": "2025-06-17T09:58:18.539733-0500",
23
+ * "ts": "2025-06-17T09:58:18.539000-0500",
24
24
  * "stage": "after",
25
25
  * "domain": "localhost:3000",
26
26
  * "request": "/api/hotels",
@@ -37,6 +37,17 @@ export declare enum LogLevel {
37
37
  Error = "Error",
38
38
  Fatal = "Fatal"
39
39
  }
40
+ /**
41
+ * Запам'ятати назву проекту для всіх наступних записів. Викликається серверними
42
+ * частинами модуля при старті (Nitro-плагін) та на першому запиті (middleware,
43
+ * SSR-плагін). Порожні значення ігноруються.
44
+ */
45
+ export declare function setProjectName(name: unknown): void;
46
+ /**
47
+ * Назва проекту для запису: явно передана → з runtimeConfig (setProjectName) →
48
+ * env `NUXT_PUBLIC_PROJECT_NAME` → 'hotelme'.
49
+ */
50
+ export declare function resolveProjectName(explicit?: unknown): string;
40
51
  /**
41
52
  * Генерує унікальний ID запиту
42
53
  * Використовується UUID v4 без дефісів для компактності
@@ -51,6 +62,7 @@ export declare function getRequestId(): string;
51
62
  *
52
63
  * Важливі поля в details:
53
64
  * - reqid: ID запиту (береться з X-Request-ID header)
65
+ * - project: назва проекту (за замовчуванням з runtimeConfig, див. resolveProjectName)
54
66
  * - request: URL запиту
55
67
  * - request_time: час виконання запиту в секундах
56
68
  * - domain: домен
@@ -8,8 +8,22 @@ export var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
8
8
  LogLevel2["Fatal"] = "Fatal";
9
9
  return LogLevel2;
10
10
  })(LogLevel || {});
11
- const PROJECT_NAME = import.meta.server ? process.env.NUXT_PUBLIC_PROJECT_NAME || "hotelme" : "hotelme";
11
+ const DEFAULT_PROJECT_NAME = "hotelme";
12
12
  const PID = import.meta.server ? process.pid : void 0;
13
+ let projectName;
14
+ export function setProjectName(name) {
15
+ if (typeof name === "string" && name) {
16
+ projectName = name;
17
+ }
18
+ }
19
+ export function resolveProjectName(explicit) {
20
+ if (typeof explicit === "string" && explicit) return explicit;
21
+ if (projectName) return projectName;
22
+ if (import.meta.server && typeof process !== "undefined" && process.env?.NUXT_PUBLIC_PROJECT_NAME) {
23
+ return process.env.NUXT_PUBLIC_PROJECT_NAME;
24
+ }
25
+ return DEFAULT_PROJECT_NAME;
26
+ }
13
27
  export function getRequestId() {
14
28
  return uuidv4().replace(/-/g, "");
15
29
  }
@@ -27,12 +41,12 @@ function formatTimestamp() {
27
41
  }
28
42
  export function log(level, msg, details) {
29
43
  const reqid = details?.reqid || getRequestId();
30
- const { request, request_time, domain, method, response, stage, ...otherDetails } = details || {};
44
+ const { project, request, request_time, domain, method, response, stage, ...otherDetails } = details || {};
31
45
  const logEntry = {
32
46
  msg,
33
47
  level,
34
48
  reqid,
35
- project: PROJECT_NAME,
49
+ project: resolveProjectName(project),
36
50
  log_type: "frontend",
37
51
  pid: PID,
38
52
  ts: formatTimestamp(),
@@ -1,6 +1,6 @@
1
1
  import { defineEventHandler, getRequestURL } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
- import { logger } from "../logger.js";
3
+ import { logger, setProjectName } from "../logger.js";
4
4
  import { v4 as uuidv4 } from "uuid";
5
5
  export default defineEventHandler(async (event) => {
6
6
  if (event.context.reqid) {
@@ -9,6 +9,7 @@ export default defineEventHandler(async (event) => {
9
9
  try {
10
10
  const config = useRuntimeConfig();
11
11
  const projectName = config.public.projectName || "hotelme";
12
+ setProjectName(projectName);
12
13
  let reqid = event.node.req.headers["x-request-id"];
13
14
  if (!reqid) {
14
15
  reqid = uuidv4().replace(/-/g, "");
@@ -1,8 +1,9 @@
1
1
  import { defineNuxtPlugin, useRequestEvent, useRuntimeConfig } from "#imports";
2
- import { logger } from "../logger.js";
2
+ import { logger, setProjectName } from "../logger.js";
3
3
  import { isBrowserExtensionError } from "../browserExtensionFilter.js";
4
4
  export default defineNuxtPlugin((nuxtApp) => {
5
5
  if (import.meta.server) {
6
+ setProjectName(useRuntimeConfig().public.projectName);
6
7
  const event = useRequestEvent();
7
8
  const reqid = event?.context?.reqid;
8
9
  const request = event?.path;
@@ -7,6 +7,14 @@
7
7
  *
8
8
  * `reqid` is read from `event.context.reqid` set by `logger.server.ts` middleware,
9
9
  * so SSR and backend logs stay correlated by the same id.
10
+ *
11
+ * Levels (since 1.3.0):
12
+ * - 4xx (`createError({ statusCode: 404 })`, missing `/_nuxt/*` asset, validation
13
+ * errors): `msg: "request_error"`, level Warn, no stack trace. These are the
14
+ * client's problem, and the request middleware already logs the same request
15
+ * as `stats`/`api request` with the status code. Stale hashed chunks after a
16
+ * deploy used to produce dozens of Error entries with 1 KB Nitro stacks each.
17
+ * - 5xx and non-HTTP throws: `msg: "unhandled_error"`, level Error, with stack.
10
18
  */
11
19
  declare const _default: import("nitropack").NitroAppPlugin;
12
20
  export default _default;
@@ -1,17 +1,28 @@
1
1
  import { defineNitroPlugin } from "nitropack/runtime";
2
- import { logger } from "../logger.js";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { logger, setProjectName } from "../logger.js";
4
+ import { httpStatusOf, isClientError, errorText } from "../httpError.js";
3
5
  export default defineNitroPlugin((nitroApp) => {
6
+ setProjectName(useRuntimeConfig().public.projectName);
4
7
  nitroApp.hooks.hook("error", (error, ctx) => {
5
8
  const event = ctx?.event;
6
9
  const req = event?.node?.req;
7
10
  const res = event?.node?.res;
8
- logger.error("unhandled_error", {
11
+ const status = httpStatusOf(error, res?.statusCode);
12
+ const base = {
9
13
  reqid: event?.context?.reqid,
10
14
  domain: req?.headers?.host,
11
15
  request: event?.path || req?.url,
12
16
  method: req?.method,
13
- response: res?.statusCode,
14
- error: error instanceof Error ? error.message : String(error),
17
+ response: status,
18
+ error: errorText(error)
19
+ };
20
+ if (isClientError(status)) {
21
+ logger.warn("request_error", base);
22
+ return;
23
+ }
24
+ logger.error("unhandled_error", {
25
+ ...base,
15
26
  error_name: error instanceof Error ? error.name : void 0,
16
27
  stack: error instanceof Error ? error.stack : void 0
17
28
  });
@@ -1,4 +1,5 @@
1
1
  import { defineEventHandler, readBody } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
2
3
  import { logger } from "../../logger.js";
3
4
  import { isBrowserExtensionError } from "../../browserExtensionFilter.js";
4
5
  export default defineEventHandler(async (event) => {
@@ -9,6 +10,7 @@ export default defineEventHandler(async (event) => {
9
10
  }
10
11
  logger.error(typeof msg === "string" && msg ? msg : "client_error", {
11
12
  reqid: event.context.reqid,
13
+ project: useRuntimeConfig().public.projectName,
12
14
  domain: event.node.req.headers.host,
13
15
  referer: event.node.req.headers.referer,
14
16
  user_agent: event.node.req.headers["user-agent"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hotelme-nuxt-logger",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "Стандартний logger для Nuxt проектів з JSON виводом сумісним з бекенд-логами",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -20,6 +20,10 @@
20
20
  "types": "./dist/types.d.ts",
21
21
  "import": "./dist/module.mjs",
22
22
  "require": "./dist/module.cjs"
23
+ },
24
+ "./runtime/logger": {
25
+ "types": "./dist/runtime/logger.d.ts",
26
+ "import": "./dist/runtime/logger.js"
23
27
  }
24
28
  },
25
29
  "main": "./dist/module.cjs",