@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.
- package/CHANGELOG.md +173 -5
- package/README.md +183 -81
- package/dist/server/index.cjs +159 -6
- package/dist/server/index.d.cts +75 -22
- package/dist/server/index.d.ts +75 -22
- package/dist/server/index.mjs +157 -7
- package/dist/shared/index.d.cts +2 -1
- package/dist/shared/index.d.ts +2 -1
- package/package.json +2 -2
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
|
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"
|
|
1610
|
-
|
|
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,
|
|
2599
|
-
stream.once("error",
|
|
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 };
|
package/dist/shared/index.d.cts
CHANGED
|
@@ -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 {
|
|
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/dist/shared/index.d.ts
CHANGED
|
@@ -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 {
|
|
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.
|
|
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": "^
|
|
145
|
+
"tsup": "^9.0.0",
|
|
146
146
|
"tsx": "^4.19.2",
|
|
147
147
|
"typescript": "^5.9.3"
|
|
148
148
|
},
|