@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
package/dist/sinks.js ADDED
@@ -0,0 +1,58 @@
1
+ import { createJsonFormatter, createPrettyFormatter } from './formatters.js';
2
+ // ---------------------------------------------------------------------------
3
+ // Console Sink
4
+ // ---------------------------------------------------------------------------
5
+ const CONSOLE_METHOD = {
6
+ debug: 'debug',
7
+ error: 'error',
8
+ fatal: 'error',
9
+ info: 'info',
10
+ trace: 'debug',
11
+ warn: 'warn',
12
+ };
13
+ /**
14
+ * Sink that writes to `console.*` methods.
15
+ *
16
+ * By default, uses `createPrettyFormatter()` when `TRAILS_ENV` is
17
+ * `"development"` and `createJsonFormatter()` otherwise.
18
+ */
19
+ export const createConsoleSink = (options) => {
20
+ const isDev = process.env['TRAILS_ENV'] === 'development';
21
+ const formatter = options?.formatter ??
22
+ (isDev ? createPrettyFormatter() : createJsonFormatter());
23
+ const allStderr = options?.stderr === true;
24
+ return {
25
+ name: 'console',
26
+ write(record) {
27
+ const output = formatter.format(record);
28
+ const method = CONSOLE_METHOD[record.level] ?? 'info';
29
+ if (allStderr) {
30
+ console.error(output);
31
+ }
32
+ else {
33
+ console[method](output);
34
+ }
35
+ },
36
+ };
37
+ };
38
+ // ---------------------------------------------------------------------------
39
+ // File Sink
40
+ // ---------------------------------------------------------------------------
41
+ /**
42
+ * Sink that appends log records to a file using `Bun.file()`.
43
+ */
44
+ export const createFileSink = (options) => {
45
+ const formatter = options.formatter ?? createJsonFormatter();
46
+ const writer = Bun.file(options.path).writer();
47
+ return {
48
+ async flush() {
49
+ await writer.flush();
50
+ },
51
+ name: 'file',
52
+ write(record) {
53
+ const output = formatter.format(record);
54
+ writer.write(`${output}\n`);
55
+ },
56
+ };
57
+ };
58
+ //# sourceMappingURL=sinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sinks.js","sourceRoot":"","sources":["../src/sinks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAQ7E,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E,MAAM,cAAc,GAAwD;IAC1E,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,MAAM;CACb,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAA4B,EAAW,EAAE;IACzE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,KAAK,aAAa,CAAC;IAC1D,MAAM,SAAS,GACb,OAAO,EAAE,SAAS;QAClB,CAAC,KAAK,CAAC,CAAC,CAAC,qBAAqB,EAAE,CAAC,CAAC,CAAC,mBAAmB,EAAE,CAAC,CAAC;IAC5D,MAAM,SAAS,GAAG,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAE3C,OAAO;QACL,IAAI,EAAE,SAAS;QACf,KAAK,CAAC,MAAiB;YACrB,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACxC,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC;YAEtD,IAAI,SAAS,EAAE,CAAC;gBACd,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAAwB,EAAW,EAAE;IAClE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,mBAAmB,EAAE,CAAC;IAC7D,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;IAE/C,OAAO;QACL,KAAK,CAAC,KAAK;YACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;QACD,IAAI,EAAE,MAAM;QACZ,KAAK,CAAC,MAAiB;YACrB,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACxC,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC;QAC9B,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,51 @@
1
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
2
+ export type LogMetadata = Record<string, unknown>;
3
+ export interface LogRecord {
4
+ readonly level: LogLevel;
5
+ readonly message: string;
6
+ readonly category: string;
7
+ readonly timestamp: Date;
8
+ readonly metadata: Record<string, unknown>;
9
+ }
10
+ export interface LogSink {
11
+ readonly name: string;
12
+ write(record: LogRecord): void;
13
+ flush?(): Promise<void>;
14
+ }
15
+ export interface LogFormatter {
16
+ format(record: LogRecord): string;
17
+ }
18
+ export interface ConsoleSinkOptions {
19
+ /** Formatter to use. Defaults to createPrettyFormatter() in dev, createJsonFormatter() in production. */
20
+ readonly formatter?: LogFormatter | undefined;
21
+ /** Send all output to stderr. Defaults to false (stderr only for warn/error/fatal). */
22
+ readonly stderr?: boolean | undefined;
23
+ }
24
+ export interface FileSinkOptions {
25
+ /** Path to the log file. */
26
+ readonly path: string;
27
+ /** Formatter. Defaults to createJsonFormatter(). */
28
+ readonly formatter?: LogFormatter | undefined;
29
+ }
30
+ export interface PrettyFormatterOptions {
31
+ /** Show timestamps. Defaults to true. */
32
+ readonly timestamps?: boolean | undefined;
33
+ /** Use colors (ANSI). Defaults to true when stdout is a TTY. */
34
+ readonly colors?: boolean | undefined;
35
+ }
36
+ export interface LoggerConfig {
37
+ /** Logger category name. Dot-separated for hierarchy: "app.db.queries" */
38
+ readonly name: string;
39
+ /** Base log level. Overridden by category-specific levels and env vars. */
40
+ readonly level?: LogLevel | undefined;
41
+ /** Category prefix -> level mapping for hierarchical filtering. */
42
+ readonly levels?: Record<string, LogLevel> | undefined;
43
+ /** Sinks to write log records to. Defaults to [createConsoleSink()]. */
44
+ readonly sinks?: readonly LogSink[] | undefined;
45
+ /** Redaction config. Defaults to core's DEFAULT_PATTERNS + DEFAULT_SENSITIVE_KEYS. */
46
+ readonly redaction?: {
47
+ readonly patterns?: RegExp[] | undefined;
48
+ readonly sensitiveKeys?: string[] | undefined;
49
+ } | undefined;
50
+ }
51
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,QAAQ,GAChB,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,OAAO,GACP,OAAO,GACP,QAAQ,CAAC;AAMb,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AAMD,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAMD,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAAC;CACnC;AAMD,MAAM,WAAW,kBAAkB;IACjC,yGAAyG;IACzG,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IAC9C,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;CAC/C;AAMD,MAAM,WAAW,sBAAsB;IACrC,yCAAyC;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1C,gEAAgE;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACvC;AAMD,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAEtC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC;IAEvD,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,CAAC;IAEhD,sFAAsF;IACtF,QAAQ,CAAC,SAAS,CAAC,EACf;QACE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QACzC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;KAC/C,GACD,SAAS,CAAC;CACf"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Log Level
3
+ // ---------------------------------------------------------------------------
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E"}
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@ontrails/logging",
3
+ "version": "1.0.0-beta.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./logtape": "./src/logtape/index.ts",
8
+ "./package.json": "./package.json"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc -b",
12
+ "test": "bun test",
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "oxlint ./src",
15
+ "clean": "rm -rf dist *.tsbuildinfo"
16
+ },
17
+ "peerDependencies": {
18
+ "@ontrails/core": "workspace:*"
19
+ },
20
+ "peerDependenciesMeta": {
21
+ "@logtape/logtape": {
22
+ "optional": true
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+
3
+ import { resolveLogLevel } from '../env.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // resolveLogLevel
7
+ // ---------------------------------------------------------------------------
8
+
9
+ describe('resolveLogLevel', () => {
10
+ test('reads from TRAILS_LOG_LEVEL', () => {
11
+ expect(resolveLogLevel({ TRAILS_LOG_LEVEL: 'debug' })).toBe('debug');
12
+ expect(resolveLogLevel({ TRAILS_LOG_LEVEL: 'error' })).toBe('error');
13
+ expect(resolveLogLevel({ TRAILS_LOG_LEVEL: 'trace' })).toBe('trace');
14
+ });
15
+
16
+ test('TRAILS_LOG_LEVEL takes precedence over TRAILS_ENV', () => {
17
+ expect(
18
+ resolveLogLevel({
19
+ TRAILS_ENV: 'development',
20
+ TRAILS_LOG_LEVEL: 'error',
21
+ })
22
+ ).toBe('error');
23
+ });
24
+
25
+ test('falls back to TRAILS_ENV profile defaults', () => {
26
+ expect(resolveLogLevel({ TRAILS_ENV: 'development' })).toBe('debug');
27
+ });
28
+
29
+ test('TRAILS_ENV=test returns undefined', () => {
30
+ expect(resolveLogLevel({ TRAILS_ENV: 'test' })).toBeUndefined();
31
+ });
32
+
33
+ test('TRAILS_ENV=production returns undefined', () => {
34
+ expect(resolveLogLevel({ TRAILS_ENV: 'production' })).toBeUndefined();
35
+ });
36
+
37
+ test('returns undefined when no env is set', () => {
38
+ expect(resolveLogLevel({})).toBeUndefined();
39
+ });
40
+
41
+ test('invalid TRAILS_LOG_LEVEL values are ignored', () => {
42
+ expect(resolveLogLevel({ TRAILS_LOG_LEVEL: 'banana' })).toBeUndefined();
43
+ expect(resolveLogLevel({ TRAILS_LOG_LEVEL: '' })).toBeUndefined();
44
+ });
45
+
46
+ test('invalid TRAILS_LOG_LEVEL falls through to TRAILS_ENV', () => {
47
+ expect(
48
+ resolveLogLevel({
49
+ TRAILS_ENV: 'development',
50
+ TRAILS_LOG_LEVEL: 'banana',
51
+ })
52
+ ).toBe('debug');
53
+ });
54
+ });
@@ -0,0 +1,119 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+
3
+ import { createJsonFormatter, createPrettyFormatter } from '../formatters.js';
4
+ import type { LogRecord } from '../types.js';
5
+
6
+ const makeRecord = (overrides?: Partial<LogRecord>): LogRecord => ({
7
+ category: 'app.entity',
8
+ level: 'info',
9
+ message: 'Entity created',
10
+ metadata: { entityId: 'e1', requestId: 'abc-123' },
11
+ timestamp: new Date('2026-03-25T10:00:00.000Z'),
12
+ ...overrides,
13
+ });
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // createJsonFormatter
17
+ // ---------------------------------------------------------------------------
18
+
19
+ describe('createJsonFormatter', () => {
20
+ test('produces valid JSON with all record fields', () => {
21
+ const formatter = createJsonFormatter();
22
+ const output = formatter.format(makeRecord());
23
+ const parsed = JSON.parse(output) as Record<string, unknown>;
24
+
25
+ expect(parsed['level']).toBe('info');
26
+ expect(parsed['message']).toBe('Entity created');
27
+ expect(parsed['category']).toBe('app.entity');
28
+ expect(parsed['timestamp']).toBe('2026-03-25T10:00:00.000Z');
29
+ });
30
+
31
+ test('flattens metadata into top-level object', () => {
32
+ const formatter = createJsonFormatter();
33
+ const output = formatter.format(makeRecord());
34
+ const parsed = JSON.parse(output) as Record<string, unknown>;
35
+
36
+ expect(parsed['requestId']).toBe('abc-123');
37
+ expect(parsed['entityId']).toBe('e1');
38
+ // metadata key itself should not appear
39
+ expect(parsed['metadata']).toBeUndefined();
40
+ });
41
+
42
+ test('handles empty metadata', () => {
43
+ const formatter = createJsonFormatter();
44
+ const output = formatter.format(makeRecord({ metadata: {} }));
45
+ const parsed = JSON.parse(output) as Record<string, unknown>;
46
+
47
+ expect(parsed['level']).toBe('info');
48
+ expect(parsed['message']).toBe('Entity created');
49
+ });
50
+
51
+ test('handles metadata with nested objects', () => {
52
+ const formatter = createJsonFormatter();
53
+ const output = formatter.format(
54
+ makeRecord({ metadata: { user: { id: 1 } } })
55
+ );
56
+ const parsed = JSON.parse(output) as Record<string, unknown>;
57
+
58
+ expect(parsed['user']).toEqual({ id: 1 });
59
+ });
60
+ });
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // createPrettyFormatter
64
+ // ---------------------------------------------------------------------------
65
+
66
+ describe('createPrettyFormatter', () => {
67
+ test('produces human-readable output with level, category, and message', () => {
68
+ const formatter = createPrettyFormatter({ colors: false });
69
+ const output = formatter.format(makeRecord());
70
+
71
+ expect(output).toContain('INFO');
72
+ expect(output).toContain('[app.entity]');
73
+ expect(output).toContain('Entity created');
74
+ });
75
+
76
+ test('includes metadata as key=value pairs', () => {
77
+ const formatter = createPrettyFormatter({ colors: false });
78
+ const output = formatter.format(makeRecord());
79
+
80
+ expect(output).toContain('requestId=abc-123');
81
+ expect(output).toContain('entityId=e1');
82
+ });
83
+
84
+ test('respects timestamps: false', () => {
85
+ const formatter = createPrettyFormatter({
86
+ colors: false,
87
+ timestamps: false,
88
+ });
89
+ const output = formatter.format(makeRecord());
90
+
91
+ // Should NOT contain the time portion
92
+ expect(output).not.toContain('10:00:00');
93
+ // Should still contain message
94
+ expect(output).toContain('Entity created');
95
+ });
96
+
97
+ test('includes timestamp by default', () => {
98
+ const formatter = createPrettyFormatter({ colors: false });
99
+ const output = formatter.format(makeRecord());
100
+
101
+ expect(output).toContain('10:00:00');
102
+ });
103
+
104
+ test('respects colors: false', () => {
105
+ const formatter = createPrettyFormatter({ colors: false });
106
+ const output = formatter.format(makeRecord());
107
+
108
+ // No ANSI escape codes
109
+ expect(output).not.toContain('\u001B[');
110
+ });
111
+
112
+ test('adds ANSI escape codes when colors: true', () => {
113
+ const formatter = createPrettyFormatter({ colors: true });
114
+ const output = formatter.format(makeRecord());
115
+
116
+ // Should contain ANSI color codes
117
+ expect(output).toContain('\u001B[');
118
+ });
119
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+
3
+ import { shouldLog, resolveCategory, LEVEL_PRIORITY } from '../levels.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // LEVEL_PRIORITY
7
+ // ---------------------------------------------------------------------------
8
+
9
+ describe('LEVEL_PRIORITY', () => {
10
+ test('trace < debug < info < warn < error < fatal < silent', () => {
11
+ expect(LEVEL_PRIORITY.trace).toBeLessThan(LEVEL_PRIORITY.debug);
12
+ expect(LEVEL_PRIORITY.debug).toBeLessThan(LEVEL_PRIORITY.info);
13
+ expect(LEVEL_PRIORITY.info).toBeLessThan(LEVEL_PRIORITY.warn);
14
+ expect(LEVEL_PRIORITY.warn).toBeLessThan(LEVEL_PRIORITY.error);
15
+ expect(LEVEL_PRIORITY.error).toBeLessThan(LEVEL_PRIORITY.fatal);
16
+ expect(LEVEL_PRIORITY.fatal).toBeLessThan(LEVEL_PRIORITY.silent);
17
+ });
18
+ });
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // shouldLog
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe('shouldLog', () => {
25
+ test('returns true when message level >= configured level', () => {
26
+ expect(shouldLog('info', 'info')).toBe(true);
27
+ expect(shouldLog('warn', 'info')).toBe(true);
28
+ expect(shouldLog('error', 'debug')).toBe(true);
29
+ expect(shouldLog('fatal', 'trace')).toBe(true);
30
+ });
31
+
32
+ test('returns false when message level < configured level', () => {
33
+ expect(shouldLog('debug', 'info')).toBe(false);
34
+ expect(shouldLog('trace', 'warn')).toBe(false);
35
+ expect(shouldLog('info', 'error')).toBe(false);
36
+ });
37
+
38
+ test('silent level suppresses all messages', () => {
39
+ expect(shouldLog('trace', 'silent')).toBe(false);
40
+ expect(shouldLog('debug', 'silent')).toBe(false);
41
+ expect(shouldLog('info', 'silent')).toBe(false);
42
+ expect(shouldLog('warn', 'silent')).toBe(false);
43
+ expect(shouldLog('error', 'silent')).toBe(false);
44
+ expect(shouldLog('fatal', 'silent')).toBe(false);
45
+ });
46
+ });
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // resolveCategory
50
+ // ---------------------------------------------------------------------------
51
+
52
+ describe('resolveCategory', () => {
53
+ test('returns exact match for a full category name', () => {
54
+ const levels = { 'app.db.queries': 'debug' as const };
55
+ expect(resolveCategory('app.db.queries', levels, 'info')).toBe('debug');
56
+ });
57
+
58
+ test('walks up hierarchy: app.db.queries -> app.db -> app', () => {
59
+ const levels = { app: 'warn' as const, 'app.db': 'debug' as const };
60
+ // "app.db.queries" not found, falls to "app.db"
61
+ expect(resolveCategory('app.db.queries', levels, 'info')).toBe('debug');
62
+ });
63
+
64
+ test('walks all the way to parent prefix', () => {
65
+ const levels = { app: 'error' as const };
66
+ expect(resolveCategory('app.db.queries', levels, 'info')).toBe('error');
67
+ });
68
+
69
+ test('returns fallback when no prefix matches', () => {
70
+ const levels = { other: 'debug' as const };
71
+ expect(resolveCategory('app.db.queries', levels, 'info')).toBe('info');
72
+ });
73
+
74
+ test('returns fallback when levels is undefined', () => {
75
+ expect(resolveCategory('app.db', undefined, 'warn')).toBe('warn');
76
+ });
77
+
78
+ test('returns fallback when category has no dots and no match', () => {
79
+ const levels = { other: 'debug' as const };
80
+ expect(resolveCategory('app', levels, 'info')).toBe('info');
81
+ });
82
+ });