@spfn/core 0.3.0-beta.5 → 0.3.0-beta.6
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 +132 -4
- package/dist/db/index.d.ts +173 -27
- package/dist/db/index.js +192 -57
- package/dist/db/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +18 -1
- package/dist/nextjs/index.js +40 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +34 -1
- package/dist/nextjs/server.js +14 -0
- package/dist/nextjs/server.js.map +1 -1
- package/docs/file-upload.md +195 -333
- package/package.json +6 -5
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +516 -0
- package/src/config/README.md +326 -0
- package/src/contract/README.md +326 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +344 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +651 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +736 -0
- package/src/job/README.md +514 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +608 -0
- package/src/route/README.md +738 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# @spfn/core/logger — Zero-dependency structured logger (singleton + child loggers)
|
|
2
|
+
|
|
3
|
+
Transport-based logging for Next.js + SPFN server. One process-wide singleton `logger`,
|
|
4
|
+
five levels, automatic sensitive-data masking, and per-module child loggers. No external
|
|
5
|
+
logging library.
|
|
6
|
+
|
|
7
|
+
## Import paths
|
|
8
|
+
|
|
9
|
+
One entry point. Everything public comes from `@spfn/core/logger`.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { logger } from '@spfn/core/logger';
|
|
13
|
+
import type { LogLevel, Transport } from '@spfn/core/logger';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
There is **no** root `.` export for `@spfn/core`, so `import { logger } from '@spfn/core'`
|
|
17
|
+
does not resolve. `logger`, `Logger`, `LogLevel` and `Transport` all come from the
|
|
18
|
+
`@spfn/core/logger` subpath. (Older snippets showing the root form are stale.)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Public API (complete)
|
|
23
|
+
|
|
24
|
+
From `@spfn/core/logger` (`src/logger/index.ts` is the only export surface):
|
|
25
|
+
|
|
26
|
+
- `logger` — the singleton `Logger` instance. This is what you use 99% of the time.
|
|
27
|
+
- `Logger` — the class. Exported mainly for typing; you do **not** construct it yourself
|
|
28
|
+
(the singleton is created in `factory.ts`). Use `logger` / `logger.child(...)`.
|
|
29
|
+
- Types: `LogLevel`, `Transport`.
|
|
30
|
+
|
|
31
|
+
Instance methods on `logger` / any child logger:
|
|
32
|
+
|
|
33
|
+
- `logger.debug(message, ...)`, `logger.info(...)`, `logger.warn(...)`, `logger.error(...)`,
|
|
34
|
+
`logger.fatal(...)` — the five level methods (overloaded, see Logging below).
|
|
35
|
+
- `logger.child(module: string): Logger` — new logger that tags every line with `[module=...]`.
|
|
36
|
+
- `logger.close(): Promise<void>` — close/flush all transports (graceful shutdown).
|
|
37
|
+
- `logger.level: LogLevel` — getter for the currently active level (read-only).
|
|
38
|
+
|
|
39
|
+
> **No such API — do not use these (they appear in old docs but are not in the code):**
|
|
40
|
+
> - `createLogger(name)` — does **not** exist. Use `logger.child(name)`.
|
|
41
|
+
> - `withLogContext(ctx, fn)` / any AsyncLocalStorage context helper — does **not** exist.
|
|
42
|
+
> Pass context explicitly as the last argument on each call.
|
|
43
|
+
> - File transport, `LOGGER_FILE_ENABLED`, `LOG_DIR` — there is **no** file transport.
|
|
44
|
+
> The only transport is console (stdout/stderr).
|
|
45
|
+
> - `LOG_LEVEL` env var — the level is read from `SPFN_LOG_LEVEL` /
|
|
46
|
+
> `NEXT_PUBLIC_SPFN_LOG_LEVEL`, **not** `LOG_LEVEL`.
|
|
47
|
+
> - Slack / Email / CloudWatch / Sentry transports — none exist and none are "already
|
|
48
|
+
> configured". The factory wires exactly one `ConsoleTransport`.
|
|
49
|
+
>
|
|
50
|
+
> Internal formatter functions (`maskSensitiveData`, `formatConsole`, `formatJSON`,
|
|
51
|
+
> `extractQueryInfo`, `formatUnhandledRejection`, etc.) live in `formatters.ts` but are
|
|
52
|
+
> **not** part of the public export surface — don't import them from `@spfn/core/logger`.
|
|
53
|
+
> In particular `formatJSON` exists but is **not wired into any transport**: production
|
|
54
|
+
> output is plain (uncolored) console text, not JSON (see Output format).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { logger } from '@spfn/core/logger';
|
|
62
|
+
|
|
63
|
+
logger.debug('Cache miss', { key: 'user:123' });
|
|
64
|
+
logger.info('Server started', { port: 3000 });
|
|
65
|
+
logger.warn('Retry attempt', { attempt: 3 });
|
|
66
|
+
logger.error('Operation failed', error, { userId: 123 }); // error as 2nd arg
|
|
67
|
+
logger.fatal('Database unreachable', error);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
No initialization, no `await`, no setup — importing `logger` is enough. The singleton is
|
|
71
|
+
constructed on first import in `factory.ts`.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Logging: message + (error | context) + context
|
|
76
|
+
|
|
77
|
+
Each level method (`debug`/`info`/`warn`/`error`/`fatal`) shares the same overloads.
|
|
78
|
+
The second argument is detected at runtime, so argument **order and type matter**:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// 1. message only
|
|
82
|
+
logger.info('Server started');
|
|
83
|
+
|
|
84
|
+
// 2. message + context object → rendered as [key=value] pairs
|
|
85
|
+
logger.info('Request received', { method: 'POST', path: '/users' });
|
|
86
|
+
|
|
87
|
+
// 3. message + Error → stack trace (and cause chain) printed
|
|
88
|
+
logger.error('Query failed', error);
|
|
89
|
+
|
|
90
|
+
// 4. message + Error + context → both
|
|
91
|
+
logger.error('Query failed', error, { userId: 123, op: 'createUser' });
|
|
92
|
+
|
|
93
|
+
// 5. printf-style — if message contains a %s/%d/%i/%f/%j/%o/%O/%c token,
|
|
94
|
+
// the second arg is substituted via node:util.format (NOT treated as context)
|
|
95
|
+
logger.info('Fetching url: %s', requestUrl);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
How the second argument is classified (see `logWithLevel` in `logger.ts`):
|
|
99
|
+
- If `message` contains a printf token (`%s %d %i %f %j %o %O %c %%`), the 2nd arg is
|
|
100
|
+
formatted into the message string — it will **not** appear as context.
|
|
101
|
+
- An `Error` instance → logged as the error (stack + `cause` chain printed).
|
|
102
|
+
- A non-Error object **with** a string `stack` property → treated as an error and wrapped.
|
|
103
|
+
- A plain object **without** a `stack` property → treated as **context**.
|
|
104
|
+
- A `string` / `number` / `boolean` → wrapped into an `Error` (so `{ key: value }`
|
|
105
|
+
context must be an object, not a bare primitive).
|
|
106
|
+
|
|
107
|
+
This `stack`-based heuristic means an object you intend as context will be misread as an
|
|
108
|
+
error if it happens to carry a string `stack` field. Prefer the explicit 3-arg form
|
|
109
|
+
`logger.error(msg, error, context)` when you have both.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Child loggers (per-module)
|
|
114
|
+
|
|
115
|
+
`logger.child(name)` returns a new `Logger` that inherits the level/transports and adds a
|
|
116
|
+
`[module=name]` tag to every line. Cheap to create; create one per module.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const dbLogger = logger.child('database');
|
|
120
|
+
const apiLogger = logger.child('api');
|
|
121
|
+
|
|
122
|
+
dbLogger.info('Connection established');
|
|
123
|
+
// [2026-06-10 10:30:00.123] [pid=12345] [module=database] (INFO): Connection established
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Child loggers do **not** stack — calling `.child('b')` on a child created with `.child('a')`
|
|
127
|
+
replaces the module name with `b`, it does not produce `a.b`.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Log levels
|
|
132
|
+
|
|
133
|
+
Five levels, filtered by priority. A log is emitted only when its priority is `>=` the
|
|
134
|
+
configured level (filtering happens *before* metadata is built, so suppressed
|
|
135
|
+
`logger.debug(...)` calls are cheap).
|
|
136
|
+
|
|
137
|
+
| Level | Priority | Stream | Use case |
|
|
138
|
+
|---------|----------|--------|----------|
|
|
139
|
+
| `debug` | 0 | stdout | Development diagnostics |
|
|
140
|
+
| `info` | 1 | stdout | Normal operations (server start, etc.) |
|
|
141
|
+
| `warn` | 2 | stderr | Potential issues, retries |
|
|
142
|
+
| `error` | 3 | stderr | Failures needing attention |
|
|
143
|
+
| `fatal` | 4 | stderr | Critical / shutdown-level errors |
|
|
144
|
+
|
|
145
|
+
`warn`, `error`, `fatal` go to **stderr**; `debug`, `info` go to **stdout** (via
|
|
146
|
+
`console.error` / `console.log`).
|
|
147
|
+
|
|
148
|
+
`LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'`.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Configuration (environment variables)
|
|
153
|
+
|
|
154
|
+
The level is resolved once, at module load, in `factory.ts`:
|
|
155
|
+
|
|
156
|
+
| Variable | Effect | Default |
|
|
157
|
+
|----------|--------|---------|
|
|
158
|
+
| `SPFN_LOG_LEVEL` | Minimum level. First choice. | `info` |
|
|
159
|
+
| `NEXT_PUBLIC_SPFN_LOG_LEVEL` | Fallback level (client-visible in Next.js). Used only if `SPFN_LOG_LEVEL` is unset. | `info` |
|
|
160
|
+
| `NODE_ENV` | `production` → colorized output **off**; anything else → colors **on**. | (warns if unset) |
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
SPFN_LOG_LEVEL=debug # show everything
|
|
164
|
+
NODE_ENV=production # disable ANSI colors (plain text for log collectors)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- An invalid `SPFN_LOG_LEVEL` value falls back to `info` with a stderr warning.
|
|
168
|
+
- Because the level is read **once at import time**, changing `process.env.SPFN_LOG_LEVEL`
|
|
169
|
+
at runtime has **no effect** — set it before the process starts.
|
|
170
|
+
- The console transport itself is created with level `debug` and is always enabled; the
|
|
171
|
+
effective floor is the logger-level computed from `SPFN_LOG_LEVEL`.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Sensitive data masking
|
|
176
|
+
|
|
177
|
+
Context objects are recursively scanned and any key whose lowercased name *contains* a
|
|
178
|
+
sensitive token is replaced with the literal string `***MASKED***` before output. The
|
|
179
|
+
original value is never written to any transport.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
logger.info('Login attempt', { username: 'john', password: pw, token: t });
|
|
183
|
+
// [username=john] [password=***MASKED***] [token=***MASKED***] (INFO): Login attempt
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Masking applies to nested objects and arrays, and circular references render as
|
|
187
|
+
`[Circular]`. Matching is substring + case-insensitive, so `userPassword`, `X-Api-Key`,
|
|
188
|
+
`refresh_token`, etc. all match.
|
|
189
|
+
|
|
190
|
+
Masked key tokens (a key matches if it contains any of these):
|
|
191
|
+
`password`, `passwd`, `pwd`, `secret`, `token`, `apikey`, `api_key`, `accesstoken`,
|
|
192
|
+
`access_token`, `refreshtoken`, `refresh_token`, `authorization`, `auth`, `cookie`,
|
|
193
|
+
`session`, `sessionid`, `session_id`, `privatekey`, `private_key`, `creditcard`,
|
|
194
|
+
`credit_card`, `cardnumber`, `card_number`, `cvv`, `ssn`, `pin`.
|
|
195
|
+
|
|
196
|
+
> Masking only applies to the **context** object. It does **not** scan the `message`
|
|
197
|
+
> string. Never interpolate a secret into the message — `logger.info(\`token: ${t}\`)`
|
|
198
|
+
> is logged verbatim, unmasked. Put potentially-sensitive data in the context object so
|
|
199
|
+
> the masker can catch it.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Output format
|
|
204
|
+
|
|
205
|
+
There is exactly one transport (`ConsoleTransport`) and it always renders the
|
|
206
|
+
human-readable console format — colorized when `NODE_ENV !== 'production'`, plain text
|
|
207
|
+
otherwise. Layout:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
[timestamp] [pid=N] [module=name] [key=value]... (LEVEL): message
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
[2026-06-10 10:30:00.123] [pid=12345] [module=database] (INFO): Connection established
|
|
215
|
+
[2026-06-10 10:30:01.456] [pid=12345] [module=api] [userId=123] (ERROR): Request failed
|
|
216
|
+
Error: Connection timeout
|
|
217
|
+
at processRequest (/app/src/api.ts:45:11)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Errors are printed on their own line after the message, including the `Caused by:` cause
|
|
221
|
+
chain. In containers (Docker/K8s), set `NODE_ENV=production` so output is plain text on
|
|
222
|
+
stdout/stderr, which the platform collects automatically.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Pitfalls & anti-patterns
|
|
227
|
+
|
|
228
|
+
- **`createLogger` / `withLogContext` don't exist.** Use `logger.child(name)` for scoping
|
|
229
|
+
and pass context explicitly per call. There is no ambient/async context.
|
|
230
|
+
- **No JSON output.** `formatJSON` exists in the source but is unused; the only transport
|
|
231
|
+
emits console text. "Production = JSON logs" (from old docs) is false — production just
|
|
232
|
+
turns colors off.
|
|
233
|
+
- **Wrong env var.** The level comes from `SPFN_LOG_LEVEL` (or
|
|
234
|
+
`NEXT_PUBLIC_SPFN_LOG_LEVEL`), **not** `LOG_LEVEL`.
|
|
235
|
+
- **Level is frozen at import.** Mutating `process.env.SPFN_LOG_LEVEL` after startup does
|
|
236
|
+
nothing. Set it in the environment before launching.
|
|
237
|
+
- **Masked value is `***MASKED***`, not `***`.** Don't assert on `***` in tests/parsers.
|
|
238
|
+
- **Secrets in the message are NOT masked.** Only the context object is scanned. Keep
|
|
239
|
+
sensitive values out of the message string; put them in context.
|
|
240
|
+
- **Context vs error ambiguity.** A plain object with a string `stack` property is treated
|
|
241
|
+
as an error, not context. Use the 3-arg form `logger.error(msg, error, context)` when
|
|
242
|
+
you have both, and don't put a `stack` key on a context object.
|
|
243
|
+
- **Bare primitives as 2nd arg become errors.** `logger.info('x', 42)` logs `42` as an
|
|
244
|
+
Error (or printf-substitutes it if the message has a `%` token) — it is not context.
|
|
245
|
+
Context must be an object: `logger.info('x', { n: 42 })`.
|
|
246
|
+
- **Don't import internals.** `maskSensitiveData`, `formatConsole`, `formatJSON`,
|
|
247
|
+
`extractQueryInfo`, `formatUnhandledRejection`, `Logger` construction, etc. are not the
|
|
248
|
+
public surface. Use the `logger` singleton + `child`.
|
|
249
|
+
- **No file transport.** `LOGGER_FILE_ENABLED` / `LOG_DIR` do nothing. Route stdout/stderr
|
|
250
|
+
to your log collector (Loki/CloudWatch/ELK) instead.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Complete example
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { logger } from '@spfn/core/logger';
|
|
258
|
+
|
|
259
|
+
const dbLogger = logger.child('database');
|
|
260
|
+
|
|
261
|
+
async function createUser(input: { email: string; password: string }): Promise<void>
|
|
262
|
+
{
|
|
263
|
+
dbLogger.debug('createUser called', { email: input.email });
|
|
264
|
+
// 'password' in context would be auto-masked if included.
|
|
265
|
+
|
|
266
|
+
try
|
|
267
|
+
{
|
|
268
|
+
await db.insert(/* ... */);
|
|
269
|
+
dbLogger.info('User created', { email: input.email });
|
|
270
|
+
}
|
|
271
|
+
catch (error)
|
|
272
|
+
{
|
|
273
|
+
// 3-arg form: message + Error (stack + cause) + extra context
|
|
274
|
+
dbLogger.error('User insert failed', error as Error, { email: input.email });
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Graceful shutdown: flush transports
|
|
280
|
+
process.on('SIGTERM', async () =>
|
|
281
|
+
{
|
|
282
|
+
logger.info('Shutting down');
|
|
283
|
+
await logger.close();
|
|
284
|
+
process.exit(0);
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Types reference
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
294
|
+
|
|
295
|
+
interface Transport
|
|
296
|
+
{
|
|
297
|
+
name: string;
|
|
298
|
+
level: LogLevel;
|
|
299
|
+
enabled: boolean;
|
|
300
|
+
log(metadata: LogMetadata): Promise<void>;
|
|
301
|
+
close?(): Promise<void>;
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`LogMetadata` (the object passed to a `Transport.log`; not exported, shown for
|
|
306
|
+
implementers) carries `timestamp: Date`, `level`, `message`, optional `module`,
|
|
307
|
+
optional `error: Error`, and optional `context` (already masked).
|
|
308
|
+
|
|
309
|
+
`logger.level` is a `LogLevel` getter (read-only).
|
|
310
|
+
|
|
311
|
+
The `Transport` interface is public so you *could* implement a custom transport, but the
|
|
312
|
+
factory does not expose a registration hook — there is currently no supported way to add a
|
|
313
|
+
transport to the singleton without editing `factory.ts`.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Related
|
|
318
|
+
|
|
319
|
+
- `@spfn/core` — package root (re-exports `logger`).
|
|
320
|
+
- `src/logger/factory.ts` — where the singleton + console transport are wired and the
|
|
321
|
+
level is resolved from env.
|