@bymax-one/nest-logger 1.4.1 → 1.4.3

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.
@@ -2,6 +2,7 @@ import { join } from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { Injectable, Inject, HttpStatus, Catch, ConfigurableModuleBuilder, Module, HttpException, SetMetadata } from '@nestjs/common';
4
4
  import { APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core';
5
+ import { types } from 'util';
5
6
  import { tap, catchError, throwError } from 'rxjs';
6
7
  import pino from 'pino';
7
8
  import { createRequire } from 'module';
@@ -26,7 +27,7 @@ var DEFAULT_HTTP = {
26
27
  isEnabled: false,
27
28
  shouldCaptureExceptions: true,
28
29
  shouldGenerateRequestId: true,
29
- excludePaths: [/^\/health$/, /^\/metrics$/],
30
+ excludePaths: [/^\/health$/, /^\/health\/live$/, /^\/health\/ready$/, /^\/metrics$/],
30
31
  tenantIdHeader: "x-tenant-id"
31
32
  };
32
33
  var DEFAULT_OTEL = {
@@ -111,7 +112,36 @@ var LOG_LEVEL_PRIORITY = Object.freeze([
111
112
  ]);
112
113
 
113
114
  // src/server/config/validate-options.ts
114
- function validateOptions(options) {
115
+ function reject(path2, rule, got) {
116
+ const received = got === void 0 ? "" : `. Got: ${describeValue(got)}`;
117
+ throw new Error(`[BymaxLoggerModule] options.${path2} ${rule}${received}`);
118
+ }
119
+ function describeValue(value) {
120
+ if (value === null) return "null";
121
+ if (Array.isArray(value)) return "an array";
122
+ if (typeof value === "object") return "an object";
123
+ if (typeof value === "function") return "a function";
124
+ if (typeof value === "string") return JSON.stringify(value);
125
+ return String(value);
126
+ }
127
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
128
+ var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
129
+ function checkBoolean(path2, value) {
130
+ if (value !== void 0 && typeof value !== "boolean") {
131
+ reject(path2, "must be a boolean", value);
132
+ }
133
+ }
134
+ function checkNonEmptyString(path2, value) {
135
+ if (value !== void 0 && !isNonEmptyString(value)) {
136
+ reject(path2, "must be a non-empty string", value);
137
+ }
138
+ }
139
+ function checkGroup(path2, value) {
140
+ if (value === void 0) return void 0;
141
+ if (!isRecord(value)) reject(path2, "must be an object", value);
142
+ return value;
143
+ }
144
+ function validateCore(options) {
115
145
  if (!options.service) {
116
146
  throw new Error("[BymaxLoggerModule] options.service is required");
117
147
  }
@@ -128,9 +158,15 @@ function validateOptions(options) {
128
158
  )}. Got: ${String(options.level)}`
129
159
  );
130
160
  }
161
+ const size = options.maxEntrySizeBytes;
162
+ if (size !== void 0 && !Number.isFinite(size)) {
163
+ reject("maxEntrySizeBytes", "must be a finite number", size);
164
+ }
131
165
  if (options.maxEntrySizeBytes !== void 0 && options.maxEntrySizeBytes <= 0) {
132
166
  throw new Error("[BymaxLoggerModule] options.maxEntrySizeBytes must be > 0");
133
167
  }
168
+ }
169
+ function validateClosedSets(options) {
134
170
  if (options.redactStrategy !== void 0 && options.redactStrategy !== "names" && options.redactStrategy !== "paths") {
135
171
  throw new Error(
136
172
  `[BymaxLoggerModule] options.redactStrategy must be 'names' or 'paths'. Got: ${String(
@@ -160,6 +196,109 @@ function validateOptions(options) {
160
196
  );
161
197
  }
162
198
  }
199
+ function validateCustomization(options) {
200
+ checkBoolean("isGlobal", options.isGlobal);
201
+ checkBoolean("shouldDisableDefaultRedact", options.shouldDisableDefaultRedact);
202
+ if (options.redactCensor !== void 0 && typeof options.redactCensor !== "string") {
203
+ reject("redactCensor", "must be a string", options.redactCensor);
204
+ }
205
+ if (options.timestamp !== void 0 && typeof options.timestamp !== "function") {
206
+ reject("timestamp", "must be a function", options.timestamp);
207
+ }
208
+ const paths = options.redactPaths;
209
+ if (paths !== void 0) {
210
+ if (!Array.isArray(paths)) reject("redactPaths", "must be an array of strings", paths);
211
+ paths.forEach((entry, index) => {
212
+ if (!isNonEmptyString(entry)) {
213
+ reject(`redactPaths[${String(index)}]`, "must be a non-empty string", entry);
214
+ }
215
+ });
216
+ }
217
+ const serializers = options.serializers;
218
+ if (serializers !== void 0) {
219
+ if (!isRecord(serializers)) reject("serializers", "must be an object", serializers);
220
+ for (const [key, serializer] of Object.entries(serializers)) {
221
+ if (typeof serializer !== "function") {
222
+ reject(`serializers.${key}`, "must be a function", serializer);
223
+ }
224
+ }
225
+ }
226
+ }
227
+ var UNREADABLE = /* @__PURE__ */ Symbol("unreadable");
228
+ function readMember(destination, key) {
229
+ try {
230
+ return Reflect.get(destination, key);
231
+ } catch {
232
+ return UNREADABLE;
233
+ }
234
+ }
235
+ function validateDestination(entry, index) {
236
+ const path2 = `destinations[${String(index)}]`;
237
+ if (!isRecord(entry)) {
238
+ reject(path2, "must be an object implementing ILogDestination", entry);
239
+ }
240
+ const write = readMember(entry, "write");
241
+ if (write !== UNREADABLE && typeof write !== "function") {
242
+ reject(path2, "must have a write(payload) method");
243
+ }
244
+ const name = readMember(entry, "name");
245
+ if (name !== UNREADABLE && !isNonEmptyString(name)) {
246
+ reject(path2, "must have a non-empty string name", name);
247
+ }
248
+ for (const hook of ["onInit", "onShutdown", "onRegistryReady"]) {
249
+ const value = readMember(entry, hook);
250
+ if (value !== UNREADABLE && value !== void 0 && typeof value !== "function") {
251
+ reject(`${path2}.${hook}`, "must be a function", value);
252
+ }
253
+ }
254
+ }
255
+ function validateDestinations(options) {
256
+ const destinations = options.destinations;
257
+ if (destinations === void 0) return;
258
+ if (!Array.isArray(destinations)) {
259
+ reject("destinations", "must be an array of ILogDestination", destinations);
260
+ }
261
+ destinations.forEach((entry, index) => validateDestination(entry, index));
262
+ }
263
+ function validateHttp(options) {
264
+ const http = checkGroup("http", options.http);
265
+ if (http === void 0) return;
266
+ checkBoolean("http.isEnabled", http["isEnabled"]);
267
+ checkBoolean("http.shouldCaptureExceptions", http["shouldCaptureExceptions"]);
268
+ checkBoolean("http.shouldGenerateRequestId", http["shouldGenerateRequestId"]);
269
+ const excludePaths = http["excludePaths"];
270
+ if (excludePaths !== void 0) {
271
+ if (!Array.isArray(excludePaths)) {
272
+ reject("http.excludePaths", "must be an array of RegExp", excludePaths);
273
+ }
274
+ excludePaths.forEach((pattern, index) => {
275
+ if (!types.isRegExp(pattern)) {
276
+ reject(`http.excludePaths[${String(index)}]`, "must be a RegExp", pattern);
277
+ }
278
+ });
279
+ }
280
+ checkNonEmptyString("http.tenantIdHeader", http["tenantIdHeader"]);
281
+ }
282
+ function validateOtel(options) {
283
+ const otel = checkGroup("otel", options.otel);
284
+ if (otel === void 0) return;
285
+ checkBoolean("otel.shouldAutoInjectTraceContext", otel["shouldAutoInjectTraceContext"]);
286
+ const format = otel["fieldFormat"];
287
+ if (format !== void 0 && format !== "camelCase" && format !== "snake_case") {
288
+ reject("otel.fieldFormat", "must be 'camelCase' or 'snake_case'", format);
289
+ }
290
+ checkNonEmptyString("otel.traceIdField", otel["traceIdField"]);
291
+ checkNonEmptyString("otel.spanIdField", otel["spanIdField"]);
292
+ checkNonEmptyString("otel.traceFlagsField", otel["traceFlagsField"]);
293
+ }
294
+ function validateOptions(options) {
295
+ validateCore(options);
296
+ validateClosedSets(options);
297
+ validateCustomization(options);
298
+ validateDestinations(options);
299
+ validateHttp(options);
300
+ validateOtel(options);
301
+ }
163
302
 
164
303
  // src/server/constants/injection-tokens.constants.ts
165
304
  var LOGGER_OPTIONS_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_OPTIONS");
@@ -1606,8 +1745,11 @@ function buildPinoInstance(options, logContext, destinations, health) {
1606
1745
  options.resourceFormat,
1607
1746
  extraServiceFields(options.service)
1608
1747
  ),
1609
- // Pino requires the timestamp fn to emit the `,"time":"..."` fragment.
1610
- timestamp: () => `,"time":"${options.timestamp()}"`,
1748
+ // Pino requires the timestamp fn to emit the `,"time":<json>` fragment, and
1749
+ // inserts it into the line verbatim. The value comes from consumer code, so it
1750
+ // is encoded as a JSON string here: a quote, a backslash or a newline in it must
1751
+ // neither break the line nor add fields to it.
1752
+ timestamp: () => `,"time":${JSON.stringify(String(options.timestamp()))}`,
1611
1753
  formatters: {
1612
1754
  // Emit the level as a string label instead of the numeric code — easier
1613
1755
  // for log aggregators. Trace context is injected via `mixin`, never via
@@ -2465,10 +2607,18 @@ var PrettyDevDestination = class {
2465
2607
  * Lazily resolve `pino-pretty` and build the colorized transform that renders
2466
2608
  * each entry to `process.stdout`.
2467
2609
  *
2610
+ * @throws Error When `NODE_ENV` is `production`: this sink is development
2611
+ * output, so it refuses rather than put ANSI colour into the log stream.
2468
2612
  * @throws Error When `pino-pretty` is not installed — with an actionable
2469
2613
  * message telling the consumer to install it or drop this destination.
2470
2614
  */
2471
2615
  async onInit() {
2616
+ if (process.env["NODE_ENV"] === "production") {
2617
+ this.initFailed = true;
2618
+ throw new Error(
2619
+ "[PrettyDevDestination] refused in production (NODE_ENV=production): it renders ANSI colour through a main-thread transform. Use DefaultStdoutDestination, or register PrettyDevDestination only outside production."
2620
+ );
2621
+ }
2472
2622
  try {
2473
2623
  const { build } = await import('pino-pretty');
2474
2624
  this.stream = build({
@@ -2595,8 +2745,8 @@ var PrettyDevDestination = class {
2595
2745
  this.flushBuffer(writeRawToStdout);
2596
2746
  return;
2597
2747
  }
2598
- await new Promise((resolve, reject) => {
2599
- stream.once("error", reject);
2748
+ await new Promise((resolve, reject2) => {
2749
+ stream.once("error", reject2);
2600
2750
  stream.end(resolve);
2601
2751
  });
2602
2752
  }
@@ -3025,4 +3175,4 @@ function LogPerformance(thresholdMs = DEFAULT_THRESHOLD_MS) {
3025
3175
  // src/shared/constants/log-keys-convention.constants.ts
3026
3176
  var LOG_KEYS_CONVENTION_REGEX = /^[A-Z][A-Z0-9_]+_[A-Z][A-Z0-9_]+(_[A-Z][A-Z0-9_]+)?$/;
3027
3177
 
3028
- export { BymaxLoggerModule, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpAccessLogMiddleware, HttpExceptionFilter, HttpLoggingInterceptor, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, LogContextService, LogPerformance, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PinoUnexpectedErrorSink, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, applyAccessLog, applyRequestIdMiddleware };
3178
+ export { BymaxLoggerModule, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpAccessLogMiddleware, HttpExceptionFilter, HttpLoggingInterceptor, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOGGER_REDACTOR_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, LogContextService, LogPerformance, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PinoUnexpectedErrorSink, PrettyDevDestination, REDACT_COMMON_FIELDS, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, applyAccessLog, applyRequestIdMiddleware, isValidTraceId };
@@ -270,4 +270,5 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
270
270
  */
271
271
  type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
272
272
 
273
- export { type EmittedDeploymentResource, type EmittedServiceResource, LOG_KEYS_CONVENTION_REGEX, type LogEntry, type LogLevel, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, type ReservedLogKey, type ResolvedServiceMetadata, type ServiceMetadata };
273
+ export { LOG_KEYS_CONVENTION_REGEX, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED };
274
+ export type { EmittedDeploymentResource, EmittedServiceResource, LogEntry, LogLevel, ReservedLogKey, ResolvedServiceMetadata, ServiceMetadata };
@@ -270,4 +270,5 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
270
270
  */
271
271
  type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
272
272
 
273
- export { type EmittedDeploymentResource, type EmittedServiceResource, LOG_KEYS_CONVENTION_REGEX, type LogEntry, type LogLevel, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, type ReservedLogKey, type ResolvedServiceMetadata, type ServiceMetadata };
273
+ export { LOG_KEYS_CONVENTION_REGEX, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED };
274
+ export type { EmittedDeploymentResource, EmittedServiceResource, LogEntry, LogLevel, ReservedLogKey, ResolvedServiceMetadata, ServiceMetadata };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Structured JSON logging for NestJS based on Pino 10, with optional OpenTelemetry correlation.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",
@@ -142,7 +142,7 @@
142
142
  "tinybench": "^6.0.2",
143
143
  "ts-jest": "^29.4.9",
144
144
  "ts-node": "^10.9.2",
145
- "tsup": "^8.5.1",
145
+ "tsup": "^9.0.0",
146
146
  "tsx": "^4.19.2",
147
147
  "typescript": "^5.9.3"
148
148
  },