@polygonlabs/logger 0.0.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/README.md +237 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# @polygonlabs/logger
|
|
2
|
+
|
|
3
|
+
Shared pino-based logger for Polygon Apps Team services. Pre-configured for Datadog
|
|
4
|
+
ingestion with VError-aware error logging and optional Sentry capture.
|
|
5
|
+
|
|
6
|
+
## Why this package exists
|
|
7
|
+
|
|
8
|
+
Every service in the team needs the same pino configuration: `message` key instead of
|
|
9
|
+
`msg`, ISO 8601 timestamps, string level labels, no `pid`/`hostname`. Getting this right
|
|
10
|
+
in each service individually leads to drift — one service logs `"msg"` while another logs
|
|
11
|
+
`"message"`, breaking Datadog log parsing.
|
|
12
|
+
|
|
13
|
+
This package provides one factory, one type, and a consistent output shape across all
|
|
14
|
+
services.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createLogger } from '@polygonlabs/logger';
|
|
20
|
+
|
|
21
|
+
const logger = await createLogger();
|
|
22
|
+
logger.info({ requestId: '123' }, 'request received');
|
|
23
|
+
logger.logError({ err });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Do not import as a module-level singleton.** Construct once at the service entry point
|
|
27
|
+
and pass down via constructor arguments or function parameters. Module-level singletons
|
|
28
|
+
make it impossible to add scoped bindings per request, swap the logger in tests, or
|
|
29
|
+
integrate Sentry cleanly.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// entry point
|
|
33
|
+
const logger = await createLogger({ sentry });
|
|
34
|
+
|
|
35
|
+
// handler / service layer
|
|
36
|
+
class UserService {
|
|
37
|
+
constructor(private readonly logger: AppLogger) {}
|
|
38
|
+
|
|
39
|
+
async getUser(id: string) {
|
|
40
|
+
const log = this.logger.child({ userId: id });
|
|
41
|
+
// ...
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Customisation via child loggers
|
|
47
|
+
|
|
48
|
+
`createLogger()` intentionally does not accept options for the output shape — that
|
|
49
|
+
consistency is the point of the package. All customisation happens through `child()`.
|
|
50
|
+
|
|
51
|
+
`child(bindings, options?)` takes two arguments. The first attaches context fields; the
|
|
52
|
+
second (pino's `ChildLoggerOptions`) changes behaviour for that subtree:
|
|
53
|
+
|
|
54
|
+
| Option | Effect |
|
|
55
|
+
|--------|--------|
|
|
56
|
+
| `level` | Minimum log level for this child and all its descendants |
|
|
57
|
+
| `serializers` | Add or override field serializers (e.g. custom `req` formatting) |
|
|
58
|
+
| `redact` | Strip sensitive field paths before they reach the transport |
|
|
59
|
+
|
|
60
|
+
**Service-level setup** — create one child immediately after construction with the fields
|
|
61
|
+
and options that should apply everywhere in the service:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const base = await createLogger({ sentry });
|
|
65
|
+
const logger = base.child(
|
|
66
|
+
{ service: 'user-api', version: process.env.npm_package_version, env: process.env.NODE_ENV },
|
|
67
|
+
{ level: process.env.LOG_LEVEL ?? 'info' }
|
|
68
|
+
);
|
|
69
|
+
// Inject `logger` (not `base`) into the rest of the app.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Serializers and redaction** — scope them to a subtree so they only apply where needed:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const httpLogger = logger.child(
|
|
76
|
+
{ component: 'http' },
|
|
77
|
+
{
|
|
78
|
+
serializers: { req: (req) => ({ method: req.method, url: req.url }) },
|
|
79
|
+
redact: ['req.headers.authorization', 'req.headers.cookie']
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Request/handler-scoped fields** — create further children inside handlers:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
async function handleRequest(req: Request, logger: AppLogger) {
|
|
88
|
+
const log = logger.child({ requestId: req.id, method: req.method });
|
|
89
|
+
log.info('handling request'); // { service, env, requestId, method, message }
|
|
90
|
+
log.logError({ err }); // { service, env, requestId, method, err, message }
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Child bindings and options merge at any depth — grandchild loggers carry all ancestor
|
|
95
|
+
bindings, and `logError` and `child()` are preserved at every level.
|
|
96
|
+
|
|
97
|
+
## `AppLogger` type
|
|
98
|
+
|
|
99
|
+
`AppLogger` is a standard `pino.Logger` with two additions:
|
|
100
|
+
|
|
101
|
+
- **`logError({ err, ...context }, message?)`** — VError/WError-aware error logging (see below)
|
|
102
|
+
- **`child()`** — overridden to return `AppLogger` so child loggers carry `logError`
|
|
103
|
+
at any depth
|
|
104
|
+
|
|
105
|
+
Use `AppLogger` as the type throughout your service code rather than importing
|
|
106
|
+
`createLogger` everywhere.
|
|
107
|
+
|
|
108
|
+
## `logError({ err, ...context }, message?)`
|
|
109
|
+
|
|
110
|
+
`logError` is the only method `AppLogger` adds to the standard pino API. Its signature
|
|
111
|
+
mirrors pino's merge-object form with `err` as a required key:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
try {
|
|
115
|
+
await db.query(sql);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
logger.logError({ err });
|
|
118
|
+
logger.logError({ err, requestId, userId }); // with call-site context
|
|
119
|
+
logger.logError({ err, requestId }, 'user-facing message'); // with message override
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Any fields beyond `err` are merged into the log entry at the top level, exactly as they
|
|
124
|
+
would be if you called `logger.error({ err, requestId }, message)` directly. All entries
|
|
125
|
+
carry the child bindings of the logger and are always at `error` level.
|
|
126
|
+
|
|
127
|
+
`err` must be an `Error` instance — passing a non-Error is a TypeScript error. For unknown
|
|
128
|
+
values from a catch block, narrow first or fall back to `logger.error()` directly:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
try {
|
|
132
|
+
await something();
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err instanceof Error) {
|
|
135
|
+
logger.logError({ err });
|
|
136
|
+
} else {
|
|
137
|
+
logger.error(String(err));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### VError `info` is namespaced under `"error_info"`
|
|
143
|
+
|
|
144
|
+
VError `info` fields from the full cause chain are always emitted under the reserved
|
|
145
|
+
`error_info` key — never spread at the top level. This keeps error-carried context clearly
|
|
146
|
+
separated from call-site context, with no collision risk and no precedence rules to remember:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const err = new VError('query failed', { info: { requestId: 'abc', table: 'users' } });
|
|
150
|
+
logger.logError({ err, traceId: 'xyz' });
|
|
151
|
+
// { level: 'error', message: 'query failed', err: { ... },
|
|
152
|
+
// traceId: 'xyz', ← call-site context, top level
|
|
153
|
+
// error_info: { requestId: 'abc', table: 'users' } ← error info, always nested
|
|
154
|
+
// }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`error_info` is a **reserved key** in the context object — passing it is a TypeScript error:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
logger.logError({ err, error_info: { foo: 'bar' } });
|
|
161
|
+
// ^^^^^^^^^^ TypeScript error: error_info is not assignable to never
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
If a VError has no `info`, the `error_info` key is omitted from the log entry entirely.
|
|
165
|
+
|
|
166
|
+
### Behaviour by `err` type
|
|
167
|
+
|
|
168
|
+
**Plain `Error`** — logged with the error under `err` (serialised via pino's built-in
|
|
169
|
+
`stdSerializers.err`) and the error message as the log message. No `error_info` key:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
logger.logError({ err: new Error('connection refused') });
|
|
173
|
+
// { level: 'error', message: 'connection refused', err: { message, stack, type } }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**`VError`** — same as plain `Error`, plus VError `info` nested under `"error_info"`:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const err = new VError('query failed', { info: { requestId: 'abc', table: 'users' } });
|
|
180
|
+
logger.logError({ err });
|
|
181
|
+
// { level: 'error', message: 'query failed', err: { ... }, error_info: { requestId: 'abc', table: 'users' } }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**`WError`** — the wrapper is discarded entirely; only the cause is logged. Call-site
|
|
185
|
+
context is carried through to the cause's entry:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const root = new Error('connection refused');
|
|
189
|
+
const err = new WError('could not load user', { cause: root });
|
|
190
|
+
logger.logError({ err, requestId: 'abc' });
|
|
191
|
+
// { level: 'error', message: 'connection refused', err: { ... }, requestId: 'abc' }
|
|
192
|
+
// 'could not load user' is NOT logged — the cause is what matters
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The cause is processed by the same rules, so a `WError` wrapping a `VError` with `info`
|
|
196
|
+
will emit the `VError` entry with `info` nested and call-site context at the top level.
|
|
197
|
+
|
|
198
|
+
### Sentry
|
|
199
|
+
|
|
200
|
+
If a Sentry client was passed to `createLogger`, `logError` captures alongside the pino
|
|
201
|
+
entries: `captureException` for `Error` instances, `captureMessage` for non-Error values.
|
|
202
|
+
For a `WError`, only the cause is captured — consistent with the logging behaviour above.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import * as Sentry from '@sentry/node';
|
|
206
|
+
|
|
207
|
+
const base = await createLogger({ sentry: Sentry });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The `sentry` option accepts any object satisfying `{ captureException, captureMessage }`.
|
|
211
|
+
`@sentry/node` is not imported directly, so it stays an optional peer dependency. Sentry
|
|
212
|
+
is propagated automatically to all child loggers.
|
|
213
|
+
|
|
214
|
+
## Development output
|
|
215
|
+
|
|
216
|
+
Pass `{ pretty: true }` for human-readable output. Requires `pino-pretty` to be installed
|
|
217
|
+
as a peer dependency:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const logger = await createLogger({ pretty: process.env.NODE_ENV !== 'production' });
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Output format
|
|
224
|
+
|
|
225
|
+
The logger is pre-configured for Datadog ingestion:
|
|
226
|
+
|
|
227
|
+
| Field | Value |
|
|
228
|
+
|-------|-------|
|
|
229
|
+
| `message` | log message (pino's default `msg` is renamed) |
|
|
230
|
+
| `level` | string label: `"info"`, `"error"`, etc. |
|
|
231
|
+
| `timestamp` | ISO 8601: `"2024-01-01T12:00:00.000Z"` |
|
|
232
|
+
| `pid`, `hostname` | suppressed |
|
|
233
|
+
| `err` | serialised via pino's built-in `stdSerializers.err` |
|
|
234
|
+
|
|
235
|
+
Passing a `timestamp` key in a merge object is detected and renamed to `callerTimestamp`
|
|
236
|
+
with a warning. Letting caller-supplied timestamps shadow the authoritative timestamp
|
|
237
|
+
causes Datadog to sort log entries incorrectly.
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@polygonlabs/logger",
|
|
3
|
+
"version": "0.0.0-0",
|
|
4
|
+
"description": "Pino-based logger with Sentry integration, configured for Datadog ingestion and prettified output capable.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"pino": "^10.3.1",
|
|
18
|
+
"@polygonlabs/verror": "0.0.0-0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@tsconfig/node-ts": "^23.0.0",
|
|
22
|
+
"@tsconfig/node24": "^24.0.0",
|
|
23
|
+
"@types/node": "^24.0.0",
|
|
24
|
+
"pino-pretty": "^13.1.3",
|
|
25
|
+
"typescript": "^5.9.3",
|
|
26
|
+
"vitest": "^3.2.3"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@sentry/node": ">=8.0.0",
|
|
30
|
+
"pino-pretty": ">=13.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"@sentry/node": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"pino-pretty": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.build.json",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"lint": "eslint .",
|
|
45
|
+
"clean": "rm -rf node_modules dist *.tsbuildinfo"
|
|
46
|
+
}
|
|
47
|
+
}
|