@ontrails/logging 1.0.0-beta.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.
Files changed (53) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-lint.log +3 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/CHANGELOG.md +20 -0
  5. package/README.md +160 -0
  6. package/dist/env.d.ts +13 -0
  7. package/dist/env.d.ts.map +1 -0
  8. package/dist/env.js +38 -0
  9. package/dist/env.js.map +1 -0
  10. package/dist/formatters.d.ts +8 -0
  11. package/dist/formatters.d.ts.map +1 -0
  12. package/dist/formatters.js +72 -0
  13. package/dist/formatters.js.map +1 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +10 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/levels.d.ts +9 -0
  19. package/dist/levels.d.ts.map +1 -0
  20. package/dist/levels.js +52 -0
  21. package/dist/levels.js.map +1 -0
  22. package/dist/logger.d.ts +10 -0
  23. package/dist/logger.d.ts.map +1 -0
  24. package/dist/logger.js +80 -0
  25. package/dist/logger.js.map +1 -0
  26. package/dist/logtape/index.d.ts +24 -0
  27. package/dist/logtape/index.d.ts.map +1 -0
  28. package/dist/logtape/index.js +31 -0
  29. package/dist/logtape/index.js.map +1 -0
  30. package/dist/sinks.d.ts +13 -0
  31. package/dist/sinks.d.ts.map +1 -0
  32. package/dist/sinks.js +58 -0
  33. package/dist/sinks.js.map +1 -0
  34. package/dist/types.d.ts +51 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +5 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +25 -0
  39. package/src/__tests__/env.test.ts +54 -0
  40. package/src/__tests__/formatters.test.ts +119 -0
  41. package/src/__tests__/levels.test.ts +82 -0
  42. package/src/__tests__/logger.test.ts +279 -0
  43. package/src/__tests__/sinks.test.ts +181 -0
  44. package/src/env.ts +49 -0
  45. package/src/formatters.ts +101 -0
  46. package/src/index.ts +28 -0
  47. package/src/levels.ts +69 -0
  48. package/src/logger.ts +135 -0
  49. package/src/logtape/index.ts +68 -0
  50. package/src/sinks.ts +71 -0
  51. package/src/types.ts +99 -0
  52. package/tsconfig.json +9 -0
  53. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,279 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+
3
+ import { createLogger } from '../logger.js';
4
+ import type { LogRecord, LogSink } from '../types.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const captureSink = (): { records: LogRecord[]; sink: LogSink } => {
11
+ const records: LogRecord[] = [];
12
+ const sink: LogSink = {
13
+ name: 'capture',
14
+ write(record: LogRecord): void {
15
+ records.push(record);
16
+ },
17
+ };
18
+ return { records, sink };
19
+ };
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // createLogger basics
23
+ // ---------------------------------------------------------------------------
24
+
25
+ describe('createLogger', () => {
26
+ test('produces a working logger with default config', () => {
27
+ const { records, sink } = captureSink();
28
+ const logger = createLogger({ name: 'app', sinks: [sink] });
29
+
30
+ logger.info('hello');
31
+ expect(records).toHaveLength(1);
32
+ const [first] = records;
33
+ expect(first?.message).toBe('hello');
34
+ expect(first?.category).toBe('app');
35
+ expect(first?.level).toBe('info');
36
+ });
37
+
38
+ test('logger.name returns the category', () => {
39
+ const logger = createLogger({ name: 'my.app', sinks: [] });
40
+ expect(logger.name).toBe('my.app');
41
+ });
42
+
43
+ test('logger with no sinks does not throw', () => {
44
+ const logger = createLogger({ name: 'silent', sinks: [] });
45
+ expect(() => logger.info('hello')).not.toThrow();
46
+ });
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Level methods
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe('log level methods', () => {
54
+ test('all six methods dispatch to sinks at appropriate levels', () => {
55
+ const { records, sink } = captureSink();
56
+ const logger = createLogger({
57
+ level: 'trace',
58
+ name: 'app',
59
+ sinks: [sink],
60
+ });
61
+
62
+ logger.trace('t');
63
+ logger.debug('d');
64
+ logger.info('i');
65
+ logger.warn('w');
66
+ logger.error('e');
67
+ logger.fatal('f');
68
+
69
+ expect(records).toHaveLength(6);
70
+ expect(records.map((r) => r.level)).toEqual([
71
+ 'trace',
72
+ 'debug',
73
+ 'info',
74
+ 'warn',
75
+ 'error',
76
+ 'fatal',
77
+ ]);
78
+ });
79
+
80
+ test('messages below configured level are suppressed', () => {
81
+ const { records, sink } = captureSink();
82
+ const logger = createLogger({
83
+ level: 'warn',
84
+ name: 'app',
85
+ sinks: [sink],
86
+ });
87
+
88
+ logger.trace('t');
89
+ logger.debug('d');
90
+ logger.info('i');
91
+ logger.warn('w');
92
+ logger.error('e');
93
+
94
+ expect(records).toHaveLength(2);
95
+ expect(records.map((r) => r.level)).toEqual(['warn', 'error']);
96
+ });
97
+ });
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // child()
101
+ // ---------------------------------------------------------------------------
102
+
103
+ describe('child()', () => {
104
+ test('inherits parent config and merges metadata', () => {
105
+ const { records, sink } = captureSink();
106
+ const logger = createLogger({
107
+ level: 'trace',
108
+ name: 'app',
109
+ sinks: [sink],
110
+ });
111
+
112
+ const child = logger.child({ requestId: 'r1' });
113
+ child.info('hello');
114
+
115
+ expect(records).toHaveLength(1);
116
+ const [first] = records;
117
+ expect(first?.metadata).toEqual({ requestId: 'r1' });
118
+ });
119
+
120
+ test('child logger metadata appears on every log record', () => {
121
+ const { records, sink } = captureSink();
122
+ const logger = createLogger({
123
+ level: 'trace',
124
+ name: 'app',
125
+ sinks: [sink],
126
+ });
127
+
128
+ const child = logger.child({ traceId: 't1' });
129
+ child.info('a');
130
+ child.warn('b');
131
+
132
+ expect(records[0]?.metadata).toEqual({ traceId: 't1' });
133
+ expect(records[1]?.metadata).toEqual({ traceId: 't1' });
134
+ });
135
+
136
+ test('multiple child() calls compose metadata correctly', () => {
137
+ const { records, sink } = captureSink();
138
+ const logger = createLogger({
139
+ level: 'trace',
140
+ name: 'app',
141
+ sinks: [sink],
142
+ });
143
+
144
+ const child1 = logger.child({ requestId: 'r1' });
145
+ const child2 = child1.child({ userId: 'u1' });
146
+ child2.info('hello');
147
+
148
+ const [first] = records;
149
+ expect(first?.metadata).toEqual({ requestId: 'r1', userId: 'u1' });
150
+ });
151
+
152
+ test('child metadata merges with per-call metadata', () => {
153
+ const { records, sink } = captureSink();
154
+ const logger = createLogger({
155
+ level: 'trace',
156
+ name: 'app',
157
+ sinks: [sink],
158
+ });
159
+
160
+ const child = logger.child({ requestId: 'r1' });
161
+ child.info('hello', { extra: true });
162
+
163
+ const [first] = records;
164
+ expect(first?.metadata).toEqual({ extra: true, requestId: 'r1' });
165
+ });
166
+
167
+ test('child preserves parent name', () => {
168
+ const logger = createLogger({ name: 'app.db', sinks: [] });
169
+ const child = logger.child({ rid: 'x' });
170
+ expect(child.name).toBe('app.db');
171
+ });
172
+ });
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Redaction
176
+ // ---------------------------------------------------------------------------
177
+
178
+ describe('redaction', () => {
179
+ test('redaction is applied to messages before sink dispatch', () => {
180
+ const { records, sink } = captureSink();
181
+ const logger = createLogger({
182
+ level: 'trace',
183
+ name: 'app',
184
+ sinks: [sink],
185
+ });
186
+
187
+ logger.info('card: 4111-1111-1111-1111');
188
+ const [first] = records;
189
+ expect(first?.message).toBe('card: [REDACTED]');
190
+ });
191
+
192
+ test('redaction is applied to metadata before sink dispatch', () => {
193
+ const { records, sink } = captureSink();
194
+ const logger = createLogger({
195
+ level: 'trace',
196
+ name: 'app',
197
+ sinks: [sink],
198
+ });
199
+
200
+ logger.info('hello', { password: 's3cret' });
201
+ const [first] = records;
202
+ expect(first?.metadata['password']).toBe('[REDACTED]');
203
+ });
204
+
205
+ test('default redaction scrubs patterns matching DEFAULT_PATTERNS', () => {
206
+ const { records, sink } = captureSink();
207
+ const logger = createLogger({
208
+ level: 'trace',
209
+ name: 'app',
210
+ sinks: [sink],
211
+ });
212
+
213
+ logger.info('key: sk-abc123def456ghi');
214
+ const [first] = records;
215
+ expect(first?.message).toBe('key: [REDACTED]');
216
+ });
217
+
218
+ test('default redaction scrubs keys matching DEFAULT_SENSITIVE_KEYS', () => {
219
+ const { records, sink } = captureSink();
220
+ const logger = createLogger({
221
+ level: 'trace',
222
+ name: 'app',
223
+ sinks: [sink],
224
+ });
225
+
226
+ logger.info('auth', { apiKey: 'key-123', token: 'secret-value' });
227
+ const [first] = records;
228
+ expect(first?.metadata['token']).toBe('[REDACTED]');
229
+ expect(first?.metadata['apiKey']).toBe('[REDACTED]');
230
+ });
231
+
232
+ test('custom redaction patterns are applied when provided', () => {
233
+ const { records, sink } = captureSink();
234
+ const logger = createLogger({
235
+ level: 'trace',
236
+ name: 'app',
237
+ redaction: {
238
+ patterns: [/secret-\w+/g],
239
+ },
240
+ sinks: [sink],
241
+ });
242
+
243
+ logger.info('found secret-banana here');
244
+ const [first] = records;
245
+ expect(first?.message).toBe('found [REDACTED] here');
246
+ });
247
+ });
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Hierarchical category filtering
251
+ // ---------------------------------------------------------------------------
252
+
253
+ describe('hierarchical category filtering', () => {
254
+ test('uses category-specific level', () => {
255
+ const { records, sink } = captureSink();
256
+ const logger = createLogger({
257
+ level: 'info',
258
+ levels: { 'app.db': 'debug' },
259
+ name: 'app.db',
260
+ sinks: [sink],
261
+ });
262
+
263
+ logger.debug('query executed');
264
+ expect(records).toHaveLength(1);
265
+ });
266
+
267
+ test('walks up to parent category level', () => {
268
+ const { records, sink } = captureSink();
269
+ const logger = createLogger({
270
+ level: 'error',
271
+ levels: { 'app.db': 'debug' },
272
+ name: 'app.db.queries',
273
+ sinks: [sink],
274
+ });
275
+
276
+ logger.debug('query executed');
277
+ expect(records).toHaveLength(1);
278
+ });
279
+ });
@@ -0,0 +1,181 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
2
+
3
+ import { createConsoleSink, createFileSink } from '../sinks.js';
4
+ import type { LogFormatter, LogRecord } from '../types.js';
5
+
6
+ const makeRecord = (overrides?: Partial<LogRecord>): LogRecord => ({
7
+ category: 'app.test',
8
+ level: 'info',
9
+ message: 'test message',
10
+ metadata: {},
11
+ timestamp: new Date('2026-03-25T10:00:00.000Z'),
12
+ ...overrides,
13
+ });
14
+
15
+ const formattedSink = () =>
16
+ createConsoleSink({
17
+ formatter: { format: () => 'formatted' },
18
+ });
19
+
20
+ const parseJsonLines = async (
21
+ path: string
22
+ ): Promise<Record<string, unknown>[]> => {
23
+ const content = await Bun.file(path).text();
24
+ return content
25
+ .trim()
26
+ .split('\n')
27
+ .map((line) => JSON.parse(line) as Record<string, unknown>);
28
+ };
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // createConsoleSink
32
+ // ---------------------------------------------------------------------------
33
+
34
+ describe('createConsoleSink', () => {
35
+ const originalDebug = console.debug;
36
+ const originalInfo = console.info;
37
+ const originalWarn = console.warn;
38
+ const originalError = console.error;
39
+
40
+ beforeEach(() => {
41
+ // oxlint-disable-next-line no-empty-function
42
+ console.debug = mock(() => {
43
+ // noop
44
+ });
45
+ // oxlint-disable-next-line no-empty-function
46
+ console.info = mock(() => {
47
+ // noop
48
+ });
49
+ // oxlint-disable-next-line no-empty-function
50
+ console.warn = mock(() => {
51
+ // noop
52
+ });
53
+ // oxlint-disable-next-line no-empty-function
54
+ console.error = mock(() => {
55
+ // noop
56
+ });
57
+ });
58
+
59
+ afterEach(() => {
60
+ console.debug = originalDebug;
61
+ console.info = originalInfo;
62
+ console.warn = originalWarn;
63
+ console.error = originalError;
64
+ });
65
+
66
+ describe('level routing', () => {
67
+ test('writes to console.debug for trace level', () => {
68
+ const sink = formattedSink();
69
+ sink.write(makeRecord({ level: 'trace' }));
70
+ expect(console.debug).toHaveBeenCalledWith('formatted');
71
+ });
72
+
73
+ test('writes to console.debug for debug level', () => {
74
+ const sink = formattedSink();
75
+ sink.write(makeRecord({ level: 'debug' }));
76
+ expect(console.debug).toHaveBeenCalledWith('formatted');
77
+ });
78
+
79
+ test('writes to console.info for info level', () => {
80
+ const sink = formattedSink();
81
+ sink.write(makeRecord({ level: 'info' }));
82
+ expect(console.info).toHaveBeenCalledWith('formatted');
83
+ });
84
+
85
+ test('writes to console.warn for warn level', () => {
86
+ const sink = formattedSink();
87
+ sink.write(makeRecord({ level: 'warn' }));
88
+ expect(console.warn).toHaveBeenCalledWith('formatted');
89
+ });
90
+
91
+ test('writes to console.error for error level', () => {
92
+ const sink = formattedSink();
93
+ sink.write(makeRecord({ level: 'error' }));
94
+ expect(console.error).toHaveBeenCalledWith('formatted');
95
+ });
96
+
97
+ test('writes to console.error for fatal level', () => {
98
+ const sink = formattedSink();
99
+ sink.write(makeRecord({ level: 'fatal' }));
100
+ expect(console.error).toHaveBeenCalledWith('formatted');
101
+ });
102
+ });
103
+
104
+ describe('configuration', () => {
105
+ test('uses the provided formatter', () => {
106
+ const formatter: LogFormatter = {
107
+ format: (record) => `custom: ${record.message}`,
108
+ };
109
+ const sink = createConsoleSink({ formatter });
110
+ sink.write(makeRecord({ message: 'hello' }));
111
+ expect(console.info).toHaveBeenCalledWith('custom: hello');
112
+ });
113
+
114
+ test("sink has name 'console'", () => {
115
+ const sink = createConsoleSink();
116
+ expect(sink.name).toBe('console');
117
+ });
118
+ });
119
+ });
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // createFileSink
123
+ // ---------------------------------------------------------------------------
124
+
125
+ describe('createFileSink', () => {
126
+ test('appends records to the specified file', async () => {
127
+ const tmpPath = `/tmp/test-logging-sink-${Date.now()}.log`;
128
+ const sink = createFileSink({ path: tmpPath });
129
+
130
+ sink.write(makeRecord({ message: 'line 1' }));
131
+ sink.write(makeRecord({ message: 'line 2' }));
132
+ await sink.flush?.();
133
+
134
+ const lines = await parseJsonLines(tmpPath);
135
+
136
+ expect(lines).toHaveLength(2);
137
+ expect(lines[0]?.['message']).toBe('line 1');
138
+ expect(lines[1]?.['message']).toBe('line 2');
139
+ });
140
+
141
+ test('flush completes pending writes', async () => {
142
+ const tmpPath = `/tmp/test-logging-flush-${Date.now()}.log`;
143
+ const sink = createFileSink({ path: tmpPath });
144
+
145
+ sink.write(makeRecord({ message: 'flushed' }));
146
+ await sink.flush?.();
147
+
148
+ const content = await Bun.file(tmpPath).text();
149
+ expect(content).toContain('flushed');
150
+ });
151
+
152
+ test("sink has name 'file'", () => {
153
+ const tmpPath = `/tmp/test-logging-name-${Date.now()}.log`;
154
+ const sink = createFileSink({ path: tmpPath });
155
+ expect(sink.name).toBe('file');
156
+ });
157
+
158
+ test('write receives a well-formed LogRecord', () => {
159
+ const tmpPath = `/tmp/test-logging-record-${Date.now()}.log`;
160
+ const formatter: LogFormatter = {
161
+ format: (record) => {
162
+ // Verify shape
163
+ expect(record.level).toBe('warn');
164
+ expect(record.message).toBe('check shape');
165
+ expect(record.category).toBe('test.shape');
166
+ expect(record.timestamp).toBeInstanceOf(Date);
167
+ expect(record.metadata).toEqual({ key: 'val' });
168
+ return 'ok';
169
+ },
170
+ };
171
+ const sink = createFileSink({ formatter, path: tmpPath });
172
+ sink.write(
173
+ makeRecord({
174
+ category: 'test.shape',
175
+ level: 'warn',
176
+ message: 'check shape',
177
+ metadata: { key: 'val' },
178
+ })
179
+ );
180
+ });
181
+ });
package/src/env.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { LEVEL_PRIORITY } from './levels.js';
2
+ import type { LogLevel } from './types.js';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Valid log level set (for validation)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const VALID_LEVELS = new Set<string>(Object.keys(LEVEL_PRIORITY));
9
+
10
+ const isValidLogLevel = (value: string): value is LogLevel =>
11
+ VALID_LEVELS.has(value);
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // resolveLogLevel
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Resolve log level from environment variables.
19
+ *
20
+ * 1. `TRAILS_LOG_LEVEL` -- explicit override (if valid).
21
+ * 2. `TRAILS_ENV` profile defaults:
22
+ * - `development` -> `"debug"`
23
+ * - `test` -> `undefined` (no logging by default)
24
+ * - `production` -> `undefined` (caller falls through to `"info"`)
25
+ * 3. `undefined` -- no env-based level configured.
26
+ */
27
+ export const resolveLogLevel = (
28
+ env?: Record<string, string | undefined>
29
+ ): LogLevel | undefined => {
30
+ const source = env ?? process.env;
31
+
32
+ // 1. Explicit override
33
+ const explicit = source['TRAILS_LOG_LEVEL'];
34
+ if (explicit !== undefined && isValidLogLevel(explicit)) {
35
+ return explicit;
36
+ }
37
+
38
+ // 2. Profile defaults
39
+ const profile = source['TRAILS_ENV'];
40
+ if (profile === 'development') {
41
+ return 'debug';
42
+ }
43
+ if (profile === 'test') {
44
+ return undefined;
45
+ }
46
+
47
+ // production and everything else -- no opinion
48
+ return undefined;
49
+ };
@@ -0,0 +1,101 @@
1
+ import type {
2
+ LogFormatter,
3
+ LogRecord,
4
+ PrettyFormatterOptions,
5
+ } from './types.js';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // JSON Formatter
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /**
12
+ * One JSON object per log record, newline-delimited.
13
+ * Metadata fields are flattened into the top-level object.
14
+ */
15
+ export const createJsonFormatter = (): LogFormatter => ({
16
+ format(record: LogRecord): string {
17
+ const { level, message, category, timestamp, metadata } = record;
18
+ const obj: Record<string, unknown> = {
19
+ category,
20
+ level,
21
+ message,
22
+ timestamp: timestamp.toISOString(),
23
+ ...metadata,
24
+ };
25
+ return JSON.stringify(obj);
26
+ },
27
+ });
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Pretty Formatter
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const LEVEL_COLORS: Record<string, string> = {
34
+ debug: '\u001B[36m',
35
+ error: '\u001B[31m',
36
+ fatal: '\u001B[35m',
37
+ info: '\u001B[32m',
38
+ trace: '\u001B[90m',
39
+ warn: '\u001B[33m',
40
+ };
41
+
42
+ const RESET = '\u001B[0m';
43
+
44
+ const formatMetadata = (metadata: Record<string, unknown>): string => {
45
+ const entries = Object.entries(metadata);
46
+ if (entries.length === 0) {
47
+ return '';
48
+ }
49
+ return ` ${entries
50
+ .map(([k, v]) => {
51
+ const display = typeof v === 'string' ? v : JSON.stringify(v);
52
+ return `${k}=${display}`;
53
+ })
54
+ .join(' ')}`;
55
+ };
56
+
57
+ /**
58
+ * Human-readable formatter.
59
+ *
60
+ * Output: `10:00:00 INFO [app.entity] Entity created requestId=abc-123`
61
+ */
62
+ const formatTimestamp = (timestamp: Date): string =>
63
+ `${timestamp.toISOString().slice(11, 19)} `;
64
+
65
+ const formatLevelAndMessage = (
66
+ level: string,
67
+ levelLabel: string,
68
+ category: string,
69
+ message: string,
70
+ useColors: boolean
71
+ ): string => {
72
+ if (useColors) {
73
+ const color = LEVEL_COLORS[level] ?? '';
74
+ return `${color}${levelLabel}${RESET} [${category}] ${message}`;
75
+ }
76
+ return `${levelLabel} [${category}] ${message}`;
77
+ };
78
+
79
+ export const createPrettyFormatter = (
80
+ options?: PrettyFormatterOptions
81
+ ): LogFormatter => {
82
+ const showTimestamps = options?.timestamps !== false;
83
+ const useColors = options?.colors ?? process.stdout?.isTTY === true;
84
+
85
+ return {
86
+ format(record: LogRecord): string {
87
+ const { level, message, category, timestamp, metadata } = record;
88
+ const levelLabel = level.toUpperCase().padEnd(5);
89
+ const meta = formatMetadata(metadata);
90
+ const prefix = showTimestamps ? formatTimestamp(timestamp) : '';
91
+ const body = formatLevelAndMessage(
92
+ level,
93
+ levelLabel,
94
+ category,
95
+ message,
96
+ useColors
97
+ );
98
+ return meta.length > 0 ? `${prefix}${body}${meta}` : `${prefix}${body}`;
99
+ },
100
+ };
101
+ };
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Logger
2
+ export { createLogger } from './logger.js';
3
+
4
+ // Re-exports from core
5
+ export type { Logger } from '@ontrails/core';
6
+
7
+ // Sinks and formatters
8
+ export { createConsoleSink, createFileSink } from './sinks.js';
9
+ export { createJsonFormatter, createPrettyFormatter } from './formatters.js';
10
+
11
+ // Level resolution
12
+ export { resolveLogLevel } from './env.js';
13
+
14
+ // Levels
15
+ export { LEVEL_PRIORITY, shouldLog, resolveCategory } from './levels.js';
16
+
17
+ // Types
18
+ export type {
19
+ LogLevel,
20
+ LogMetadata,
21
+ LogRecord,
22
+ LoggerConfig,
23
+ LogSink,
24
+ LogFormatter,
25
+ ConsoleSinkOptions,
26
+ FileSinkOptions,
27
+ PrettyFormatterOptions,
28
+ } from './types.js';
package/src/levels.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { LogLevel } from './types.js';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Level Priority
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export const LEVEL_PRIORITY: Record<LogLevel, number> = {
8
+ debug: 1,
9
+ error: 4,
10
+ fatal: 5,
11
+ info: 2,
12
+ silent: 6,
13
+ trace: 0,
14
+ warn: 3,
15
+ };
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // shouldLog
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Returns true if a message at `messageLevel` should be emitted given the
23
+ * configured threshold `configuredLevel`.
24
+ */
25
+ export const shouldLog = (
26
+ messageLevel: LogLevel,
27
+ configuredLevel: LogLevel
28
+ ): boolean => LEVEL_PRIORITY[messageLevel] >= LEVEL_PRIORITY[configuredLevel];
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // resolveCategory
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Walk up the dot-separated category hierarchy to find the most specific
36
+ * configured level.
37
+ *
38
+ * Example: "app.db.queries" -> "app.db" -> "app" -> fallback
39
+ */
40
+ /** Walk up the dot-separated hierarchy looking for a configured level. */
41
+ const findLevel = (
42
+ name: string,
43
+ levels: Record<string, LogLevel>
44
+ ): LogLevel | undefined => {
45
+ let prefix = name;
46
+ while (prefix.length > 0) {
47
+ const level = levels[prefix];
48
+ if (level !== undefined) {
49
+ return level;
50
+ }
51
+ const lastDot = prefix.lastIndexOf('.');
52
+ if (lastDot === -1) {
53
+ break;
54
+ }
55
+ prefix = prefix.slice(0, lastDot);
56
+ }
57
+ return undefined;
58
+ };
59
+
60
+ export const resolveCategory = (
61
+ name: string,
62
+ levels: Record<string, LogLevel> | undefined,
63
+ fallback: LogLevel
64
+ ): LogLevel => {
65
+ if (!levels) {
66
+ return fallback;
67
+ }
68
+ return findLevel(name, levels) ?? fallback;
69
+ };