@sprqvntrs/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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 SPRQVNTRS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,311 @@
1
+ # @sprqvntrs/logger
2
+
3
+ TypeScript logging package built on [Pino](https://github.com/pinojs/pino) with structured logging, request tracing, and testing utilities.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @sprqvntrs/logger
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createLogger } from '@sprqvntrs/logger';
15
+
16
+ const logger = createLogger({
17
+ serviceName: 'my-service',
18
+ level: 'debug', // optional, defaults to LOG_LEVEL env or 'info'
19
+ });
20
+
21
+ logger.info('Server started', { port: 3000 });
22
+ logger.error('Operation failed', { error: new Error('Something went wrong') });
23
+
24
+ // Child loggers for scoped context
25
+ const requestLogger = logger.child({ requestId: '123', userId: 'user-456' });
26
+ requestLogger.info('Processing request'); // includes requestId and userId
27
+ ```
28
+
29
+ ## Features
30
+
31
+ - Factory function (`createLogger`) and static class (`StaticLogger`) APIs
32
+ - Type-safe structured logging with `LogContext` interface
33
+ - Child logger support for scoped logging
34
+ - AsyncLocalStorage-based request ID propagation
35
+ - HTTP middleware with path/extension exclusions
36
+ - Server lifecycle presets (start, shutdown, errors)
37
+ - Worker/job logging presets
38
+ - Mock logger and spy utilities for testing
39
+ - Automatic redaction of sensitive fields (passwords, tokens, etc.)
40
+ - Opt-in pretty printing via `pino-pretty` (consumer-installed)
41
+
42
+ ## Entry Points
43
+
44
+ | Import | Description |
45
+ |--------|-------------|
46
+ | `@sprqvntrs/logger` | Main exports (createLogger, StaticLogger, types, context utils) |
47
+ | `@sprqvntrs/logger/http` | HTTP middleware (createHttpLogger) |
48
+ | `@sprqvntrs/logger/server` | Server/worker lifecycle logging |
49
+ | `@sprqvntrs/logger/testing` | Mock logger for tests |
50
+
51
+ ## API Reference
52
+
53
+ ### `createLogger(options)`
54
+
55
+ Creates a new logger instance.
56
+
57
+ ```typescript
58
+ interface CreateLoggerOptions {
59
+ serviceName: string; // Required: included in all logs
60
+ level?: LogLevel; // 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'
61
+ version?: string; // Service version (defaults to npm_package_version)
62
+ pretty?: boolean; // Pretty print (default: false, requires pino-pretty installed)
63
+ redactPaths?: string[]; // Additional paths to redact
64
+ base?: Record<string, unknown>; // Additional base fields
65
+ timestamp?: boolean; // Include timestamp (default: true)
66
+ }
67
+ ```
68
+
69
+ ### Logger Methods
70
+
71
+ ```typescript
72
+ interface Logger {
73
+ trace(message: string, context?: LogContext): void;
74
+ debug(message: string, context?: LogContext): void;
75
+ info(message: string, context?: LogContext): void;
76
+ warn(message: string, context?: LogContext): void;
77
+ error(message: string, context?: LogContext): void;
78
+ fatal(message: string, context?: LogContext): void;
79
+
80
+ child(bindings: Record<string, unknown>): Logger;
81
+ readonly pino: PinoLogger; // Access underlying Pino instance
82
+ }
83
+ ```
84
+
85
+ ### Static Logger (Backward Compatible)
86
+
87
+ ```typescript
88
+ import Logger from '@sprqvntrs/logger';
89
+
90
+ // Configure once at startup
91
+ Logger.configure({ serviceName: 'my-service' });
92
+
93
+ // Use anywhere
94
+ Logger.info('message', { key: 'value' });
95
+ Logger.error('failed', { error: new Error('oops') });
96
+
97
+ // Create child loggers
98
+ const child = Logger.child({ component: 'auth' });
99
+ ```
100
+
101
+ ## HTTP Middleware
102
+
103
+ ```typescript
104
+ import { createHttpLogger } from '@sprqvntrs/logger/http';
105
+ import { createLogger } from '@sprqvntrs/logger';
106
+
107
+ const logger = createLogger({ serviceName: 'api' });
108
+
109
+ const httpLogger = createHttpLogger({
110
+ logger, // Use existing logger
111
+ excludePaths: ['/health', '/metrics'], // Don't log these paths
112
+ excludeExtensions: ['.js', '.css', '.png'], // Don't log static files
113
+ customProps: (req, res) => ({ // Add custom fields
114
+ userAgent: req.headers['user-agent'],
115
+ }),
116
+ });
117
+
118
+ // Express
119
+ app.use(httpLogger);
120
+
121
+ // Fastify
122
+ fastify.addHook('onRequest', (req, reply, done) => {
123
+ httpLogger(req.raw, reply.raw, done);
124
+ });
125
+ ```
126
+
127
+ ## Request Context Propagation
128
+
129
+ Automatically include request IDs in all logs using AsyncLocalStorage:
130
+
131
+ ```typescript
132
+ import {
133
+ withRequestContext,
134
+ generateRequestId,
135
+ createRequestContextMiddleware,
136
+ } from '@sprqvntrs/logger';
137
+
138
+ // Express middleware
139
+ app.use(createRequestContextMiddleware());
140
+
141
+ // Or manual context
142
+ app.use((req, res, next) => {
143
+ const requestId = req.headers['x-request-id'] || generateRequestId();
144
+ withRequestContext({ requestId }, () => next());
145
+ });
146
+
147
+ // All subsequent logs automatically include requestId
148
+ logger.info('Processing'); // { requestId: '...', msg: 'Processing' }
149
+ ```
150
+
151
+ ## Server Lifecycle Logging
152
+
153
+ ```typescript
154
+ import { createServerLogger } from '@sprqvntrs/logger/server';
155
+
156
+ const { logger, logServerStart, logShutdown, logServerClosed } =
157
+ createServerLogger({
158
+ serviceName: 'api',
159
+ registerGlobalHandlers: true, // Handle uncaughtException/unhandledRejection
160
+ });
161
+
162
+ const server = app.listen(3000, () => {
163
+ logServerStart(3000, { url: 'http://localhost:3000' });
164
+ });
165
+
166
+ process.on('SIGTERM', () => {
167
+ logShutdown('SIGTERM');
168
+ server.close(() => logServerClosed());
169
+ });
170
+ ```
171
+
172
+ ## Worker/Job Logging
173
+
174
+ ```typescript
175
+ import { createWorkerLogger } from '@sprqvntrs/logger/server';
176
+
177
+ const { logger, logJobStart, logJobComplete, logJobFailed } =
178
+ createWorkerLogger({ serviceName: 'background-worker' });
179
+
180
+ async function processJob(job: Job) {
181
+ const startTime = Date.now();
182
+ logJobStart(job.id, job.type);
183
+
184
+ try {
185
+ await executeJob(job);
186
+ logJobComplete(job.id, job.type, Date.now() - startTime);
187
+ } catch (error) {
188
+ logJobFailed(job.id, job.type, error);
189
+ throw error;
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## Testing
195
+
196
+ ```typescript
197
+ import { createMockLogger } from '@sprqvntrs/logger/testing';
198
+
199
+ describe('MyService', () => {
200
+ let logger: MockLogger;
201
+
202
+ beforeEach(() => {
203
+ logger = createMockLogger();
204
+ });
205
+
206
+ it('logs user creation', () => {
207
+ const service = new MyService(logger);
208
+ service.createUser({ name: 'Test' });
209
+
210
+ expect(logger.hasLog((log) =>
211
+ log.level === 'info' &&
212
+ log.message.includes('User created')
213
+ )).toBe(true);
214
+ });
215
+
216
+ it('captures error context', () => {
217
+ const service = new MyService(logger);
218
+ service.failingOperation();
219
+
220
+ const errorLogs = logger.getLogsByLevel('error');
221
+ expect(errorLogs).toHaveLength(1);
222
+ expect(errorLogs[0]?.context?.error).toBeDefined();
223
+ });
224
+
225
+ afterEach(() => {
226
+ logger.clear();
227
+ });
228
+ });
229
+ ```
230
+
231
+ ### Spy Logger
232
+
233
+ Wrap a real logger to capture calls while still logging:
234
+
235
+ ```typescript
236
+ import { createSpyLogger, createLogger } from '@sprqvntrs/logger/testing';
237
+
238
+ const realLogger = createLogger({ serviceName: 'test' });
239
+ const { logger, getCalls } = createSpyLogger(realLogger);
240
+
241
+ logger.info('test message', { key: 'value' });
242
+
243
+ expect(getCalls('info')).toContainEqual({
244
+ message: 'test message',
245
+ context: { key: 'value' },
246
+ });
247
+ ```
248
+
249
+ ## Environment Variables
250
+
251
+ | Variable | Description | Default |
252
+ |----------|-------------|---------|
253
+ | `LOG_LEVEL` | Log level | `info` |
254
+ | `NODE_ENV` | Included as `env` in log base fields | - |
255
+
256
+ ## Pretty Printing (Development)
257
+
258
+ Pretty output is **opt-in**. To enable it, install `pino-pretty` as a dev dependency in your application and pass `pretty: true`:
259
+
260
+ ```bash
261
+ pnpm add -D pino-pretty
262
+ ```
263
+
264
+ ```typescript
265
+ const logger = createLogger({
266
+ serviceName: 'my-service',
267
+ pretty: process.env.NODE_ENV !== 'production', // your choice when to enable
268
+ });
269
+ ```
270
+
271
+ Without `pretty: true`, the logger emits structured JSON regardless of `NODE_ENV` — which is what production log aggregators (Datadog, CloudWatch, Loki) expect. If you pass `pretty: true` without `pino-pretty` installed, Pino will throw at instantiation.
272
+
273
+ ## Automatic Redaction
274
+
275
+ The following paths are automatically redacted from logs:
276
+
277
+ - `password`, `token`, `authorization`, `apiKey`, `api_key`, `secret`, `credential`
278
+ - Nested paths: `*.password`, `*.token`, etc.
279
+ - Headers: `headers.authorization`, `headers.cookie`
280
+
281
+ Add custom redaction paths:
282
+
283
+ ```typescript
284
+ const logger = createLogger({
285
+ serviceName: 'api',
286
+ redactPaths: ['*.ssn', 'creditCard'],
287
+ });
288
+ ```
289
+
290
+ ## TypeScript
291
+
292
+ This package exports TypeScript source files directly. Configure your bundler to transpile `node_modules/@sprqvntrs/*`:
293
+
294
+ ```json
295
+ {
296
+ "compilerOptions": {
297
+ "moduleResolution": "bundler",
298
+ "allowImportingTsExtensions": true
299
+ }
300
+ }
301
+ ```
302
+
303
+ ## Raw TypeScript
304
+
305
+ This package ships raw TypeScript (`main` and `types` point at `index.ts`), so a Vite
306
+ consumer (Vite, React Router, Remix) must add the scope to `ssr.noExternal`:
307
+ `ssr: { noExternal: [/^@sprqvntrs\//] }`.
308
+
309
+ ## License
310
+
311
+ MIT
package/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ // Main factory function
2
+ export { createLogger, StaticLogger } from './src/core/logger';
3
+
4
+ // Backward-compatible default export (static logger)
5
+ export { StaticLogger as default } from './src/core/logger';
6
+
7
+ // Buffered logger
8
+ export { createBufferedLogger } from './src/core/buffered-logger';
9
+
10
+ // Flush destinations
11
+ export { jsonDestination } from './src/core/destinations';
12
+ export type { JsonDestinationOptions } from './src/core/destinations';
13
+
14
+ // Types
15
+ export type {
16
+ Logger,
17
+ LogLevel,
18
+ LogContext,
19
+ LogEntry,
20
+ MockLogger,
21
+ CreateLoggerOptions,
22
+ LoggerConfigureOptions,
23
+ HttpLoggerOptions,
24
+ RequestContext,
25
+ ServerLogger,
26
+ BufferedLogger,
27
+ BufferedLoggerOptions,
28
+ FlushDestination,
29
+ SerializedLogEntry,
30
+ } from './src/types';
31
+
32
+ // Context utilities for request tracing
33
+ export {
34
+ withRequestContext,
35
+ withRequestContextAsync,
36
+ getRequestContext,
37
+ getRequestId,
38
+ getContextValue,
39
+ updateRequestContext,
40
+ generateRequestId,
41
+ createRequestContextMiddleware,
42
+ } from './src/context/async-context';
43
+
44
+ // Serializers (for advanced customization)
45
+ export {
46
+ errorSerializer,
47
+ requestSerializer,
48
+ responseSerializer,
49
+ getSerializers,
50
+ } from './src/core/serializers';
51
+
52
+ // Pino config (for advanced customization)
53
+ export { createPinoConfig, createPinoInstance } from './src/core/pino-config';
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@sprqvntrs/logger",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./index.ts",
6
+ "types": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./http": "./src/middleware/http.ts",
10
+ "./server": "./src/presets/server.ts",
11
+ "./testing": "./src/testing/mock-logger.ts"
12
+ },
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/SPRQVNTRS/platform.git",
17
+ "directory": "packages/logger"
18
+ },
19
+ "files": [
20
+ "src/**/*",
21
+ "index.ts",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "test": "vitest run --passWithNoTests",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "pino": "^9.6.0",
30
+ "pino-http": "^10.4.0"
31
+ },
32
+ "peerDependencies": {
33
+ "pino-pretty": "^13.0.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "pino-pretty": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.0.0",
42
+ "pino-pretty": "^13.0.0",
43
+ "tsx": "^4.20.6",
44
+ "typescript": "^5.6.0",
45
+ "vitest": "^3.2.4"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "homepage": "https://github.com/SPRQVNTRS/platform/tree/main/packages/logger#readme",
51
+ "bugs": {
52
+ "url": "https://github.com/SPRQVNTRS/platform/issues"
53
+ }
54
+ }
@@ -0,0 +1,126 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { RequestContext } from '../types';
3
+
4
+ /**
5
+ * AsyncLocalStorage instance for maintaining request context
6
+ * across async operations
7
+ */
8
+ const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
9
+
10
+ /**
11
+ * Generates a unique request ID
12
+ * Uses crypto.randomUUID if available, falls back to timestamp-based ID
13
+ */
14
+ export function generateRequestId(): string {
15
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
16
+ return crypto.randomUUID();
17
+ }
18
+ // Fallback for older Node.js versions
19
+ return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 11)}`;
20
+ }
21
+
22
+ /**
23
+ * Run a function with request context
24
+ * The context will be available to all async operations within the callback
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * // In middleware
29
+ * app.use((req, res, next) => {
30
+ * withRequestContext({ requestId: generateRequestId() }, () => {
31
+ * next();
32
+ * });
33
+ * });
34
+ *
35
+ * // Later in any handler or service
36
+ * logger.info('Processing'); // requestId automatically included
37
+ * ```
38
+ */
39
+ export function withRequestContext<T>(context: RequestContext, fn: () => T): T {
40
+ return asyncLocalStorage.run(context, fn);
41
+ }
42
+
43
+ /**
44
+ * Run an async function with request context
45
+ * Convenience wrapper for async functions
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * await withRequestContextAsync({ requestId: '123' }, async () => {
50
+ * await someAsyncOperation();
51
+ * logger.info('Done'); // requestId automatically included
52
+ * });
53
+ * ```
54
+ */
55
+ export async function withRequestContextAsync<T>(
56
+ context: RequestContext,
57
+ fn: () => Promise<T>
58
+ ): Promise<T> {
59
+ return asyncLocalStorage.run(context, fn);
60
+ }
61
+
62
+ /**
63
+ * Get the current request context
64
+ * Returns undefined if not running within a request context
65
+ */
66
+ export function getRequestContext(): RequestContext | undefined {
67
+ return asyncLocalStorage.getStore();
68
+ }
69
+
70
+ /**
71
+ * Get a specific value from the current request context
72
+ */
73
+ export function getContextValue<K extends keyof RequestContext>(
74
+ key: K
75
+ ): RequestContext[K] | undefined {
76
+ const store = asyncLocalStorage.getStore();
77
+ return store?.[key];
78
+ }
79
+
80
+ /**
81
+ * Get the current request ID from context
82
+ * Convenience method for the most common use case
83
+ */
84
+ export function getRequestId(): string | undefined {
85
+ return getContextValue('requestId');
86
+ }
87
+
88
+ /**
89
+ * Update the current request context
90
+ * Merges new values with existing context
91
+ * Only works within an active context - no-op otherwise
92
+ */
93
+ export function updateRequestContext(updates: Partial<RequestContext>): void {
94
+ const store = asyncLocalStorage.getStore();
95
+ if (store) {
96
+ Object.assign(store, updates);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Create a middleware-style function for Express/Koa/etc
102
+ * that sets up request context for each request
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // Express
107
+ * app.use(createRequestContextMiddleware());
108
+ *
109
+ * // With custom ID extraction
110
+ * app.use(createRequestContextMiddleware((req) => ({
111
+ * requestId: req.headers['x-request-id'] || generateRequestId(),
112
+ * userId: req.user?.id,
113
+ * })));
114
+ * ```
115
+ */
116
+ export function createRequestContextMiddleware(
117
+ contextExtractor?: (req: unknown) => RequestContext
118
+ ): (req: unknown, res: unknown, next: () => void) => void {
119
+ return (req, _res, next) => {
120
+ const context = contextExtractor
121
+ ? contextExtractor(req)
122
+ : { requestId: generateRequestId() };
123
+
124
+ withRequestContext(context, next);
125
+ };
126
+ }