@bymax-one/nest-logger 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/README.md +797 -0
- package/dist/server/index.cjs +1470 -0
- package/dist/server/index.d.cts +953 -0
- package/dist/server/index.d.ts +953 -0
- package/dist/server/index.mjs +1452 -0
- package/dist/shared/index.cjs +27 -0
- package/dist/shared/index.d.cts +120 -0
- package/dist/shared/index.d.ts +120 -0
- package/dist/shared/index.mjs +24 -0
- package/package.json +141 -0
package/README.md
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/badge/%40bymax--one-nest--logger-000000?style=for-the-badge&logo=nestjs&logoColor=E0234E" alt="@bymax-one/nest-logger" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@bymax-one/nest-logger</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Structured JSON logging for NestJS</strong><br />
|
|
9
|
+
<sub>Pino 10 · OpenTelemetry · PII Redaction · Multi-Destination · Multi-Tenant · Zero Runtime Dependencies</sub>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@bymax-one/nest-logger"><img src="https://img.shields.io/npm/v/@bymax-one/nest-logger?style=flat-square&colorA=000000&colorB=000000" alt="npm version" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@bymax-one/nest-logger"><img src="https://img.shields.io/npm/dm/@bymax-one/nest-logger?style=flat-square&colorA=000000&colorB=000000" alt="npm downloads" /></a>
|
|
15
|
+
<a href="https://github.com/bymaxone/nest-logger/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/bymaxone/nest-logger/ci.yml?branch=main&style=flat-square&colorA=000000&label=CI" alt="CI status" /></a>
|
|
16
|
+
<a href="https://github.com/bymaxone/nest-logger/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square&colorA=000000" alt="coverage" /></a>
|
|
17
|
+
<a href="https://github.com/bymaxone/nest-logger/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-97.42%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
|
|
18
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/bymaxone/nest-logger"><img src="https://api.scorecard.dev/projects/github.com/bymaxone/nest-logger/badge?style=flat-square" alt="OpenSSF Scorecard" /></a>
|
|
19
|
+
<a href="https://github.com/bymaxone/nest-logger/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bymaxone/nest-logger?style=flat-square&colorA=000000&colorB=000000" alt="license" /></a>
|
|
20
|
+
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
|
21
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-24%2B-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js" /></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<a href="https://github.com/bymaxone/nest-logger">GitHub</a> ·
|
|
26
|
+
<a href="https://github.com/bymaxone/nest-logger/issues">Issues</a> ·
|
|
27
|
+
<a href="#-quick-start">Quick Start</a> ·
|
|
28
|
+
<a href="#-api-reference">API Reference</a> ·
|
|
29
|
+
<a href="https://github.com/bymaxone/nest-logger-example">Example App</a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## ✨ Overview
|
|
35
|
+
|
|
36
|
+
`@bymax-one/nest-logger` replaces ad-hoc `console.log` and legacy Winston setups with a production-grade structured logging pipeline. Every log entry is a JSON object carrying a `logKey` (`MODULE_ACTION_RESULT` convention), `requestId`, `tenantId`, and — when OpenTelemetry is active — the correlated `traceId`/`spanId`.
|
|
37
|
+
|
|
38
|
+
**Why Pino?** At ~750,000 logs/sec (vs Winston's ~110,000), Pino consumes 3× less CPU and half the RSS under load. The difference is measurable in production billing/payments backends where the logger is on the hot path for every request.
|
|
39
|
+
|
|
40
|
+
The library has **zero direct dependencies** — all packages arrive as peer dependencies, so you control exact versions and the supply chain surface stays minimal.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🔥 Features
|
|
45
|
+
|
|
46
|
+
- **Structured JSON** — every entry has `level`, `time`, `service`, `logKey`, `msg`, and arbitrary metadata fields
|
|
47
|
+
- **PII redaction by default** — 113 paths covering passwords, tokens, generic secrets, PCI DSS card data, MFA secrets, CPF/CNPJ/RG (LGPD), and common HTTP headers — powered by `fast-redact`
|
|
48
|
+
- **OpenTelemetry correlation** — optional `@opentelemetry/api` peer; injects `traceId`/`spanId`/`traceFlags` into every log via a Pino mixin when an active span is detected
|
|
49
|
+
- **AsyncLocalStorage context** — `requestId`, `tenantId`, `userId` flow automatically through the request lifecycle without prop drilling
|
|
50
|
+
- **HTTP logging interceptor** — auto-logs all HTTP requests/responses with URL normalization (UUIDs and numeric IDs replaced by `:id` placeholder)
|
|
51
|
+
- **Exception filter** — captures NestJS `HttpException` and unexpected errors with structured output
|
|
52
|
+
- **Pluggable destinations** — implement `ILogDestination` to ship logs to Loki, Postgres, rolling files, or any sink
|
|
53
|
+
- **NestJS `LoggerService` bridge** — drop-in replacement; all NestJS internal logs flow through Pino
|
|
54
|
+
- **`MODULE_ACTION_RESULT` log key convention** — enforced by an exported regex for CI validation
|
|
55
|
+
- **Pretty-print in dev** — auto-enabled when `NODE_ENV !== 'production'` (requires optional `pino-pretty`)
|
|
56
|
+
- **Entry size guard** — truncates oversized entries (default 64 KB) with a structured warning instead of silently dropping them
|
|
57
|
+
- **Zero-downtime destination lifecycle** — `onInit()` / `onShutdown()` hooks; a failing destination is removed without affecting others
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 📦 Subpath Exports
|
|
62
|
+
|
|
63
|
+
The package ships two entry points:
|
|
64
|
+
|
|
65
|
+
| Import path | Contents | Deps |
|
|
66
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
|
|
67
|
+
| `@bymax-one/nest-logger` | Full server API: `BymaxLoggerModule`, `PinoLoggerService`, `LogContextService`, destinations, decorators, interceptors | NestJS + Pino peer deps |
|
|
68
|
+
| `@bymax-one/nest-logger/shared` | Zero-dependency utilities: `LOG_KEYS_CONVENTION_REGEX`, `RESERVED_LOG_KEYS`, types (`LogLevel`, `LogEntry`, `ServiceMetadata`) | None |
|
|
69
|
+
|
|
70
|
+
The `/shared` subpath is safe to import in isomorphic code, test helpers, CLI scripts, or shared packages that must not pull in NestJS.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 📥 Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Install the library
|
|
78
|
+
pnpm add @bymax-one/nest-logger
|
|
79
|
+
|
|
80
|
+
# Required peer dependencies
|
|
81
|
+
pnpm add pino reflect-metadata
|
|
82
|
+
|
|
83
|
+
# Optional — pretty dev output
|
|
84
|
+
pnpm add -D pino-pretty
|
|
85
|
+
|
|
86
|
+
# Optional — OpenTelemetry correlation
|
|
87
|
+
pnpm add @opentelemetry/api @opentelemetry/sdk-node
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 🚀 Quick Start
|
|
93
|
+
|
|
94
|
+
### Step 1 — Register the module
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// app.module.ts
|
|
98
|
+
import { Module } from '@nestjs/common'
|
|
99
|
+
import { BymaxLoggerModule } from '@bymax-one/nest-logger'
|
|
100
|
+
|
|
101
|
+
@Module({
|
|
102
|
+
imports: [
|
|
103
|
+
BymaxLoggerModule.forRoot({
|
|
104
|
+
service: { name: 'my-app', version: '1.0.0' },
|
|
105
|
+
level: 'info',
|
|
106
|
+
http: { isEnabled: true }
|
|
107
|
+
})
|
|
108
|
+
]
|
|
109
|
+
})
|
|
110
|
+
export class AppModule {}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`isPretty` defaults to `true` when `NODE_ENV !== 'production'` — no extra config needed in development.
|
|
114
|
+
|
|
115
|
+
### Step 2 — Async configuration with `ConfigService`
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// app.module.ts
|
|
119
|
+
import { Module } from '@nestjs/common'
|
|
120
|
+
import { ConfigModule, ConfigService } from '@nestjs/config'
|
|
121
|
+
import { BymaxLoggerModule } from '@bymax-one/nest-logger'
|
|
122
|
+
|
|
123
|
+
@Module({
|
|
124
|
+
imports: [
|
|
125
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
126
|
+
BymaxLoggerModule.forRootAsync({
|
|
127
|
+
imports: [ConfigModule],
|
|
128
|
+
inject: [ConfigService],
|
|
129
|
+
useFactory: (cfg: ConfigService) => ({
|
|
130
|
+
service: {
|
|
131
|
+
name: cfg.getOrThrow<string>('OTEL_SERVICE_NAME'),
|
|
132
|
+
version: cfg.getOrThrow<string>('RELEASE_SHA')
|
|
133
|
+
},
|
|
134
|
+
level: cfg.get('LOG_LEVEL') ?? 'info',
|
|
135
|
+
http: { isEnabled: true }
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
})
|
|
140
|
+
export class AppModule {}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Step 3 — Inject the logger in a service
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// payments.service.ts
|
|
147
|
+
import { Injectable } from '@nestjs/common'
|
|
148
|
+
import { InjectLogger, PinoLoggerService } from '@bymax-one/nest-logger'
|
|
149
|
+
|
|
150
|
+
@Injectable()
|
|
151
|
+
export class PaymentsService {
|
|
152
|
+
constructor(
|
|
153
|
+
@InjectLogger(PaymentsService.name)
|
|
154
|
+
private readonly logger: PinoLoggerService
|
|
155
|
+
) {}
|
|
156
|
+
|
|
157
|
+
async refund(paymentId: string, amount: number, requestedBy: string) {
|
|
158
|
+
this.logger.info('PAYMENT_REFUND_REQUESTED', 'Refund requested', requestedBy, {
|
|
159
|
+
paymentId,
|
|
160
|
+
amount
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const result = await this.stripe.refunds.create({ payment_intent: paymentId, amount })
|
|
165
|
+
this.logger.info('PAYMENT_REFUND_SUCCESS', 'Refund processed', requestedBy, {
|
|
166
|
+
paymentId,
|
|
167
|
+
stripeRefundId: result.id
|
|
168
|
+
})
|
|
169
|
+
return result
|
|
170
|
+
} catch (err) {
|
|
171
|
+
this.logger.errorStructured(
|
|
172
|
+
'PAYMENT_REFUND_FAILED',
|
|
173
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
174
|
+
requestedBy,
|
|
175
|
+
{ paymentId, amount }
|
|
176
|
+
)
|
|
177
|
+
throw err
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Output (JSON, production):
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"level": 30,
|
|
188
|
+
"time": "2026-05-28T10:12:44.512Z",
|
|
189
|
+
"service": { "name": "my-app", "version": "abc123" },
|
|
190
|
+
"logKey": "PAYMENT_REFUND_SUCCESS",
|
|
191
|
+
"msg": "Refund processed",
|
|
192
|
+
"context": "PaymentsService",
|
|
193
|
+
"requestId": "r_7f3a9b",
|
|
194
|
+
"tenantId": "t_acme",
|
|
195
|
+
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
196
|
+
"spanId": "00f067aa0ba902b7",
|
|
197
|
+
"paymentId": "pi_xyz",
|
|
198
|
+
"stripeRefundId": "re_abc"
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Output (pretty-print, development):
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
[10:12:44.512] INFO (my-app): PAYMENT_REFUND_SUCCESS
|
|
206
|
+
requestId: "r_7f3a9b"
|
|
207
|
+
tenantId: "t_acme"
|
|
208
|
+
paymentId: "pi_xyz"
|
|
209
|
+
stripeRefundId: "re_abc"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Step 4 — HTTP logging (automatic)
|
|
213
|
+
|
|
214
|
+
Enable `http.isEnabled: true` in the module options. The `HttpLoggingInterceptor` is registered globally and emits:
|
|
215
|
+
|
|
216
|
+
| Log key | When |
|
|
217
|
+
| --------------------------- | ------------------------------------- |
|
|
218
|
+
| `HTTP_REQUEST_START` | Request received |
|
|
219
|
+
| `HTTP_REQUEST_SUCCESS` | 2xx response |
|
|
220
|
+
| `HTTP_REQUEST_REDIRECT` | 3xx response |
|
|
221
|
+
| `HTTP_REQUEST_CLIENT_ERROR` | 4xx response |
|
|
222
|
+
| `HTTP_REQUEST_SERVER_ERROR` | 5xx response |
|
|
223
|
+
| `HTTP_EXCEPTION_HANDLED` | `HttpException` caught by the filter |
|
|
224
|
+
| `HTTP_EXCEPTION_UNHANDLED` | Unexpected error caught by the filter |
|
|
225
|
+
|
|
226
|
+
URLs are automatically normalized — `/users/550e8400-e29b-41d4-a716-446655440000` becomes `/users/:id` so Loki/Grafana cardinality stays bounded.
|
|
227
|
+
|
|
228
|
+
### Step 5 — OpenTelemetry correlation
|
|
229
|
+
|
|
230
|
+
Initialize the OTel SDK **before** importing NestJS — this is critical:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// main.ts (top of file — before any NestJS import)
|
|
234
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
235
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
|
236
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
237
|
+
import { resourceFromAttributes } from '@opentelemetry/resources'
|
|
238
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
|
|
239
|
+
|
|
240
|
+
const sdk = new NodeSDK({
|
|
241
|
+
resource: resourceFromAttributes({
|
|
242
|
+
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME ?? 'my-app',
|
|
243
|
+
[ATTR_SERVICE_VERSION]: process.env.RELEASE_SHA ?? 'dev',
|
|
244
|
+
'deployment.environment': process.env.NODE_ENV ?? 'development'
|
|
245
|
+
}),
|
|
246
|
+
traceExporter: new OTLPTraceExporter({ url: process.env.OTLP_TRACE_ENDPOINT }),
|
|
247
|
+
instrumentations: [
|
|
248
|
+
getNodeAutoInstrumentations({
|
|
249
|
+
'@opentelemetry/instrumentation-fs': { isEnabled: false } // noisy
|
|
250
|
+
})
|
|
251
|
+
]
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
sdk.start()
|
|
255
|
+
|
|
256
|
+
process.on('SIGTERM', () => {
|
|
257
|
+
void sdk.shutdown().finally(() => process.exit(0))
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// NestJS imports come AFTER sdk.start()
|
|
261
|
+
import { NestFactory } from '@nestjs/core'
|
|
262
|
+
import { AppModule } from './app.module'
|
|
263
|
+
|
|
264
|
+
async function bootstrap() {
|
|
265
|
+
const app = await NestFactory.create(AppModule, { bufferLogs: true })
|
|
266
|
+
await app.listen(3000)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
void bootstrap()
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Once active, every log entry automatically carries `traceId`, `spanId`, and `traceFlags`. Click the `traceId` in Grafana to jump directly to the correlated span in Tempo or Honeycomb.
|
|
273
|
+
|
|
274
|
+
### Step 6 — Context propagation with `LogContextService`
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// request-id.middleware.ts
|
|
278
|
+
import { Injectable, NestMiddleware } from '@nestjs/common'
|
|
279
|
+
import { LogContextService } from '@bymax-one/nest-logger'
|
|
280
|
+
import { randomUUID } from 'node:crypto'
|
|
281
|
+
|
|
282
|
+
@Injectable()
|
|
283
|
+
export class RequestIdMiddleware implements NestMiddleware {
|
|
284
|
+
constructor(private readonly logContext: LogContextService) {}
|
|
285
|
+
|
|
286
|
+
use(req: Request, res: Response, next: () => void) {
|
|
287
|
+
const requestId = req.headers['x-request-id'] ?? `r_${randomUUID()}`
|
|
288
|
+
this.logContext.run({ requestId }, next)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Any log emitted inside the `run()` scope — regardless of nesting depth — automatically includes `requestId` with no prop drilling.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## ⚙️ Configuration
|
|
298
|
+
|
|
299
|
+
Full options reference for `BymaxLoggerModule.forRoot(options)`:
|
|
300
|
+
|
|
301
|
+
### Top-level options
|
|
302
|
+
|
|
303
|
+
| Option | Type | Default | Description |
|
|
304
|
+
| ---------------------------- | -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
305
|
+
| `service.name` | `string` | **Required** | Service name emitted in every log entry |
|
|
306
|
+
| `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
|
|
307
|
+
| `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
|
|
308
|
+
| `isPretty` | `boolean` | `!isProduction` | ⚠️ Reserved — not auto-wired. For pretty output add `new PrettyDevDestination()` to `destinations` (needs `pino-pretty`) |
|
|
309
|
+
| `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths merged with the defaults |
|
|
310
|
+
| `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the 113 default PII paths. ⚠️ Emits a bootstrap warning — document why |
|
|
311
|
+
| `redactCensor` | `string \| function` | `'[REDACTED]'` | Replacement value for redacted fields. A function receives the raw value |
|
|
312
|
+
| `maxEntrySizeBytes` | `number` | `65536` | Entries larger than this are replaced by a structured `LOGGER_ENTRY_TRUNCATED` warning |
|
|
313
|
+
| `destinations` | `ILogDestination[]` | `[]` | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |
|
|
314
|
+
|
|
315
|
+
### `http` options
|
|
316
|
+
|
|
317
|
+
| Option | Type | Default | Description |
|
|
318
|
+
| ------------------------------ | ---------- | ------------------------------- | ------------------------------------------------------------------------------------- |
|
|
319
|
+
| `http.isEnabled` | `boolean` | `false` | Register `HttpLoggingInterceptor` (and, on `forRoot`, `HttpExceptionFilter`) globally |
|
|
320
|
+
| `http.shouldCaptureExceptions` | `boolean` | `true` | Capture unhandled HTTP exceptions and emit `HTTP_EXCEPTION_UNHANDLED` |
|
|
321
|
+
| `http.shouldGenerateRequestId` | `boolean` | `true` | Generate a `requestId` when the inbound request header is absent |
|
|
322
|
+
| `http.excludePaths` | `RegExp[]` | `[/^\/health$/, /^\/metrics$/]` | Paths that bypass HTTP logging. Use anchored, linear-time regexes (ReDoS-safe) |
|
|
323
|
+
| `http.tenantIdHeader` | `string` | `'x-tenant-id'` | Request header carrying the tenant identifier |
|
|
324
|
+
|
|
325
|
+
### `otel` options
|
|
326
|
+
|
|
327
|
+
| Option | Type | Default | Description |
|
|
328
|
+
| ----------------------------------- | ----------------------------- | ------------- | ------------------------------------------------------------------------ |
|
|
329
|
+
| `otel.shouldAutoInjectTraceContext` | `boolean` | `true` | Detect `@opentelemetry/api` and inject `traceId`/`spanId` via Pino mixin |
|
|
330
|
+
| `otel.fieldFormat` | `'camelCase' \| 'snake_case'` | `'camelCase'` | Field names in log entries: `traceId`/`spanId` vs `trace_id`/`span_id` |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 🔐 PII Redaction
|
|
335
|
+
|
|
336
|
+
### Default paths
|
|
337
|
+
|
|
338
|
+
The library ships **113 redact paths** compiled at initialization into a single `fast-redact` function (< 3% throughput impact). These cover:
|
|
339
|
+
|
|
340
|
+
| Category | Fields |
|
|
341
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
342
|
+
| Passwords | `password`, `passwordHash`, `passwordConfirm`, `newPassword`, `oldPassword` |
|
|
343
|
+
| Tokens | `token`, `accessToken`, `refreshToken`, `idToken`, `apiKey`, `apiSecret` |
|
|
344
|
+
| MFA | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` |
|
|
345
|
+
| Generic secrets | `secret`, `clientSecret`, `signingSecret`, `privateKey` |
|
|
346
|
+
| Payment / PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
|
|
347
|
+
| Personal documents (LGPD) | `cpf`, `cnpj`, `rg` |
|
|
348
|
+
| Conservative PII | `email` |
|
|
349
|
+
| HTTP headers (absolute) | `req.headers.authorization`, `req.headers.cookie`, `req.headers["x-api-key"]`, `req.headers["x-auth-token"]`, `res.headers["set-cookie"]` |
|
|
350
|
+
|
|
351
|
+
Every field is listed at wildcard depths 1–4 (`*.field`, `*.*.field`, `*.*.*.field`, `*.*.*.*.field`) because `fast-redact`'s `*` matches a single level only — not recursive.
|
|
352
|
+
|
|
353
|
+
### Extending the defaults
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
BymaxLoggerModule.forRoot({
|
|
357
|
+
service: { name: 'my-app', version: '1.0.0' },
|
|
358
|
+
redactPaths: [
|
|
359
|
+
'*.internalSecret', // depth-1 wildcard
|
|
360
|
+
'body.creditCard.*', // all fields inside a subobject
|
|
361
|
+
'payload.user.taxId' // exact path
|
|
362
|
+
]
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The extra paths are **merged** with the defaults — never replacing them.
|
|
367
|
+
|
|
368
|
+
### Disabling defaults (not recommended)
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
BymaxLoggerModule.forRoot({
|
|
372
|
+
service: { name: 'my-app', version: '1.0.0' },
|
|
373
|
+
shouldDisableDefaultRedact: true, // ⚠️ emits LOGGER_BOOTSTRAP_WARNING
|
|
374
|
+
redactPaths: ['*.password'] // you own the full list
|
|
375
|
+
})
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
A `LOGGER_BOOTSTRAP_WARNING` entry is emitted on startup so security reviews can audit when PII protection was intentionally reduced.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 🔑 Log Key Convention
|
|
383
|
+
|
|
384
|
+
All structured log calls must use the `MODULE_ACTION_RESULT` format:
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
USER_LOGIN_SUCCESS AUTH_REGISTER_FAILED
|
|
388
|
+
PAYMENT_REFUND_PROCESSED WEBHOOK_STRIPE_RECEIVED
|
|
389
|
+
HTTP_REQUEST_CLIENT_ERROR METHOD_SLOW_EXECUTION
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The regex is exported from the `/shared` entry for CI validation:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
import { LOG_KEYS_CONVENTION_REGEX } from '@bymax-one/nest-logger/shared'
|
|
396
|
+
|
|
397
|
+
function assertValidLogKey(key: string) {
|
|
398
|
+
if (!LOG_KEYS_CONVENTION_REGEX.test(key)) {
|
|
399
|
+
throw new Error(`Invalid log key: "${key}". Expected MODULE_ACTION_RESULT format.`)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Reserved keys
|
|
405
|
+
|
|
406
|
+
The following keys are used internally by the library — do not reuse them in application code:
|
|
407
|
+
|
|
408
|
+
`LOGGER_BOOTSTRAP_OK` · `LOGGER_BOOTSTRAP_WARNING` · `LOGGER_SHUTDOWN_OK` · `HTTP_REQUEST_START` · `HTTP_REQUEST_SUCCESS` · `HTTP_REQUEST_REDIRECT` · `HTTP_REQUEST_CLIENT_ERROR` · `HTTP_REQUEST_SERVER_ERROR` · `HTTP_REQUEST_COMPLETED` · `HTTP_EXCEPTION_HANDLED` · `HTTP_EXCEPTION_UNHANDLED` · `METHOD_EXECUTION` · `METHOD_SLOW_EXECUTION` · `LOGGER_DESTINATION_INIT_FAILED` · `LOGGER_DESTINATION_WRITE_FAILED` · `LOGGER_ENTRY_TRUNCATED`
|
|
409
|
+
|
|
410
|
+
All reserved keys are exported as the `RESERVED_LOG_KEYS` constant from `@bymax-one/nest-logger/shared`.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## 🧩 Custom Destinations
|
|
415
|
+
|
|
416
|
+
Implement `ILogDestination` to ship logs to any sink:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import type { ILogDestination } from '@bymax-one/nest-logger'
|
|
420
|
+
import type { LogEntry } from '@bymax-one/nest-logger/shared'
|
|
421
|
+
|
|
422
|
+
export class LokiDestination implements ILogDestination {
|
|
423
|
+
readonly name = 'loki'
|
|
424
|
+
readonly minLevel = 'info' as const
|
|
425
|
+
|
|
426
|
+
private readonly url: string
|
|
427
|
+
private readonly headers: Record<string, string>
|
|
428
|
+
private readonly labels: Record<string, string>
|
|
429
|
+
private buffer: LogEntry[] = []
|
|
430
|
+
private flushTimer?: NodeJS.Timeout
|
|
431
|
+
|
|
432
|
+
constructor(opts: {
|
|
433
|
+
url: string
|
|
434
|
+
username: string
|
|
435
|
+
password: string
|
|
436
|
+
labels: Record<string, string>
|
|
437
|
+
}) {
|
|
438
|
+
this.url = `${opts.url}/loki/api/v1/push`
|
|
439
|
+
this.labels = opts.labels
|
|
440
|
+
const credentials = Buffer.from(`${opts.username}:${opts.password}`).toString('base64')
|
|
441
|
+
this.headers = {
|
|
442
|
+
'Content-Type': 'application/json',
|
|
443
|
+
Authorization: `Basic ${credentials}`
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async onInit(): Promise<void> {
|
|
448
|
+
this.flushTimer = setInterval(() => void this.flush(), 5_000)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async onShutdown(): Promise<void> {
|
|
452
|
+
clearInterval(this.flushTimer)
|
|
453
|
+
await this.flush()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
write(entry: LogEntry): void {
|
|
457
|
+
this.buffer.push(entry)
|
|
458
|
+
if (this.buffer.length >= 100) void this.flush()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private async flush(): Promise<void> {
|
|
462
|
+
if (this.buffer.length === 0) return
|
|
463
|
+
const batch = this.buffer.splice(0)
|
|
464
|
+
const body = JSON.stringify({
|
|
465
|
+
streams: [
|
|
466
|
+
{
|
|
467
|
+
stream: this.labels,
|
|
468
|
+
values: batch.map((e) => [String(BigInt(e.time) * 1_000_000n), JSON.stringify(e)])
|
|
469
|
+
}
|
|
470
|
+
]
|
|
471
|
+
})
|
|
472
|
+
await fetch(this.url, { method: 'POST', headers: this.headers, body })
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Then pass it via `destinations`:
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
BymaxLoggerModule.forRootAsync({
|
|
481
|
+
inject: [ConfigService],
|
|
482
|
+
useFactory: (cfg: ConfigService) => ({
|
|
483
|
+
service: { name: cfg.getOrThrow('OTEL_SERVICE_NAME'), version: cfg.getOrThrow('RELEASE_SHA') },
|
|
484
|
+
level: 'info',
|
|
485
|
+
destinations: [
|
|
486
|
+
new LokiDestination({
|
|
487
|
+
url: cfg.getOrThrow('LOKI_URL'),
|
|
488
|
+
username: cfg.getOrThrow('LOKI_USER'),
|
|
489
|
+
password: cfg.getOrThrow('LOKI_PASSWORD'),
|
|
490
|
+
labels: { service: cfg.getOrThrow('OTEL_SERVICE_NAME') }
|
|
491
|
+
})
|
|
492
|
+
]
|
|
493
|
+
})
|
|
494
|
+
})
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Postgres destination (Prisma)
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import type { ILogDestination } from '@bymax-one/nest-logger'
|
|
501
|
+
import type { LogEntry } from '@bymax-one/nest-logger/shared'
|
|
502
|
+
import type { PrismaClient } from '@prisma/client'
|
|
503
|
+
|
|
504
|
+
export class PrismaLogDestination implements ILogDestination {
|
|
505
|
+
readonly name = 'prisma-postgres'
|
|
506
|
+
readonly minLevel = 'warn' as const // only persist warnings and above
|
|
507
|
+
|
|
508
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
509
|
+
|
|
510
|
+
write(entry: LogEntry): void {
|
|
511
|
+
void this.prisma.applicationLog.create({
|
|
512
|
+
data: {
|
|
513
|
+
level: entry.level,
|
|
514
|
+
logKey: entry.logKey,
|
|
515
|
+
message: entry.msg,
|
|
516
|
+
payload: entry,
|
|
517
|
+
createdAt: new Date(entry.time)
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Rolling file destination (`pino-roll`)
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
import { createStream } from 'pino-roll'
|
|
528
|
+
import type { ILogDestination } from '@bymax-one/nest-logger'
|
|
529
|
+
import type { LogEntry } from '@bymax-one/nest-logger/shared'
|
|
530
|
+
|
|
531
|
+
export class RollingFileDestination implements ILogDestination {
|
|
532
|
+
readonly name = 'rolling-file'
|
|
533
|
+
private stream?: Awaited<ReturnType<typeof createStream>>
|
|
534
|
+
|
|
535
|
+
async onInit(): Promise<void> {
|
|
536
|
+
this.stream = await createStream('logs/app.log', {
|
|
537
|
+
frequency: 'daily',
|
|
538
|
+
mkdir: true,
|
|
539
|
+
size: '50m'
|
|
540
|
+
})
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async onShutdown(): Promise<void> {
|
|
544
|
+
await new Promise<void>((resolve) => this.stream?.end(resolve))
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
write(entry: LogEntry): void {
|
|
548
|
+
this.stream?.write(JSON.stringify(entry) + '\n')
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## 🏗️ Architecture
|
|
556
|
+
|
|
557
|
+
```
|
|
558
|
+
HTTP Request
|
|
559
|
+
│
|
|
560
|
+
▼
|
|
561
|
+
HttpLoggingInterceptor ← emits HTTP_REQUEST_START
|
|
562
|
+
│
|
|
563
|
+
▼
|
|
564
|
+
RequestIdMiddleware ← opens AsyncLocalStorage scope
|
|
565
|
+
│ { requestId, tenantId, userId }
|
|
566
|
+
▼
|
|
567
|
+
Application Service
|
|
568
|
+
│
|
|
569
|
+
└── PinoLoggerService.info(logKey, msg, context, metadata)
|
|
570
|
+
│
|
|
571
|
+
▼
|
|
572
|
+
Pino logger
|
|
573
|
+
│
|
|
574
|
+
composedMixin() ← runs per-log (O(1))
|
|
575
|
+
├── ALS store → { requestId, tenantId, userId }
|
|
576
|
+
└── OTel span → { traceId, spanId, traceFlags }
|
|
577
|
+
│
|
|
578
|
+
▼
|
|
579
|
+
fast-redact ← compiled redact function (113 paths)
|
|
580
|
+
│
|
|
581
|
+
▼
|
|
582
|
+
┌─────────────────────────────┐
|
|
583
|
+
│ DefaultStdoutDestination │ ← always active
|
|
584
|
+
│ LokiDestination │ ← optional
|
|
585
|
+
│ PrismaLogDestination │ ← optional
|
|
586
|
+
│ RollingFileDestination │ ← optional
|
|
587
|
+
└─────────────────────────────┘
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Key design decisions:**
|
|
591
|
+
|
|
592
|
+
- **Singleton scope, not `Scope.REQUEST`** — `AsyncLocalStorage` delivers per-request context at zero latency overhead. NestJS request scope adds ~5% latency on the injection graph; unacceptable on a logger that runs every request.
|
|
593
|
+
- **One Pino mixin** — ALS context and OTel trace context are composed into a single mixin. Merge order is deterministic: ALS fields first, then OTel (OTel wins on name conflicts because an active span is the authoritative trace identity).
|
|
594
|
+
- **Zero direct dependencies** — the library ships with `"dependencies": {}`. All packages arrive as peer dependencies so consumers control exact versions.
|
|
595
|
+
- **`fast-redact` compiled at init** — 113 paths compile once at module bootstrap into a specialized JS function. No per-log regex matching.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## 📖 API Reference
|
|
600
|
+
|
|
601
|
+
### `PinoLoggerService`
|
|
602
|
+
|
|
603
|
+
Implements NestJS `LoggerService`. All methods are available via `@InjectLogger()`.
|
|
604
|
+
|
|
605
|
+
| Method | Signature | Notes |
|
|
606
|
+
| ----------------- | ---------------------------------- | -------------------------------------- |
|
|
607
|
+
| `info` | `(logKey, msg, context?, meta?)` | Structured info log |
|
|
608
|
+
| `warn` | `(logKey, msg, context?, meta?)` | Structured warn log |
|
|
609
|
+
| `debug` | `(logKey, msg, context?, meta?)` | Structured debug log |
|
|
610
|
+
| `error` | `(logKey, msg, context?, meta?)` | Structured error log (string message) |
|
|
611
|
+
| `fatal` | `(logKey, msg, context?, meta?)` | Structured fatal log |
|
|
612
|
+
| `warnStructured` | `(logKey, error, context?, meta?)` | Warn with `Error` object (serialized) |
|
|
613
|
+
| `errorStructured` | `(logKey, error, context?, meta?)` | Error with `Error` object (serialized) |
|
|
614
|
+
| `fatalStructured` | `(logKey, error, context?, meta?)` | Fatal with `Error` object (serialized) |
|
|
615
|
+
| `log` | `(msg, context?)` | NestJS `LoggerService` bridge → `info` |
|
|
616
|
+
| `verbose` | `(msg, context?)` | NestJS bridge → `trace` |
|
|
617
|
+
|
|
618
|
+
### `LogContextService`
|
|
619
|
+
|
|
620
|
+
| Method | Signature | Description |
|
|
621
|
+
| ---------- | ------------------- | --------------------------------------------------------------------------- |
|
|
622
|
+
| `run` | `(store, callback)` | Opens an ALS scope. Any log inside `callback` carries `store` fields |
|
|
623
|
+
| `set` | `(key, value)` | Adds/updates a field in the current scope. Throws if called outside `run()` |
|
|
624
|
+
| `getStore` | `()` | Returns current scope or `undefined` outside a `run()` call |
|
|
625
|
+
|
|
626
|
+
### Decorators
|
|
627
|
+
|
|
628
|
+
| Decorator | Target | Description |
|
|
629
|
+
| ------------------------------- | ----------------- | --------------------------------------------------------------------------------------------- |
|
|
630
|
+
| `@InjectLogger(context?)` | Constructor param | Injects `PinoLoggerService` pre-bound to the given context string |
|
|
631
|
+
| `@LogContext(store)` | Method | Wraps the method in `logContext.run(store, ...)` — all downstream logs carry the given fields |
|
|
632
|
+
| `@LogPerformance(thresholdMs?)` | Method | Logs `METHOD_EXECUTION` on completion; `METHOD_SLOW_EXECUTION` if duration exceeds threshold |
|
|
633
|
+
|
|
634
|
+
### Types (from `@bymax-one/nest-logger/shared`)
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
|
|
638
|
+
|
|
639
|
+
type ServiceMetadata = { name: string; version: string }
|
|
640
|
+
|
|
641
|
+
type LogEntry = {
|
|
642
|
+
level: number
|
|
643
|
+
time: number
|
|
644
|
+
service: ServiceMetadata
|
|
645
|
+
logKey: string
|
|
646
|
+
msg: string
|
|
647
|
+
[key: string]: unknown
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### `ILogDestination`
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
interface ILogDestination {
|
|
655
|
+
readonly name: string
|
|
656
|
+
readonly minLevel?: LogLevel
|
|
657
|
+
write(entry: LogEntry): void
|
|
658
|
+
onInit?(): Promise<void>
|
|
659
|
+
onShutdown?(): Promise<void>
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## 🚨 Error Code Catalog
|
|
666
|
+
|
|
667
|
+
| Code | Severity | When | Action |
|
|
668
|
+
| --------------------------------- | ----------------- | -------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
669
|
+
| `LOGGER_INVALID_OPTIONS` | Throws at init | `service.name` or `service.version` missing | Add the required fields |
|
|
670
|
+
| `LOGGER_INVALID_LEVEL` | Throws at init | `level` is not a valid Pino value | Use `'fatal'\|'error'\|'warn'\|'info'\|'debug'\|'trace'` |
|
|
671
|
+
| `LOGGER_PRETTY_UNAVAILABLE` | Warn on bootstrap | `isPretty: true` but `pino-pretty` not installed | Install `pino-pretty` or set `isPretty: false` |
|
|
672
|
+
| `LOGGER_OTEL_API_UNAVAILABLE` | Info on bootstrap | OTel mixin active but `@opentelemetry/api` not installed | Install the package or set `otel.shouldAutoInjectTraceContext: false` |
|
|
673
|
+
| `LOGGER_DESTINATION_INIT_FAILED` | Error (logged) | `destination.onInit()` rejects | Destination is removed; other destinations continue |
|
|
674
|
+
| `LOGGER_DESTINATION_WRITE_FAILED` | Warn (logged) | `destination.write()` throws | Entry skipped for that destination; others continue |
|
|
675
|
+
| `LOGGER_CONTEXT_OUT_OF_SCOPE` | Throws | `LogContextService.set()` called outside `run()` | Wrap in `logContext.run({ ... }, () => ...)` |
|
|
676
|
+
| `LOGGER_ENTRY_TRUNCATED` | Warn (meta-log) | Entry exceeds `maxEntrySizeBytes` | Reduce metadata or raise the limit |
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## 🪜 Level Mapping
|
|
681
|
+
|
|
682
|
+
| Pino | Pino string | NestJS | Typical use |
|
|
683
|
+
| ---- | ----------- | --------- | ----------------------------------------- |
|
|
684
|
+
| 60 | `fatal` | — | Process about to exit |
|
|
685
|
+
| 50 | `error` | `error` | Failure requiring human attention |
|
|
686
|
+
| 40 | `warn` | `warn` | Recoverable anomaly |
|
|
687
|
+
| 30 | `info` | `log` | Significant business events |
|
|
688
|
+
| 20 | `debug` | `debug` | Implementation detail for troubleshooting |
|
|
689
|
+
| 10 | `trace` | `verbose` | Ultra-granular (rarely in prod) |
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## 🧪 Testing & Quality
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
pnpm test # unit tests (Jest)
|
|
697
|
+
pnpm test:cov # coverage report
|
|
698
|
+
pnpm test:e2e # end-to-end tests (supertest)
|
|
699
|
+
pnpm test:cov:all # full coverage gate (100% statements/branches/functions/lines)
|
|
700
|
+
pnpm mutation # Stryker mutation testing (95% break gate)
|
|
701
|
+
pnpm typecheck # tsc strict check (all tsconfig variants)
|
|
702
|
+
pnpm lint # ESLint
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
Coverage gate: **100%** on statements, branches, functions, and lines. Mutation: **97.42%** current vs the **95%** break gate (Stryker `thresholds.break: 95`); 97.42% is the theoretical maximum — all 10 surviving mutants are documented equivalents. See [mutation_testing_results.md](docs/mutation_testing_results.md).
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## 🧱 Tech Stack
|
|
710
|
+
|
|
711
|
+
[](https://getpino.io)
|
|
712
|
+
[](https://nestjs.com)
|
|
713
|
+
[](https://www.typescriptlang.org)
|
|
714
|
+
[](https://nodejs.org)
|
|
715
|
+
[](https://jestjs.io)
|
|
716
|
+
[](https://stryker-mutator.io)
|
|
717
|
+
[](https://pnpm.io)
|
|
718
|
+
[](https://tsup.egoist.dev)
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
## ⚡ Performance
|
|
723
|
+
|
|
724
|
+
Pino official benchmarks (Node 24, 100k logs):
|
|
725
|
+
|
|
726
|
+
| Logger | Logs/sec | Avg latency | RSS | CPU |
|
|
727
|
+
| ---------------------- | ------------ | ----------- | ---------- | ------ |
|
|
728
|
+
| **Pino 10 (JSON)** | **~750,000** | **1.3 µs** | **~45 MB** | **8%** |
|
|
729
|
+
| Pino 10 (pretty) | ~120,000 | 8 µs | ~50 MB | 14% |
|
|
730
|
+
| Winston 3 (JSON) | ~110,000 | 9 µs | ~85 MB | 22% |
|
|
731
|
+
| Winston 3 (transports) | ~75,000 | 13 µs | ~95 MB | 28% |
|
|
732
|
+
|
|
733
|
+
At high throughput (~100k req/s), Pino consumes 3× less CPU and ~2× less RSS than Winston.
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## 🚫 What This Library Does NOT Do
|
|
738
|
+
|
|
739
|
+
By design, the following are out of scope:
|
|
740
|
+
|
|
741
|
+
- ❌ **OTel SDK initialization** — consumer's responsibility in `main.ts`
|
|
742
|
+
- ❌ **Custom metrics** (OTel Metrics API) — use `@opentelemetry/api` directly
|
|
743
|
+
- ❌ **Alerting** (PagerDuty, Slack) — configure your OTLP backend (Grafana Alertmanager)
|
|
744
|
+
- ❌ **Log aggregation UI** — Grafana, Datadog, Honeycomb
|
|
745
|
+
- ❌ **Sentry SDK** — install `@sentry/node` in the consumer if desired
|
|
746
|
+
- ❌ **Immutable audit logs** — future `@bymax-one/nest-audit`
|
|
747
|
+
- ❌ **File rotation built-in** — use `RollingFileDestination` with `pino-roll`
|
|
748
|
+
- ❌ **`pino-http`** — the `HttpLoggingInterceptor` is a NestJS-native replacement; install `pino-http` independently if you prefer it (produces duplicate logs if both are active)
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
## 🤝 Contributing
|
|
753
|
+
|
|
754
|
+
This library is part of the `@bymax-one` monorepo. Development follows the Bymax coding standards:
|
|
755
|
+
|
|
756
|
+
- TypeScript strict (`noImplicitAny`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`)
|
|
757
|
+
- 100% test coverage gate
|
|
758
|
+
- Conventional Commits enforced by commitlint + husky
|
|
759
|
+
- No direct dependencies — peer deps only
|
|
760
|
+
- All boolean identifiers prefixed with `is / has / should / can`
|
|
761
|
+
- All log keys in `MODULE_ACTION_RESULT` format
|
|
762
|
+
|
|
763
|
+
```bash
|
|
764
|
+
# Clone the repository
|
|
765
|
+
git clone https://github.com/bymaxone/nest-logger.git
|
|
766
|
+
cd nest-logger
|
|
767
|
+
|
|
768
|
+
# Install dependencies
|
|
769
|
+
pnpm install
|
|
770
|
+
|
|
771
|
+
# Run tests
|
|
772
|
+
pnpm test
|
|
773
|
+
|
|
774
|
+
# Build
|
|
775
|
+
pnpm build
|
|
776
|
+
|
|
777
|
+
# Type check
|
|
778
|
+
pnpm typecheck
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
## 🔒 Security Policy
|
|
784
|
+
|
|
785
|
+
If you discover a security vulnerability, please **do not** open a public issue. Instead, email us at **support@bymax.one** with details. We take security seriously and will respond promptly.
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## 📄 License
|
|
790
|
+
|
|
791
|
+
[MIT](./LICENSE) © [Bymax One](https://github.com/bymaxone)
|
|
792
|
+
|
|
793
|
+
---
|
|
794
|
+
|
|
795
|
+
<p align="center">
|
|
796
|
+
<sub>Built with ❤️ by <a href="https://github.com/bymaxone">Bymax One</a></sub>
|
|
797
|
+
</p>
|