@bymax-one/nest-logger 1.2.1 → 1.2.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 +78 -1
- package/README.md +99 -24
- package/dist/server/index.cjs +177 -31
- package/dist/server/index.d.cts +55 -10
- package/dist/server/index.d.ts +55 -10
- package/dist/server/index.mjs +177 -31
- package/dist/shared/index.d.cts +12 -1
- package/dist/shared/index.d.ts +12 -1
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,81 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.3] - 2026-08-14
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A destination that fails to initialize no longer silences the entire application.** Reported
|
|
19
|
+
by a consumer while wiring pretty local output, and reproduced end to end: `destinations`
|
|
20
|
+
**replaces** the default stdout sink, so `destinations: [new PrettyDevDestination()]` without the
|
|
21
|
+
optional `pino-pretty` installed left the app booting, running, exiting `0` and writing **not one
|
|
22
|
+
byte to stdout or stderr** — including the `LOGGER_DESTINATION_INIT_FAILED` entry that exists to
|
|
23
|
+
explain exactly this, which was delivered into the dead sink. Not a crash and not a degraded
|
|
24
|
+
mode: silence that looks like a quiet service.
|
|
25
|
+
|
|
26
|
+
Four independently reasonable behaviours composed into it — `destinations` replacing rather than
|
|
27
|
+
adding, the failure being reported through `this.logger`, the multistream being built from the
|
|
28
|
+
full registered list (it is wired while NestJS builds the DI graph, before any `onInit` runs), and
|
|
29
|
+
a failed sink dropping its writes. Three changes break the chain:
|
|
30
|
+
|
|
31
|
+
- the init failure is written **directly to `process.stderr`**, matching what the shutdown path
|
|
32
|
+
and the write path already do, and for the same reason — reporting a broken sink through the
|
|
33
|
+
sink set is a feedback loop;
|
|
34
|
+
- a destination whose `onInit` rejected is **excluded from the write fan-out**;
|
|
35
|
+
- when **no** destination initializes, entries **fall back to raw NDJSON on stdout**, so a
|
|
36
|
+
misconfiguration degrades visibly instead of going quiet. The fallback is elected before the
|
|
37
|
+
bootstrap entries are announced, so `LOGGER_BOOTSTRAP_WARNING` — the signal that PII redaction
|
|
38
|
+
was disabled — survives the failure it would otherwise have been lost to.
|
|
39
|
+
|
|
40
|
+
The defect belonged to `destinations` as a general contract, not to `PrettyDevDestination`: any
|
|
41
|
+
consumer-written sink that failed `onInit` as the only registered destination did the same thing.
|
|
42
|
+
`pino-pretty` remains an optional peer and nothing was added to `dependencies`, which stays `{}`.
|
|
43
|
+
|
|
44
|
+
### Breaking
|
|
45
|
+
|
|
46
|
+
- **`isPretty` is removed from `BymaxLoggerModuleOptions`.** It has been `@deprecated` and **inert**
|
|
47
|
+
— a `true` value never changed any output on its own. An option that does nothing is worse than
|
|
48
|
+
an absent one, because it is a knob a consumer turns and then reasons from.
|
|
49
|
+
|
|
50
|
+
**Migration:** delete the option. Nothing about your output changes, because nothing about your
|
|
51
|
+
output depended on it. For pretty local logs, add `new PrettyDevDestination()` to `destinations`
|
|
52
|
+
and install the optional `pino-pretty` peer — which is what the option's own documentation had
|
|
53
|
+
been telling you to do.
|
|
54
|
+
|
|
55
|
+
Type-level only: `applyDefaults` no longer emits the field, and no runtime behaviour is affected.
|
|
56
|
+
|
|
57
|
+
### Documentation
|
|
58
|
+
|
|
59
|
+
- **`destinations` documents that it REPLACES the default stdout sink**, rather than claiming to add
|
|
60
|
+
"custom destinations beyond `DefaultStdoutDestination`". Replacement is the correct behaviour — a
|
|
61
|
+
file-only or socket-only deployment must be able to turn stdout off — but the doc described the
|
|
62
|
+
opposite, and it is the assumption that makes the failure above easy to arrive at.
|
|
63
|
+
- **`PrettyDevDestination` no longer implies it follows Pino's recommended transport form.** It runs
|
|
64
|
+
`pino-pretty` as a main-thread transform, a documented pino-pretty API; Pino's own docs recommend
|
|
65
|
+
`transport: { target: 'pino-pretty' }` in a worker, which is unreachable here because this library
|
|
66
|
+
owns the Pino instance and fans out through `pino.multistream`.
|
|
67
|
+
|
|
68
|
+
## [1.2.2] - 2026-08-13
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **`EmittedServiceResource` and `EmittedDeploymentResource` are exported.** `LogEntry['service']`
|
|
73
|
+
and `LogEntry['deployment']` have been typed by these interfaces since 1.2.1, but neither was
|
|
74
|
+
reexported from `@bymax-one/nest-logger/shared`, so a consumer could hold `entry.service` and had
|
|
75
|
+
no way to write its type — a function taking one had to inline the shape or reach for
|
|
76
|
+
`LogEntry['service']`. Reported by a consumer adopting 1.2.1.
|
|
77
|
+
- **Every shared type is now reexported from the server subpath, not a subset.** `LogEntry` was
|
|
78
|
+
already there while the types of its own properties were not, which is the same defect one level
|
|
79
|
+
up. `ResolvedServiceMetadata` and `ReservedLogKey` join them for the same reason.
|
|
80
|
+
- **`LogEntry` declares `'event.name'`.** The field has been emitted since 1.2.1 and documented as
|
|
81
|
+
part of the entry, but only the index signature covered it — so a consumer reading
|
|
82
|
+
`entry['event.name']` got `unknown` and needed a cast. A documented field that cannot be read
|
|
83
|
+
without a cast is not really published. The declaration describes the default key; a renamed
|
|
84
|
+
`eventNameField` still arrives through the index signature.
|
|
85
|
+
|
|
86
|
+
Type-only change: `dist/*.mjs` and `dist/*.cjs` are byte-identical to 1.2.1, since `export type`
|
|
87
|
+
erases at compile time. Only the `.d.ts` files differ.
|
|
88
|
+
|
|
14
89
|
## [1.2.1] - 2026-08-13
|
|
15
90
|
|
|
16
91
|
### Security
|
|
@@ -793,7 +868,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
793
868
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
794
869
|
`release.yml`, Dependabot, and issue templates
|
|
795
870
|
|
|
796
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.
|
|
871
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.3...HEAD
|
|
872
|
+
[1.2.3]: https://github.com/bymaxone/nest-logger/compare/v1.2.2...v1.2.3
|
|
873
|
+
[1.2.2]: https://github.com/bymaxone/nest-logger/compare/v1.2.1...v1.2.2
|
|
797
874
|
[1.2.1]: https://github.com/bymaxone/nest-logger/compare/v1.2.0...v1.2.1
|
|
798
875
|
[1.2.0]: https://github.com/bymaxone/nest-logger/compare/v1.1.0...v1.2.0
|
|
799
876
|
[1.1.0]: https://github.com/bymaxone/nest-logger/compare/v1.0.8...v1.1.0
|
package/README.md
CHANGED
|
@@ -101,10 +101,10 @@ pnpm add @bymax-one/nest-logger
|
|
|
101
101
|
|
|
102
102
|
One package, two entry points — import only what your app needs:
|
|
103
103
|
|
|
104
|
-
| Subpath | Import | Purpose
|
|
105
|
-
| ---------- | ------------------------------- |
|
|
106
|
-
| **Server** | `@bymax-one/nest-logger` | NestJS module, logger service, interceptor, filter, decorators, destinations
|
|
107
|
-
| **Shared** | `@bymax-one/nest-logger/shared` | Types, constants, the log-key regex — `LogLevel`, `LogEntry`, `ServiceMetadata` | None |
|
|
104
|
+
| Subpath | Import | Purpose | Dependencies |
|
|
105
|
+
| ---------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------: |
|
|
106
|
+
| **Server** | `@bymax-one/nest-logger` | NestJS module, logger service, interceptor, filter, decorators, destinations | NestJS 11, pino, rxjs, reflect-metadata |
|
|
107
|
+
| **Shared** | `@bymax-one/nest-logger/shared` | Types, constants, the log-key regex — `LogLevel`, `LogEntry`, `ServiceMetadata`, `ResolvedServiceMetadata`, `EmittedServiceResource`, `EmittedDeploymentResource` | None |
|
|
108
108
|
|
|
109
109
|
```
|
|
110
110
|
shared (zero deps)
|
|
@@ -546,24 +546,23 @@ Full options reference for `BymaxLoggerModule.forRoot(options)`:
|
|
|
546
546
|
|
|
547
547
|
### Top-level options
|
|
548
548
|
|
|
549
|
-
| Option | Type | Default
|
|
550
|
-
| ---------------------------- | ------------------------------- |
|
|
551
|
-
| `service.name` | `string` | **Required**
|
|
552
|
-
| `service.version` | `string` | **Required**
|
|
553
|
-
| `service.namespace` | `string` | —
|
|
554
|
-
| `service.instanceId` | `string` | —
|
|
555
|
-
| `service.environment` | `string` | `NODE_ENV`
|
|
556
|
-
| `resourceFormat` | `'nested' \| 'flat'` | `'nested'`
|
|
557
|
-
| `eventNameField` | `string \| false` | `'event.name'`
|
|
558
|
-
| `errorFormat` | `'pino' \| 'semconv' \| 'both'` | `'pino'`
|
|
559
|
-
| `level` | `LogLevel` | `'info'`
|
|
560
|
-
| `
|
|
561
|
-
| `
|
|
562
|
-
| `
|
|
563
|
-
| `
|
|
564
|
-
| `
|
|
565
|
-
| `
|
|
566
|
-
| `destinations` | `ILogDestination[]` | `[]` | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |
|
|
549
|
+
| Option | Type | Default | Description |
|
|
550
|
+
| ---------------------------- | ------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
551
|
+
| `service.name` | `string` | **Required** | Service name emitted in every log entry |
|
|
552
|
+
| `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
|
|
553
|
+
| `service.namespace` | `string` | — | OTel `service.namespace` (**Stable**) — the group the service belongs to |
|
|
554
|
+
| `service.instanceId` | `string` | — | OTel `service.instance.id` (**Stable**). **Never generated** — supply it from the platform. See [Resource identity](#6b-resource-identity) |
|
|
555
|
+
| `service.environment` | `string` | `NODE_ENV` | OTel `deployment.environment.name` (**Stable**). The deprecated `deployment.environment` is never emitted |
|
|
556
|
+
| `resourceFormat` | `'nested' \| 'flat'` | `'nested'` | Shape of the identity fields. `'flat'` emits the dotted attribute names verbatim |
|
|
557
|
+
| `eventNameField` | `string \| false` | `'event.name'` | Field carrying the derived event name (`PAYMENT_FAILED` → `payment.failed`). `false` disables it |
|
|
558
|
+
| `errorFormat` | `'pino' \| 'semconv' \| 'both'` | `'pino'` | `'both'` adds `exception.*` and `error.type` beside `err.*`; `'semconv'` replaces them |
|
|
559
|
+
| `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
|
|
560
|
+
| `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths, applied on top of the default coverage |
|
|
561
|
+
| `redactStrategy` | `'names' \| 'paths'` | `'names'` | Engine behind the DEFAULT set. `'paths'` restores the pre-1.2 `fast-redact` expansion (four-level ceiling, ~100× slower) |
|
|
562
|
+
| `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the default PII coverage entirely. ⚠️ Emits `LOGGER_BOOTSTRAP_WARNING` at startup — document why |
|
|
563
|
+
| `redactCensor` | `string` | `'[REDACTED]'` | Replacement value written in place of every redacted field |
|
|
564
|
+
| `maxEntrySizeBytes` | `number` | `65536` | UTF-8 byte ceiling per serialized field (`err` + any custom serializer); over it the value becomes a truncation envelope |
|
|
565
|
+
| `destinations` | `ILogDestination[]` | `[new DefaultStdoutDestination()]` | The sinks entries are written to. ⚠️ A non-empty list **replaces** that default — see [Destinations](#destinations-replace-stdout) |
|
|
567
566
|
|
|
568
567
|
### `http` options
|
|
569
568
|
|
|
@@ -713,6 +712,21 @@ BymaxLoggerModule.forRootAsync({
|
|
|
713
712
|
})
|
|
714
713
|
```
|
|
715
714
|
|
|
715
|
+
### Destinations replace stdout
|
|
716
|
+
|
|
717
|
+
A non-empty `destinations` **replaces** `DefaultStdoutDestination` — it does not add to it. That is deliberate: a file-only or socket-only deployment has to be able to turn structured stdout off. To keep stdout alongside a custom sink, list it explicitly:
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
destinations: [new DefaultStdoutDestination(), new LokiDestination({ ... })]
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
The consequence worth knowing: a sink you supply may be the **only** one the application has, so its failure is the application's silence. Two guarantees cover that, and neither requires anything from you:
|
|
724
|
+
|
|
725
|
+
- **A destination that fails `onInit` is reported as `LOGGER_DESTINATION_INIT_FAILED` on `stderr`** — not through the logger, whose sinks are the ones that just failed — and receives no entries.
|
|
726
|
+
- **If _every_ destination fails to initialize, entries fall back to raw NDJSON on `stdout`.** Degraded and ugly, but visible; nothing is lost, including the bootstrap entries.
|
|
727
|
+
|
|
728
|
+
So the common accident — adding `new PrettyDevDestination()` without installing the optional `pino-pretty` — costs you colours and a line on stderr telling you why, never your logs.
|
|
729
|
+
|
|
716
730
|
### Postgres destination (Prisma)
|
|
717
731
|
|
|
718
732
|
```typescript
|
|
@@ -1093,7 +1107,28 @@ PINO_LEVEL_NAMES[30] // 'info' — numeric code → label
|
|
|
1093
1107
|
```typescript
|
|
1094
1108
|
type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
|
|
1095
1109
|
|
|
1096
|
-
|
|
1110
|
+
// What you CONFIGURE.
|
|
1111
|
+
type ServiceMetadata = {
|
|
1112
|
+
readonly name: string
|
|
1113
|
+
readonly version: string
|
|
1114
|
+
readonly namespace?: string
|
|
1115
|
+
readonly instanceId?: string
|
|
1116
|
+
readonly environment?: string
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// What is EMITTED. Deliberately a different type: `instanceId` nests as
|
|
1120
|
+
// `service.instance.id`, and the environment is not under `service` at all.
|
|
1121
|
+
// Reusing the configuration type here would advertise `entry.service.instanceId`,
|
|
1122
|
+
// which no entry carries.
|
|
1123
|
+
type EmittedServiceResource = {
|
|
1124
|
+
readonly name: string
|
|
1125
|
+
readonly version: string
|
|
1126
|
+
readonly namespace?: string
|
|
1127
|
+
readonly instance?: { readonly id: string }
|
|
1128
|
+
readonly [key: string]: unknown
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
type EmittedDeploymentResource = { readonly environment: { readonly name: string } }
|
|
1097
1132
|
|
|
1098
1133
|
type LogEntry = {
|
|
1099
1134
|
/** Pino string label — `'info'`, not `30`. Convert with `PINO_LEVEL_NUMBERS`. */
|
|
@@ -1102,7 +1137,12 @@ type LogEntry = {
|
|
|
1102
1137
|
time: string
|
|
1103
1138
|
msg: string
|
|
1104
1139
|
logKey?: string
|
|
1105
|
-
|
|
1140
|
+
/** Present under `resourceFormat: 'nested'` (the default). */
|
|
1141
|
+
service?: EmittedServiceResource
|
|
1142
|
+
/** Present when an environment resolved — `deployment.environment.name`. */
|
|
1143
|
+
deployment?: EmittedDeploymentResource
|
|
1144
|
+
/** Derived from `logKey` unless `eventNameField: false`. */
|
|
1145
|
+
'event.name'?: string
|
|
1106
1146
|
context?: string
|
|
1107
1147
|
requestId?: string
|
|
1108
1148
|
tenantId?: string
|
|
@@ -1113,6 +1153,41 @@ type LogEntry = {
|
|
|
1113
1153
|
}
|
|
1114
1154
|
```
|
|
1115
1155
|
|
|
1156
|
+
Under `resourceFormat: 'flat'` there is no `service` object at all — the values arrive as dotted
|
|
1157
|
+
top-level keys (`"service.instance.id"`), which the index signature already admits.
|
|
1158
|
+
|
|
1159
|
+
The emitted types are exported, so a helper that receives one can be typed without inlining its
|
|
1160
|
+
shape:
|
|
1161
|
+
|
|
1162
|
+
```typescript
|
|
1163
|
+
import type {
|
|
1164
|
+
EmittedDeploymentResource,
|
|
1165
|
+
EmittedServiceResource,
|
|
1166
|
+
LogEntry
|
|
1167
|
+
} from '@bymax-one/nest-logger/shared'
|
|
1168
|
+
|
|
1169
|
+
/** Build the identity triplet a backend joins on, from a parsed entry. */
|
|
1170
|
+
function identityOf(entry: LogEntry): string {
|
|
1171
|
+
const service: EmittedServiceResource | undefined = entry.service
|
|
1172
|
+
const deployment: EmittedDeploymentResource | undefined = entry.deployment
|
|
1173
|
+
if (service === undefined) {
|
|
1174
|
+
return 'unknown'
|
|
1175
|
+
}
|
|
1176
|
+
// `service.namespace` is part of the identity, not decoration: OTel allows two
|
|
1177
|
+
// services to share a name when their namespaces differ, so a key built without
|
|
1178
|
+
// it collapses them into one.
|
|
1179
|
+
const namespace = service.namespace ?? 'no-namespace'
|
|
1180
|
+
const instance = service.instance?.id ?? 'no-instance'
|
|
1181
|
+
const environment = deployment?.environment.name ?? 'no-environment'
|
|
1182
|
+
return `${namespace}/${service.name}@${service.version}/${instance}/${environment}`
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/** `event.name` is declared, so it reads as a string without a cast. */
|
|
1186
|
+
function eventOf(entry: LogEntry): string | undefined {
|
|
1187
|
+
return entry['event.name']
|
|
1188
|
+
}
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1116
1191
|
### `ILogDestination`
|
|
1117
1192
|
|
|
1118
1193
|
```typescript
|
package/dist/server/index.cjs
CHANGED
|
@@ -76,7 +76,6 @@ function applyDefaults(options) {
|
|
|
76
76
|
redactCensor: options.redactCensor ?? "[REDACTED]",
|
|
77
77
|
shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
|
|
78
78
|
destinations: Object.freeze([...options.destinations ?? []]),
|
|
79
|
-
isPretty: options.isPretty ?? !isProduction,
|
|
80
79
|
http: Object.freeze({
|
|
81
80
|
...http,
|
|
82
81
|
excludePaths: Object.freeze([...http.excludePaths])
|
|
@@ -1021,22 +1020,43 @@ function compileRedactPaths(extraPaths, shouldSkipDefaults) {
|
|
|
1021
1020
|
const sources = shouldSkipDefaults ? [extraPaths] : [DEFAULT_REDACT_PATHS, extraPaths];
|
|
1022
1021
|
return Array.from(new Set(sources.flat()));
|
|
1023
1022
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1023
|
+
|
|
1024
|
+
// src/server/utils/report-destination-failure.util.ts
|
|
1025
|
+
function reportDestinationFailure(logKey, name, cause, msg) {
|
|
1026
1026
|
try {
|
|
1027
|
+
const err = cause instanceof Error ? {
|
|
1028
|
+
type: toSingleLineMessage(String(cause.name)),
|
|
1029
|
+
message: toSingleLineMessage(String(cause.message))
|
|
1030
|
+
} : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
|
|
1027
1031
|
process.stderr.write(
|
|
1028
1032
|
JSON.stringify({
|
|
1029
1033
|
level: "error",
|
|
1030
|
-
logKey
|
|
1031
|
-
destination: name,
|
|
1034
|
+
logKey,
|
|
1035
|
+
destination: toSingleLineMessage(name),
|
|
1032
1036
|
err,
|
|
1033
|
-
msg:
|
|
1037
|
+
msg: toSingleLineMessage(msg)
|
|
1034
1038
|
}) + "\n"
|
|
1035
1039
|
);
|
|
1036
1040
|
} catch {
|
|
1037
1041
|
}
|
|
1038
1042
|
}
|
|
1039
|
-
|
|
1043
|
+
|
|
1044
|
+
// src/server/utils/destination-to-stream.ts
|
|
1045
|
+
function reportWriteFailure(name, cause) {
|
|
1046
|
+
reportDestinationFailure(
|
|
1047
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_WRITE_FAILED,
|
|
1048
|
+
name,
|
|
1049
|
+
cause,
|
|
1050
|
+
`Log destination "${name}" failed to write; the entry was dropped`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
function rescueToStdout(payload) {
|
|
1054
|
+
try {
|
|
1055
|
+
process.stdout.write(payload);
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function destinationToStream(destination, health) {
|
|
1040
1060
|
return new stream.Writable({
|
|
1041
1061
|
// Keep string chunks as strings (Node's default re-encodes them to Buffers
|
|
1042
1062
|
// before `_write`): the common Pino path emits NDJSON strings, so this skips
|
|
@@ -1050,6 +1070,13 @@ function destinationToStream(destination) {
|
|
|
1050
1070
|
};
|
|
1051
1071
|
try {
|
|
1052
1072
|
const payload = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
1073
|
+
if (health.isFailed(destination)) {
|
|
1074
|
+
if (health.shouldRescue(destination)) {
|
|
1075
|
+
rescueToStdout(payload);
|
|
1076
|
+
}
|
|
1077
|
+
callback();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1053
1080
|
const result = destination.write(payload);
|
|
1054
1081
|
if (result instanceof Promise) {
|
|
1055
1082
|
result.then(() => callback(), onFailure);
|
|
@@ -1439,7 +1466,7 @@ function resolveRedactOption(options) {
|
|
|
1439
1466
|
}
|
|
1440
1467
|
return { paths, censor: options.redactCensor };
|
|
1441
1468
|
}
|
|
1442
|
-
function buildPinoInstance(options, logContext, destinations) {
|
|
1469
|
+
function buildPinoInstance(options, logContext, destinations, health) {
|
|
1443
1470
|
const maxBytes = options.maxEntrySizeBytes;
|
|
1444
1471
|
const redact = resolveNameRedactor(options);
|
|
1445
1472
|
const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
|
|
@@ -1511,55 +1538,162 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1511
1538
|
};
|
|
1512
1539
|
const streams = destinations.map((destination) => ({
|
|
1513
1540
|
level: destination.minLevel ?? options.level,
|
|
1514
|
-
stream: destinationToStream(destination)
|
|
1541
|
+
stream: destinationToStream(destination, health)
|
|
1515
1542
|
}));
|
|
1516
1543
|
return pino__default.default(pinoOptions, pino__default.default.multistream(streams));
|
|
1517
1544
|
}
|
|
1545
|
+
var DestinationHealth = class {
|
|
1546
|
+
constructor() {
|
|
1547
|
+
/** Destinations whose `onInit()` rejected. Identity-keyed, not name-keyed. */
|
|
1548
|
+
this.failed = /* @__PURE__ */ new Set();
|
|
1549
|
+
/**
|
|
1550
|
+
* Whether ANY destination initialized successfully.
|
|
1551
|
+
*
|
|
1552
|
+
* A boolean rather than a count, deliberately: the only question ever asked of
|
|
1553
|
+
* it is "is there somewhere to write?". Mutation testing made the point — a
|
|
1554
|
+
* counter incremented with `+=` survived being flipped to `-=`, because
|
|
1555
|
+
* `count === 0` means the same thing either way. A value whose arithmetic
|
|
1556
|
+
* cannot be observed was not a counter, it was a flag wearing a number.
|
|
1557
|
+
*/
|
|
1558
|
+
this.hasHealthy = false;
|
|
1559
|
+
/**
|
|
1560
|
+
* The rescuer's severity index within {@link LOG_LEVEL_PRIORITY}. Starts above
|
|
1561
|
+
* every real level so the first `markFailed` always wins the comparison.
|
|
1562
|
+
*/
|
|
1563
|
+
this.rescuerLevel = Number.POSITIVE_INFINITY;
|
|
1564
|
+
}
|
|
1565
|
+
/** Record that a destination initialized successfully. */
|
|
1566
|
+
markHealthy() {
|
|
1567
|
+
this.hasHealthy = true;
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Record that a destination failed to initialize, and re-elect the rescuer.
|
|
1571
|
+
*
|
|
1572
|
+
* The rescuer is the failed destination with the LOWEST effective level, ties
|
|
1573
|
+
* broken by registration order (the comparison is strict). Registration order
|
|
1574
|
+
* alone is deterministic but arbitrary: a `warn` sink registered before an
|
|
1575
|
+
* `info` one would rescue at `warn` and silently drop the `info` entries the
|
|
1576
|
+
* second was configured to receive, and swapping the array would change the
|
|
1577
|
+
* output for the same configuration. Electing by level rescues the UNION of
|
|
1578
|
+
* what the consumer asked to see.
|
|
1579
|
+
*
|
|
1580
|
+
* Inheriting a level filter at all is deliberate — `minLevel` is the consumer's
|
|
1581
|
+
* own instruction, so the rescue delivers what a working sink would have, never
|
|
1582
|
+
* more. The one entry that must never be filtered, the init failure itself, goes
|
|
1583
|
+
* straight to stderr, outside the multistream.
|
|
1584
|
+
*
|
|
1585
|
+
* @param destination - The destination whose `onInit()` rejected.
|
|
1586
|
+
* @param effectiveLevel - Its multistream level: `minLevel` when set, otherwise
|
|
1587
|
+
* the module-wide `level`.
|
|
1588
|
+
*/
|
|
1589
|
+
markFailed(destination, effectiveLevel) {
|
|
1590
|
+
this.failed.add(destination);
|
|
1591
|
+
const level = LOG_LEVEL_PRIORITY.indexOf(effectiveLevel);
|
|
1592
|
+
if (level < this.rescuerLevel) {
|
|
1593
|
+
this.rescuer = destination;
|
|
1594
|
+
this.rescuerLevel = level;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Whether a destination failed to initialize and must not receive writes.
|
|
1599
|
+
*
|
|
1600
|
+
* @param destination - The destination backing a fan-out stream.
|
|
1601
|
+
* @returns `true` when its `onInit()` rejected.
|
|
1602
|
+
*/
|
|
1603
|
+
isFailed(destination) {
|
|
1604
|
+
return this.failed.has(destination);
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Whether this destination is the one that must write the entry as raw NDJSON
|
|
1608
|
+
* to stdout because NOTHING initialized.
|
|
1609
|
+
*
|
|
1610
|
+
* Exactly one failed destination can satisfy this, so N failed sinks rescue an
|
|
1611
|
+
* entry once rather than N times.
|
|
1612
|
+
*
|
|
1613
|
+
* @param destination - The destination backing a fan-out stream.
|
|
1614
|
+
* @returns `true` when no destination initialized and this one is the elected
|
|
1615
|
+
* rescuer.
|
|
1616
|
+
*/
|
|
1617
|
+
shouldRescue(destination) {
|
|
1618
|
+
return !this.hasHealthy && this.rescuer === destination;
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
DestinationHealth = __decorateClass([
|
|
1622
|
+
common.Injectable()
|
|
1623
|
+
], DestinationHealth);
|
|
1518
1624
|
var DestinationRegistry = class {
|
|
1519
1625
|
/**
|
|
1520
1626
|
* @param registered - Every destination declared under
|
|
1521
1627
|
* `LOGGER_DESTINATIONS_TOKEN`. Injected by token explicitly because the
|
|
1522
1628
|
* package is bundled without `emitDecoratorMetadata`.
|
|
1523
|
-
* @param logger - Structured logger used
|
|
1524
|
-
* token explicitly for the same bundling reason.
|
|
1629
|
+
* @param logger - Structured logger used for the bootstrap entries. Injected by
|
|
1630
|
+
* token explicitly for the same bundling reason. Deliberately NOT used to
|
|
1631
|
+
* report init failures — see the file-level note.
|
|
1632
|
+
* @param options - Resolved module options, read for the bootstrap warnings and
|
|
1633
|
+
* for each destination's effective level.
|
|
1634
|
+
* @param health - Init-health record shared with the write fan-out.
|
|
1525
1635
|
*/
|
|
1526
|
-
constructor(registered, logger, options) {
|
|
1636
|
+
constructor(registered, logger, options, health) {
|
|
1527
1637
|
this.registered = registered;
|
|
1528
1638
|
this.logger = logger;
|
|
1529
1639
|
this.options = options;
|
|
1640
|
+
this.health = health;
|
|
1530
1641
|
/**
|
|
1531
1642
|
* Destinations that initialized successfully, in registration order.
|
|
1532
1643
|
*
|
|
1533
|
-
*
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
* write fan-out; a sink that failed `onInit` still receives writes, contained
|
|
1538
|
-
* fail-soft by the wrapper.
|
|
1644
|
+
* Authoritative for BOTH shutdown (reverse order) and — via the shared
|
|
1645
|
+
* `DestinationHealth` record written alongside it — the write fan-out. The two
|
|
1646
|
+
* are kept in step here because the multistream itself is wired before any
|
|
1647
|
+
* `onInit` runs and cannot be rebuilt afterwards; see `DestinationHealth`.
|
|
1539
1648
|
*/
|
|
1540
1649
|
this.active = [];
|
|
1541
1650
|
}
|
|
1542
1651
|
/**
|
|
1543
1652
|
* Initialize every registered destination. A destination whose `onInit`
|
|
1544
|
-
* rejects is skipped (not added to the active set)
|
|
1545
|
-
*
|
|
1653
|
+
* rejects is skipped (not added to the active set), marked failed so the
|
|
1654
|
+
* fan-out stops writing to it, and reported to stderr — it must not block
|
|
1655
|
+
* application bootstrap.
|
|
1656
|
+
*
|
|
1657
|
+
* The loop completes BEFORE {@link announceBootstrap}, and that ordering is
|
|
1658
|
+
* load-bearing rather than incidental: the health record is what lets a
|
|
1659
|
+
* last-resort sink carry the bootstrap entries when every destination failed.
|
|
1660
|
+
* Announcing first would emit them into a fan-out that has not yet been told
|
|
1661
|
+
* anything is wrong, and `LOGGER_BOOTSTRAP_WARNING` — the signal that exists so
|
|
1662
|
+
* a security review can see PII redaction was disabled — would be lost exactly
|
|
1663
|
+
* when the configuration is already known to be broken.
|
|
1546
1664
|
*/
|
|
1547
1665
|
async onModuleInit() {
|
|
1548
1666
|
for (const destination of this.registered) {
|
|
1549
1667
|
try {
|
|
1550
1668
|
await destination.onInit?.();
|
|
1551
1669
|
this.active.push(destination);
|
|
1670
|
+
this.health.markHealthy();
|
|
1552
1671
|
} catch (cause) {
|
|
1553
|
-
this.
|
|
1554
|
-
|
|
1555
|
-
cause instanceof Error ? cause : new Error(String(cause)),
|
|
1556
|
-
void 0,
|
|
1557
|
-
{ destination: destination.name }
|
|
1558
|
-
);
|
|
1672
|
+
this.health.markFailed(destination, destination.minLevel ?? this.options.level);
|
|
1673
|
+
this.reportInitFailure(destination.name, cause);
|
|
1559
1674
|
}
|
|
1560
1675
|
}
|
|
1561
1676
|
this.announceBootstrap();
|
|
1562
1677
|
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Report one destination init failure as a structured
|
|
1680
|
+
* `LOGGER_DESTINATION_INIT_FAILED` line on `process.stderr`.
|
|
1681
|
+
*
|
|
1682
|
+
* Shares {@link reportDestinationFailure} with the write-failure path rather
|
|
1683
|
+
* than restating the shape, so an operator greps one `logKey` regardless of
|
|
1684
|
+
* which stage failed — and the two cannot drift apart.
|
|
1685
|
+
*
|
|
1686
|
+
* @param name - The failing destination's name.
|
|
1687
|
+
* @param cause - The thrown or rejected value.
|
|
1688
|
+
*/
|
|
1689
|
+
reportInitFailure(name, cause) {
|
|
1690
|
+
reportDestinationFailure(
|
|
1691
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
|
|
1692
|
+
name,
|
|
1693
|
+
cause,
|
|
1694
|
+
`Log destination "${name}" failed to initialize and will receive no entries. If no destination initializes, entries fall back to raw NDJSON on stdout.`
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1563
1697
|
/**
|
|
1564
1698
|
* Emit the one-shot bootstrap entries, AFTER every destination has been
|
|
1565
1699
|
* initialized.
|
|
@@ -1637,7 +1771,8 @@ DestinationRegistry = __decorateClass([
|
|
|
1637
1771
|
common.Injectable(),
|
|
1638
1772
|
__decorateParam(0, common.Inject(LOGGER_DESTINATIONS_TOKEN)),
|
|
1639
1773
|
__decorateParam(1, common.Inject(exports.PinoLoggerService)),
|
|
1640
|
-
__decorateParam(2, common.Inject(LOGGER_OPTIONS_TOKEN))
|
|
1774
|
+
__decorateParam(2, common.Inject(LOGGER_OPTIONS_TOKEN)),
|
|
1775
|
+
__decorateParam(3, common.Inject(DestinationHealth))
|
|
1641
1776
|
], DestinationRegistry);
|
|
1642
1777
|
var FORBIDDEN_CONTEXT_KEYS = PROTOTYPE_POLLUTING_KEYS;
|
|
1643
1778
|
exports.LogContextService = class LogContextService {
|
|
@@ -1732,6 +1867,10 @@ function buildCommonProviders() {
|
|
|
1732
1867
|
return [
|
|
1733
1868
|
exports.LogContextService,
|
|
1734
1869
|
{ provide: LOG_CONTEXT_TOKEN, useExisting: exports.LogContextService },
|
|
1870
|
+
// Shared by the Pino factory (which closes over it in every fan-out stream)
|
|
1871
|
+
// and by DestinationRegistry (which writes to it during onModuleInit). Both
|
|
1872
|
+
// depend on this provider and it depends on nothing, so there is no cycle.
|
|
1873
|
+
DestinationHealth,
|
|
1735
1874
|
{
|
|
1736
1875
|
// The DEFAULT-coverage redactor, shared by the Pino factory's hooks and by
|
|
1737
1876
|
// `PinoLoggerService.child()`. The latter needs it as a provider because
|
|
@@ -1742,8 +1881,13 @@ function buildCommonProviders() {
|
|
|
1742
1881
|
},
|
|
1743
1882
|
{
|
|
1744
1883
|
provide: LOGGER_PINO_INSTANCE_TOKEN,
|
|
1745
|
-
useFactory: (options, logContext, destinations) => buildPinoInstance(options, logContext, destinations),
|
|
1746
|
-
inject: [
|
|
1884
|
+
useFactory: (options, logContext, destinations, health) => buildPinoInstance(options, logContext, destinations, health),
|
|
1885
|
+
inject: [
|
|
1886
|
+
LOGGER_OPTIONS_TOKEN,
|
|
1887
|
+
exports.LogContextService,
|
|
1888
|
+
LOGGER_DESTINATIONS_TOKEN,
|
|
1889
|
+
DestinationHealth
|
|
1890
|
+
]
|
|
1747
1891
|
},
|
|
1748
1892
|
{
|
|
1749
1893
|
provide: LOGGER_DESTINATIONS_TOKEN,
|
|
@@ -1934,8 +2078,10 @@ var PrettyDevDestination = class {
|
|
|
1934
2078
|
*
|
|
1935
2079
|
* Three states:
|
|
1936
2080
|
* - initialized → forward to the pino-pretty transform;
|
|
1937
|
-
* - init failed → DROP
|
|
1938
|
-
*
|
|
2081
|
+
* - init failed → DROP, so a co-registered stdout sink is not duplicated. The
|
|
2082
|
+
* fan-out already skips a failed destination before reaching this method
|
|
2083
|
+
* (and rescues the entry when nothing else initialized); this branch is the
|
|
2084
|
+
* belt to that, for any caller holding the destination directly;
|
|
1939
2085
|
* - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
|
|
1940
2086
|
* → fall back to raw stdout so the entry is not lost.
|
|
1941
2087
|
*
|
package/dist/server/index.d.cts
CHANGED
|
@@ -125,6 +125,26 @@ interface ServiceMetadata {
|
|
|
125
125
|
*/
|
|
126
126
|
readonly environment?: string;
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Service identity after precedence has been applied.
|
|
130
|
+
*
|
|
131
|
+
* Distinct from {@link ServiceMetadata} because the optional fields here mean
|
|
132
|
+
* "resolved to nothing" rather than "not configured": every source — options,
|
|
133
|
+
* `OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`, `NODE_ENV` — has already been
|
|
134
|
+
* consulted, and a field still absent will not appear on any entry.
|
|
135
|
+
*/
|
|
136
|
+
interface ResolvedServiceMetadata {
|
|
137
|
+
/** `service.name`, always present. */
|
|
138
|
+
name: string;
|
|
139
|
+
/** `service.version`, always present. */
|
|
140
|
+
version: string;
|
|
141
|
+
/** `service.namespace`, when any source supplied one. */
|
|
142
|
+
namespace?: string;
|
|
143
|
+
/** `service.instance.id`, when any source supplied one. */
|
|
144
|
+
instanceId?: string;
|
|
145
|
+
/** `deployment.environment.name`, when any source supplied one. */
|
|
146
|
+
environment?: string;
|
|
147
|
+
}
|
|
128
148
|
/**
|
|
129
149
|
* The `service` block as it appears in an EMITTED record, under the default
|
|
130
150
|
* `resourceFormat: 'nested'`.
|
|
@@ -314,15 +334,22 @@ interface BymaxLoggerModuleOptions {
|
|
|
314
334
|
redactCensor?: string;
|
|
315
335
|
/** Disable default redact paths (use with caution). Default: false. */
|
|
316
336
|
shouldDisableDefaultRedact?: boolean;
|
|
317
|
-
/** Custom destinations beyond `DefaultStdoutDestination`. */
|
|
318
|
-
destinations?: readonly ILogDestination[];
|
|
319
337
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
338
|
+
* The sinks every entry is written to.
|
|
339
|
+
*
|
|
340
|
+
* A non-empty list **REPLACES** `DefaultStdoutDestination` — it does not add to
|
|
341
|
+
* it. That is deliberate: a file-only or socket-only deployment has to be able
|
|
342
|
+
* to turn stdout off. It also means a sink supplied here may be the only one
|
|
343
|
+
* the application has, so a destination that fails `onInit` is reported on
|
|
344
|
+
* stderr and, if nothing else initialized, entries fall back to raw NDJSON on
|
|
345
|
+
* stdout rather than disappearing.
|
|
346
|
+
*
|
|
347
|
+
* Include `new DefaultStdoutDestination()` explicitly to keep structured stdout
|
|
348
|
+
* alongside a custom sink.
|
|
349
|
+
*
|
|
350
|
+
* Default: `[new DefaultStdoutDestination()]`.
|
|
324
351
|
*/
|
|
325
|
-
|
|
352
|
+
destinations?: readonly ILogDestination[];
|
|
326
353
|
/** HTTP module configuration. */
|
|
327
354
|
http?: HttpOptions;
|
|
328
355
|
/** OpenTelemetry integration tuning. */
|
|
@@ -512,6 +539,11 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
|
|
|
512
539
|
*/
|
|
513
540
|
readonly HTTP_REQUEST_COMPLETED: "reserved: superseded by the four status-specific terminal keys";
|
|
514
541
|
}>;
|
|
542
|
+
/**
|
|
543
|
+
* Strongly-typed union of every reserved log key value, derived from
|
|
544
|
+
* {@link RESERVED_LOG_KEYS}. Use this type to constrain log-key arguments.
|
|
545
|
+
*/
|
|
546
|
+
type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
|
|
515
547
|
|
|
516
548
|
/**
|
|
517
549
|
* Name-based recursive redaction.
|
|
@@ -885,8 +917,10 @@ declare class PrettyDevDestination implements ILogDestination {
|
|
|
885
917
|
*
|
|
886
918
|
* Three states:
|
|
887
919
|
* - initialized → forward to the pino-pretty transform;
|
|
888
|
-
* - init failed → DROP
|
|
889
|
-
*
|
|
920
|
+
* - init failed → DROP, so a co-registered stdout sink is not duplicated. The
|
|
921
|
+
* fan-out already skips a failed destination before reaching this method
|
|
922
|
+
* (and rescues the entry when nothing else initialized); this branch is the
|
|
923
|
+
* belt to that, for any caller holding the destination directly;
|
|
890
924
|
* - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
|
|
891
925
|
* → fall back to raw stdout so the entry is not lost.
|
|
892
926
|
*
|
|
@@ -1287,6 +1321,17 @@ interface LogEntry {
|
|
|
1287
1321
|
msg: string;
|
|
1288
1322
|
/** Convention key — `MODULE_ACTION_RESULT` format. */
|
|
1289
1323
|
logKey?: string;
|
|
1324
|
+
/**
|
|
1325
|
+
* OTel event name derived from {@link logKey} (`PAYMENT_FAILED` →
|
|
1326
|
+
* `payment.failed`), emitted on structured calls unless `eventNameField` is
|
|
1327
|
+
* `false`. Declared explicitly because the index signature below would type
|
|
1328
|
+
* it `unknown`: a documented field a consumer cannot read without a cast is
|
|
1329
|
+
* not really published.
|
|
1330
|
+
*
|
|
1331
|
+
* The key is configurable, so a renamed field arrives through the index
|
|
1332
|
+
* signature instead — this declaration describes the default only.
|
|
1333
|
+
*/
|
|
1334
|
+
'event.name'?: string;
|
|
1290
1335
|
/**
|
|
1291
1336
|
* Service identity as EMITTED — see {@link EmittedServiceResource}. Not the
|
|
1292
1337
|
* configuration shape: `service.instance.id` nests, and the environment lives
|
|
@@ -1337,4 +1382,4 @@ interface LogEntry {
|
|
|
1337
1382
|
*/
|
|
1338
1383
|
declare const LOG_KEYS_CONVENTION_REGEX: RegExp;
|
|
1339
1384
|
|
|
1340
|
-
export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ResolvedBymaxLoggerModuleOptions, type ServiceMetadata, applyRequestIdMiddleware };
|
|
1385
|
+
export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, applyRequestIdMiddleware };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -125,6 +125,26 @@ interface ServiceMetadata {
|
|
|
125
125
|
*/
|
|
126
126
|
readonly environment?: string;
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Service identity after precedence has been applied.
|
|
130
|
+
*
|
|
131
|
+
* Distinct from {@link ServiceMetadata} because the optional fields here mean
|
|
132
|
+
* "resolved to nothing" rather than "not configured": every source — options,
|
|
133
|
+
* `OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`, `NODE_ENV` — has already been
|
|
134
|
+
* consulted, and a field still absent will not appear on any entry.
|
|
135
|
+
*/
|
|
136
|
+
interface ResolvedServiceMetadata {
|
|
137
|
+
/** `service.name`, always present. */
|
|
138
|
+
name: string;
|
|
139
|
+
/** `service.version`, always present. */
|
|
140
|
+
version: string;
|
|
141
|
+
/** `service.namespace`, when any source supplied one. */
|
|
142
|
+
namespace?: string;
|
|
143
|
+
/** `service.instance.id`, when any source supplied one. */
|
|
144
|
+
instanceId?: string;
|
|
145
|
+
/** `deployment.environment.name`, when any source supplied one. */
|
|
146
|
+
environment?: string;
|
|
147
|
+
}
|
|
128
148
|
/**
|
|
129
149
|
* The `service` block as it appears in an EMITTED record, under the default
|
|
130
150
|
* `resourceFormat: 'nested'`.
|
|
@@ -314,15 +334,22 @@ interface BymaxLoggerModuleOptions {
|
|
|
314
334
|
redactCensor?: string;
|
|
315
335
|
/** Disable default redact paths (use with caution). Default: false. */
|
|
316
336
|
shouldDisableDefaultRedact?: boolean;
|
|
317
|
-
/** Custom destinations beyond `DefaultStdoutDestination`. */
|
|
318
|
-
destinations?: readonly ILogDestination[];
|
|
319
337
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
338
|
+
* The sinks every entry is written to.
|
|
339
|
+
*
|
|
340
|
+
* A non-empty list **REPLACES** `DefaultStdoutDestination` — it does not add to
|
|
341
|
+
* it. That is deliberate: a file-only or socket-only deployment has to be able
|
|
342
|
+
* to turn stdout off. It also means a sink supplied here may be the only one
|
|
343
|
+
* the application has, so a destination that fails `onInit` is reported on
|
|
344
|
+
* stderr and, if nothing else initialized, entries fall back to raw NDJSON on
|
|
345
|
+
* stdout rather than disappearing.
|
|
346
|
+
*
|
|
347
|
+
* Include `new DefaultStdoutDestination()` explicitly to keep structured stdout
|
|
348
|
+
* alongside a custom sink.
|
|
349
|
+
*
|
|
350
|
+
* Default: `[new DefaultStdoutDestination()]`.
|
|
324
351
|
*/
|
|
325
|
-
|
|
352
|
+
destinations?: readonly ILogDestination[];
|
|
326
353
|
/** HTTP module configuration. */
|
|
327
354
|
http?: HttpOptions;
|
|
328
355
|
/** OpenTelemetry integration tuning. */
|
|
@@ -512,6 +539,11 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
|
|
|
512
539
|
*/
|
|
513
540
|
readonly HTTP_REQUEST_COMPLETED: "reserved: superseded by the four status-specific terminal keys";
|
|
514
541
|
}>;
|
|
542
|
+
/**
|
|
543
|
+
* Strongly-typed union of every reserved log key value, derived from
|
|
544
|
+
* {@link RESERVED_LOG_KEYS}. Use this type to constrain log-key arguments.
|
|
545
|
+
*/
|
|
546
|
+
type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
|
|
515
547
|
|
|
516
548
|
/**
|
|
517
549
|
* Name-based recursive redaction.
|
|
@@ -885,8 +917,10 @@ declare class PrettyDevDestination implements ILogDestination {
|
|
|
885
917
|
*
|
|
886
918
|
* Three states:
|
|
887
919
|
* - initialized → forward to the pino-pretty transform;
|
|
888
|
-
* - init failed → DROP
|
|
889
|
-
*
|
|
920
|
+
* - init failed → DROP, so a co-registered stdout sink is not duplicated. The
|
|
921
|
+
* fan-out already skips a failed destination before reaching this method
|
|
922
|
+
* (and rescues the entry when nothing else initialized); this branch is the
|
|
923
|
+
* belt to that, for any caller holding the destination directly;
|
|
890
924
|
* - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
|
|
891
925
|
* → fall back to raw stdout so the entry is not lost.
|
|
892
926
|
*
|
|
@@ -1287,6 +1321,17 @@ interface LogEntry {
|
|
|
1287
1321
|
msg: string;
|
|
1288
1322
|
/** Convention key — `MODULE_ACTION_RESULT` format. */
|
|
1289
1323
|
logKey?: string;
|
|
1324
|
+
/**
|
|
1325
|
+
* OTel event name derived from {@link logKey} (`PAYMENT_FAILED` →
|
|
1326
|
+
* `payment.failed`), emitted on structured calls unless `eventNameField` is
|
|
1327
|
+
* `false`. Declared explicitly because the index signature below would type
|
|
1328
|
+
* it `unknown`: a documented field a consumer cannot read without a cast is
|
|
1329
|
+
* not really published.
|
|
1330
|
+
*
|
|
1331
|
+
* The key is configurable, so a renamed field arrives through the index
|
|
1332
|
+
* signature instead — this declaration describes the default only.
|
|
1333
|
+
*/
|
|
1334
|
+
'event.name'?: string;
|
|
1290
1335
|
/**
|
|
1291
1336
|
* Service identity as EMITTED — see {@link EmittedServiceResource}. Not the
|
|
1292
1337
|
* configuration shape: `service.instance.id` nests, and the environment lives
|
|
@@ -1337,4 +1382,4 @@ interface LogEntry {
|
|
|
1337
1382
|
*/
|
|
1338
1383
|
declare const LOG_KEYS_CONVENTION_REGEX: RegExp;
|
|
1339
1384
|
|
|
1340
|
-
export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ResolvedBymaxLoggerModuleOptions, type ServiceMetadata, applyRequestIdMiddleware };
|
|
1385
|
+
export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, applyRequestIdMiddleware };
|
package/dist/server/index.mjs
CHANGED
|
@@ -73,7 +73,6 @@ function applyDefaults(options) {
|
|
|
73
73
|
redactCensor: options.redactCensor ?? "[REDACTED]",
|
|
74
74
|
shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
|
|
75
75
|
destinations: Object.freeze([...options.destinations ?? []]),
|
|
76
|
-
isPretty: options.isPretty ?? !isProduction,
|
|
77
76
|
http: Object.freeze({
|
|
78
77
|
...http,
|
|
79
78
|
excludePaths: Object.freeze([...http.excludePaths])
|
|
@@ -1018,22 +1017,43 @@ function compileRedactPaths(extraPaths, shouldSkipDefaults) {
|
|
|
1018
1017
|
const sources = shouldSkipDefaults ? [extraPaths] : [DEFAULT_REDACT_PATHS, extraPaths];
|
|
1019
1018
|
return Array.from(new Set(sources.flat()));
|
|
1020
1019
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1020
|
+
|
|
1021
|
+
// src/server/utils/report-destination-failure.util.ts
|
|
1022
|
+
function reportDestinationFailure(logKey, name, cause, msg) {
|
|
1023
1023
|
try {
|
|
1024
|
+
const err = cause instanceof Error ? {
|
|
1025
|
+
type: toSingleLineMessage(String(cause.name)),
|
|
1026
|
+
message: toSingleLineMessage(String(cause.message))
|
|
1027
|
+
} : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
|
|
1024
1028
|
process.stderr.write(
|
|
1025
1029
|
JSON.stringify({
|
|
1026
1030
|
level: "error",
|
|
1027
|
-
logKey
|
|
1028
|
-
destination: name,
|
|
1031
|
+
logKey,
|
|
1032
|
+
destination: toSingleLineMessage(name),
|
|
1029
1033
|
err,
|
|
1030
|
-
msg:
|
|
1034
|
+
msg: toSingleLineMessage(msg)
|
|
1031
1035
|
}) + "\n"
|
|
1032
1036
|
);
|
|
1033
1037
|
} catch {
|
|
1034
1038
|
}
|
|
1035
1039
|
}
|
|
1036
|
-
|
|
1040
|
+
|
|
1041
|
+
// src/server/utils/destination-to-stream.ts
|
|
1042
|
+
function reportWriteFailure(name, cause) {
|
|
1043
|
+
reportDestinationFailure(
|
|
1044
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_WRITE_FAILED,
|
|
1045
|
+
name,
|
|
1046
|
+
cause,
|
|
1047
|
+
`Log destination "${name}" failed to write; the entry was dropped`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
function rescueToStdout(payload) {
|
|
1051
|
+
try {
|
|
1052
|
+
process.stdout.write(payload);
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function destinationToStream(destination, health) {
|
|
1037
1057
|
return new Writable({
|
|
1038
1058
|
// Keep string chunks as strings (Node's default re-encodes them to Buffers
|
|
1039
1059
|
// before `_write`): the common Pino path emits NDJSON strings, so this skips
|
|
@@ -1047,6 +1067,13 @@ function destinationToStream(destination) {
|
|
|
1047
1067
|
};
|
|
1048
1068
|
try {
|
|
1049
1069
|
const payload = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
1070
|
+
if (health.isFailed(destination)) {
|
|
1071
|
+
if (health.shouldRescue(destination)) {
|
|
1072
|
+
rescueToStdout(payload);
|
|
1073
|
+
}
|
|
1074
|
+
callback();
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1050
1077
|
const result = destination.write(payload);
|
|
1051
1078
|
if (result instanceof Promise) {
|
|
1052
1079
|
result.then(() => callback(), onFailure);
|
|
@@ -1436,7 +1463,7 @@ function resolveRedactOption(options) {
|
|
|
1436
1463
|
}
|
|
1437
1464
|
return { paths, censor: options.redactCensor };
|
|
1438
1465
|
}
|
|
1439
|
-
function buildPinoInstance(options, logContext, destinations) {
|
|
1466
|
+
function buildPinoInstance(options, logContext, destinations, health) {
|
|
1440
1467
|
const maxBytes = options.maxEntrySizeBytes;
|
|
1441
1468
|
const redact = resolveNameRedactor(options);
|
|
1442
1469
|
const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
|
|
@@ -1508,55 +1535,162 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1508
1535
|
};
|
|
1509
1536
|
const streams = destinations.map((destination) => ({
|
|
1510
1537
|
level: destination.minLevel ?? options.level,
|
|
1511
|
-
stream: destinationToStream(destination)
|
|
1538
|
+
stream: destinationToStream(destination, health)
|
|
1512
1539
|
}));
|
|
1513
1540
|
return pino(pinoOptions, pino.multistream(streams));
|
|
1514
1541
|
}
|
|
1542
|
+
var DestinationHealth = class {
|
|
1543
|
+
constructor() {
|
|
1544
|
+
/** Destinations whose `onInit()` rejected. Identity-keyed, not name-keyed. */
|
|
1545
|
+
this.failed = /* @__PURE__ */ new Set();
|
|
1546
|
+
/**
|
|
1547
|
+
* Whether ANY destination initialized successfully.
|
|
1548
|
+
*
|
|
1549
|
+
* A boolean rather than a count, deliberately: the only question ever asked of
|
|
1550
|
+
* it is "is there somewhere to write?". Mutation testing made the point — a
|
|
1551
|
+
* counter incremented with `+=` survived being flipped to `-=`, because
|
|
1552
|
+
* `count === 0` means the same thing either way. A value whose arithmetic
|
|
1553
|
+
* cannot be observed was not a counter, it was a flag wearing a number.
|
|
1554
|
+
*/
|
|
1555
|
+
this.hasHealthy = false;
|
|
1556
|
+
/**
|
|
1557
|
+
* The rescuer's severity index within {@link LOG_LEVEL_PRIORITY}. Starts above
|
|
1558
|
+
* every real level so the first `markFailed` always wins the comparison.
|
|
1559
|
+
*/
|
|
1560
|
+
this.rescuerLevel = Number.POSITIVE_INFINITY;
|
|
1561
|
+
}
|
|
1562
|
+
/** Record that a destination initialized successfully. */
|
|
1563
|
+
markHealthy() {
|
|
1564
|
+
this.hasHealthy = true;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Record that a destination failed to initialize, and re-elect the rescuer.
|
|
1568
|
+
*
|
|
1569
|
+
* The rescuer is the failed destination with the LOWEST effective level, ties
|
|
1570
|
+
* broken by registration order (the comparison is strict). Registration order
|
|
1571
|
+
* alone is deterministic but arbitrary: a `warn` sink registered before an
|
|
1572
|
+
* `info` one would rescue at `warn` and silently drop the `info` entries the
|
|
1573
|
+
* second was configured to receive, and swapping the array would change the
|
|
1574
|
+
* output for the same configuration. Electing by level rescues the UNION of
|
|
1575
|
+
* what the consumer asked to see.
|
|
1576
|
+
*
|
|
1577
|
+
* Inheriting a level filter at all is deliberate — `minLevel` is the consumer's
|
|
1578
|
+
* own instruction, so the rescue delivers what a working sink would have, never
|
|
1579
|
+
* more. The one entry that must never be filtered, the init failure itself, goes
|
|
1580
|
+
* straight to stderr, outside the multistream.
|
|
1581
|
+
*
|
|
1582
|
+
* @param destination - The destination whose `onInit()` rejected.
|
|
1583
|
+
* @param effectiveLevel - Its multistream level: `minLevel` when set, otherwise
|
|
1584
|
+
* the module-wide `level`.
|
|
1585
|
+
*/
|
|
1586
|
+
markFailed(destination, effectiveLevel) {
|
|
1587
|
+
this.failed.add(destination);
|
|
1588
|
+
const level = LOG_LEVEL_PRIORITY.indexOf(effectiveLevel);
|
|
1589
|
+
if (level < this.rescuerLevel) {
|
|
1590
|
+
this.rescuer = destination;
|
|
1591
|
+
this.rescuerLevel = level;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Whether a destination failed to initialize and must not receive writes.
|
|
1596
|
+
*
|
|
1597
|
+
* @param destination - The destination backing a fan-out stream.
|
|
1598
|
+
* @returns `true` when its `onInit()` rejected.
|
|
1599
|
+
*/
|
|
1600
|
+
isFailed(destination) {
|
|
1601
|
+
return this.failed.has(destination);
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Whether this destination is the one that must write the entry as raw NDJSON
|
|
1605
|
+
* to stdout because NOTHING initialized.
|
|
1606
|
+
*
|
|
1607
|
+
* Exactly one failed destination can satisfy this, so N failed sinks rescue an
|
|
1608
|
+
* entry once rather than N times.
|
|
1609
|
+
*
|
|
1610
|
+
* @param destination - The destination backing a fan-out stream.
|
|
1611
|
+
* @returns `true` when no destination initialized and this one is the elected
|
|
1612
|
+
* rescuer.
|
|
1613
|
+
*/
|
|
1614
|
+
shouldRescue(destination) {
|
|
1615
|
+
return !this.hasHealthy && this.rescuer === destination;
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
DestinationHealth = __decorateClass([
|
|
1619
|
+
Injectable()
|
|
1620
|
+
], DestinationHealth);
|
|
1515
1621
|
var DestinationRegistry = class {
|
|
1516
1622
|
/**
|
|
1517
1623
|
* @param registered - Every destination declared under
|
|
1518
1624
|
* `LOGGER_DESTINATIONS_TOKEN`. Injected by token explicitly because the
|
|
1519
1625
|
* package is bundled without `emitDecoratorMetadata`.
|
|
1520
|
-
* @param logger - Structured logger used
|
|
1521
|
-
* token explicitly for the same bundling reason.
|
|
1626
|
+
* @param logger - Structured logger used for the bootstrap entries. Injected by
|
|
1627
|
+
* token explicitly for the same bundling reason. Deliberately NOT used to
|
|
1628
|
+
* report init failures — see the file-level note.
|
|
1629
|
+
* @param options - Resolved module options, read for the bootstrap warnings and
|
|
1630
|
+
* for each destination's effective level.
|
|
1631
|
+
* @param health - Init-health record shared with the write fan-out.
|
|
1522
1632
|
*/
|
|
1523
|
-
constructor(registered, logger, options) {
|
|
1633
|
+
constructor(registered, logger, options, health) {
|
|
1524
1634
|
this.registered = registered;
|
|
1525
1635
|
this.logger = logger;
|
|
1526
1636
|
this.options = options;
|
|
1637
|
+
this.health = health;
|
|
1527
1638
|
/**
|
|
1528
1639
|
* Destinations that initialized successfully, in registration order.
|
|
1529
1640
|
*
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1534
|
-
* write fan-out; a sink that failed `onInit` still receives writes, contained
|
|
1535
|
-
* fail-soft by the wrapper.
|
|
1641
|
+
* Authoritative for BOTH shutdown (reverse order) and — via the shared
|
|
1642
|
+
* `DestinationHealth` record written alongside it — the write fan-out. The two
|
|
1643
|
+
* are kept in step here because the multistream itself is wired before any
|
|
1644
|
+
* `onInit` runs and cannot be rebuilt afterwards; see `DestinationHealth`.
|
|
1536
1645
|
*/
|
|
1537
1646
|
this.active = [];
|
|
1538
1647
|
}
|
|
1539
1648
|
/**
|
|
1540
1649
|
* Initialize every registered destination. A destination whose `onInit`
|
|
1541
|
-
* rejects is skipped (not added to the active set)
|
|
1542
|
-
*
|
|
1650
|
+
* rejects is skipped (not added to the active set), marked failed so the
|
|
1651
|
+
* fan-out stops writing to it, and reported to stderr — it must not block
|
|
1652
|
+
* application bootstrap.
|
|
1653
|
+
*
|
|
1654
|
+
* The loop completes BEFORE {@link announceBootstrap}, and that ordering is
|
|
1655
|
+
* load-bearing rather than incidental: the health record is what lets a
|
|
1656
|
+
* last-resort sink carry the bootstrap entries when every destination failed.
|
|
1657
|
+
* Announcing first would emit them into a fan-out that has not yet been told
|
|
1658
|
+
* anything is wrong, and `LOGGER_BOOTSTRAP_WARNING` — the signal that exists so
|
|
1659
|
+
* a security review can see PII redaction was disabled — would be lost exactly
|
|
1660
|
+
* when the configuration is already known to be broken.
|
|
1543
1661
|
*/
|
|
1544
1662
|
async onModuleInit() {
|
|
1545
1663
|
for (const destination of this.registered) {
|
|
1546
1664
|
try {
|
|
1547
1665
|
await destination.onInit?.();
|
|
1548
1666
|
this.active.push(destination);
|
|
1667
|
+
this.health.markHealthy();
|
|
1549
1668
|
} catch (cause) {
|
|
1550
|
-
this.
|
|
1551
|
-
|
|
1552
|
-
cause instanceof Error ? cause : new Error(String(cause)),
|
|
1553
|
-
void 0,
|
|
1554
|
-
{ destination: destination.name }
|
|
1555
|
-
);
|
|
1669
|
+
this.health.markFailed(destination, destination.minLevel ?? this.options.level);
|
|
1670
|
+
this.reportInitFailure(destination.name, cause);
|
|
1556
1671
|
}
|
|
1557
1672
|
}
|
|
1558
1673
|
this.announceBootstrap();
|
|
1559
1674
|
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Report one destination init failure as a structured
|
|
1677
|
+
* `LOGGER_DESTINATION_INIT_FAILED` line on `process.stderr`.
|
|
1678
|
+
*
|
|
1679
|
+
* Shares {@link reportDestinationFailure} with the write-failure path rather
|
|
1680
|
+
* than restating the shape, so an operator greps one `logKey` regardless of
|
|
1681
|
+
* which stage failed — and the two cannot drift apart.
|
|
1682
|
+
*
|
|
1683
|
+
* @param name - The failing destination's name.
|
|
1684
|
+
* @param cause - The thrown or rejected value.
|
|
1685
|
+
*/
|
|
1686
|
+
reportInitFailure(name, cause) {
|
|
1687
|
+
reportDestinationFailure(
|
|
1688
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
|
|
1689
|
+
name,
|
|
1690
|
+
cause,
|
|
1691
|
+
`Log destination "${name}" failed to initialize and will receive no entries. If no destination initializes, entries fall back to raw NDJSON on stdout.`
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1560
1694
|
/**
|
|
1561
1695
|
* Emit the one-shot bootstrap entries, AFTER every destination has been
|
|
1562
1696
|
* initialized.
|
|
@@ -1634,7 +1768,8 @@ DestinationRegistry = __decorateClass([
|
|
|
1634
1768
|
Injectable(),
|
|
1635
1769
|
__decorateParam(0, Inject(LOGGER_DESTINATIONS_TOKEN)),
|
|
1636
1770
|
__decorateParam(1, Inject(PinoLoggerService)),
|
|
1637
|
-
__decorateParam(2, Inject(LOGGER_OPTIONS_TOKEN))
|
|
1771
|
+
__decorateParam(2, Inject(LOGGER_OPTIONS_TOKEN)),
|
|
1772
|
+
__decorateParam(3, Inject(DestinationHealth))
|
|
1638
1773
|
], DestinationRegistry);
|
|
1639
1774
|
var FORBIDDEN_CONTEXT_KEYS = PROTOTYPE_POLLUTING_KEYS;
|
|
1640
1775
|
var LogContextService = class {
|
|
@@ -1729,6 +1864,10 @@ function buildCommonProviders() {
|
|
|
1729
1864
|
return [
|
|
1730
1865
|
LogContextService,
|
|
1731
1866
|
{ provide: LOG_CONTEXT_TOKEN, useExisting: LogContextService },
|
|
1867
|
+
// Shared by the Pino factory (which closes over it in every fan-out stream)
|
|
1868
|
+
// and by DestinationRegistry (which writes to it during onModuleInit). Both
|
|
1869
|
+
// depend on this provider and it depends on nothing, so there is no cycle.
|
|
1870
|
+
DestinationHealth,
|
|
1732
1871
|
{
|
|
1733
1872
|
// The DEFAULT-coverage redactor, shared by the Pino factory's hooks and by
|
|
1734
1873
|
// `PinoLoggerService.child()`. The latter needs it as a provider because
|
|
@@ -1739,8 +1878,13 @@ function buildCommonProviders() {
|
|
|
1739
1878
|
},
|
|
1740
1879
|
{
|
|
1741
1880
|
provide: LOGGER_PINO_INSTANCE_TOKEN,
|
|
1742
|
-
useFactory: (options, logContext, destinations) => buildPinoInstance(options, logContext, destinations),
|
|
1743
|
-
inject: [
|
|
1881
|
+
useFactory: (options, logContext, destinations, health) => buildPinoInstance(options, logContext, destinations, health),
|
|
1882
|
+
inject: [
|
|
1883
|
+
LOGGER_OPTIONS_TOKEN,
|
|
1884
|
+
LogContextService,
|
|
1885
|
+
LOGGER_DESTINATIONS_TOKEN,
|
|
1886
|
+
DestinationHealth
|
|
1887
|
+
]
|
|
1744
1888
|
},
|
|
1745
1889
|
{
|
|
1746
1890
|
provide: LOGGER_DESTINATIONS_TOKEN,
|
|
@@ -1931,8 +2075,10 @@ var PrettyDevDestination = class {
|
|
|
1931
2075
|
*
|
|
1932
2076
|
* Three states:
|
|
1933
2077
|
* - initialized → forward to the pino-pretty transform;
|
|
1934
|
-
* - init failed → DROP
|
|
1935
|
-
*
|
|
2078
|
+
* - init failed → DROP, so a co-registered stdout sink is not duplicated. The
|
|
2079
|
+
* fan-out already skips a failed destination before reaching this method
|
|
2080
|
+
* (and rescues the entry when nothing else initialized); this branch is the
|
|
2081
|
+
* belt to that, for any caller holding the destination directly;
|
|
1936
2082
|
* - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
|
|
1937
2083
|
* → fall back to raw stdout so the entry is not lost.
|
|
1938
2084
|
*
|
package/dist/shared/index.d.cts
CHANGED
|
@@ -152,6 +152,17 @@ interface LogEntry {
|
|
|
152
152
|
msg: string;
|
|
153
153
|
/** Convention key — `MODULE_ACTION_RESULT` format. */
|
|
154
154
|
logKey?: string;
|
|
155
|
+
/**
|
|
156
|
+
* OTel event name derived from {@link logKey} (`PAYMENT_FAILED` →
|
|
157
|
+
* `payment.failed`), emitted on structured calls unless `eventNameField` is
|
|
158
|
+
* `false`. Declared explicitly because the index signature below would type
|
|
159
|
+
* it `unknown`: a documented field a consumer cannot read without a cast is
|
|
160
|
+
* not really published.
|
|
161
|
+
*
|
|
162
|
+
* The key is configurable, so a renamed field arrives through the index
|
|
163
|
+
* signature instead — this declaration describes the default only.
|
|
164
|
+
*/
|
|
165
|
+
'event.name'?: string;
|
|
155
166
|
/**
|
|
156
167
|
* Service identity as EMITTED — see {@link EmittedServiceResource}. Not the
|
|
157
168
|
* configuration shape: `service.instance.id` nests, and the environment lives
|
|
@@ -258,4 +269,4 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
|
|
|
258
269
|
*/
|
|
259
270
|
type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
|
|
260
271
|
|
|
261
|
-
export { LOG_KEYS_CONVENTION_REGEX, type LogEntry, type LogLevel, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, type ReservedLogKey, type ResolvedServiceMetadata, type ServiceMetadata };
|
|
272
|
+
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 };
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -152,6 +152,17 @@ interface LogEntry {
|
|
|
152
152
|
msg: string;
|
|
153
153
|
/** Convention key — `MODULE_ACTION_RESULT` format. */
|
|
154
154
|
logKey?: string;
|
|
155
|
+
/**
|
|
156
|
+
* OTel event name derived from {@link logKey} (`PAYMENT_FAILED` →
|
|
157
|
+
* `payment.failed`), emitted on structured calls unless `eventNameField` is
|
|
158
|
+
* `false`. Declared explicitly because the index signature below would type
|
|
159
|
+
* it `unknown`: a documented field a consumer cannot read without a cast is
|
|
160
|
+
* not really published.
|
|
161
|
+
*
|
|
162
|
+
* The key is configurable, so a renamed field arrives through the index
|
|
163
|
+
* signature instead — this declaration describes the default only.
|
|
164
|
+
*/
|
|
165
|
+
'event.name'?: string;
|
|
155
166
|
/**
|
|
156
167
|
* Service identity as EMITTED — see {@link EmittedServiceResource}. Not the
|
|
157
168
|
* configuration shape: `service.instance.id` nests, and the environment lives
|
|
@@ -258,4 +269,4 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
|
|
|
258
269
|
*/
|
|
259
270
|
type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
|
|
260
271
|
|
|
261
|
-
export { LOG_KEYS_CONVENTION_REGEX, type LogEntry, type LogLevel, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, type ReservedLogKey, type ResolvedServiceMetadata, type ServiceMetadata };
|
|
272
|
+
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-logger",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"test:cov:all": "jest --config jest.coverage.config.ts --coverage",
|
|
76
76
|
"test:e2e": "jest --config jest.e2e.config.ts",
|
|
77
77
|
"test:watch": "jest --watch",
|
|
78
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json"
|
|
78
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.bench.json"
|
|
79
79
|
},
|
|
80
80
|
"lint-staged": {
|
|
81
81
|
"*.{ts,tsx,js,mjs,cjs}": [
|
|
@@ -86,7 +86,6 @@
|
|
|
86
86
|
"prettier --write"
|
|
87
87
|
]
|
|
88
88
|
},
|
|
89
|
-
"dependencies": {},
|
|
90
89
|
"peerDependencies": {
|
|
91
90
|
"@nestjs/common": "^11.0.16",
|
|
92
91
|
"@nestjs/core": "^11.1.18",
|