@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/src/logger.ts ADDED
@@ -0,0 +1,135 @@
1
+ import {
2
+ createRedactor,
3
+ DEFAULT_PATTERNS,
4
+ DEFAULT_SENSITIVE_KEYS,
5
+ } from '@ontrails/core/redaction';
6
+
7
+ import type { Logger } from '@ontrails/core';
8
+
9
+ import { resolveLogLevel } from './env.js';
10
+ import { resolveCategory, shouldLog } from './levels.js';
11
+ import { createConsoleSink } from './sinks.js';
12
+ import type {
13
+ LogLevel,
14
+ LogMetadata,
15
+ LoggerConfig,
16
+ LogRecord,
17
+ LogSink,
18
+ } from './types.js';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Internal instance builder
22
+ // ---------------------------------------------------------------------------
23
+
24
+ interface RedactorLike {
25
+ redact(value: string): string;
26
+ redactObject<T extends Record<string, unknown>>(obj: T): T;
27
+ }
28
+
29
+ interface BuildInstanceConfig {
30
+ readonly baseLevel: LogLevel;
31
+ readonly levels: Record<string, LogLevel> | undefined;
32
+ }
33
+
34
+ const buildInstance = (
35
+ name: string,
36
+ effectiveLevel: LogLevel,
37
+ redactor: RedactorLike,
38
+ sinks: readonly LogSink[],
39
+ // oxlint-disable-next-line only-used-in-recursion
40
+ config: BuildInstanceConfig,
41
+ baseMetadata: Record<string, unknown>
42
+ ): Logger => {
43
+ const log = (
44
+ level: LogLevel,
45
+ message: string,
46
+ metadata?: LogMetadata
47
+ ): void => {
48
+ if (!shouldLog(level, effectiveLevel)) {
49
+ return;
50
+ }
51
+
52
+ const mergedMeta: Record<string, unknown> = {
53
+ ...baseMetadata,
54
+ ...metadata,
55
+ };
56
+
57
+ const record: LogRecord = {
58
+ category: name,
59
+ level,
60
+ message: redactor.redact(message),
61
+ metadata:
62
+ Object.keys(mergedMeta).length > 0
63
+ ? redactor.redactObject(mergedMeta)
64
+ : {},
65
+ timestamp: new Date(),
66
+ };
67
+
68
+ for (const sink of sinks) {
69
+ sink.write(record);
70
+ }
71
+ };
72
+
73
+ return {
74
+ child(metadata: LogMetadata): Logger {
75
+ return buildInstance(name, effectiveLevel, redactor, sinks, config, {
76
+ ...baseMetadata,
77
+ ...metadata,
78
+ });
79
+ },
80
+ debug(message: string, metadata?: LogMetadata): void {
81
+ log('debug', message, metadata);
82
+ },
83
+ error(message: string, metadata?: LogMetadata): void {
84
+ log('error', message, metadata);
85
+ },
86
+ fatal(message: string, metadata?: LogMetadata): void {
87
+ log('fatal', message, metadata);
88
+ },
89
+ info(message: string, metadata?: LogMetadata): void {
90
+ log('info', message, metadata);
91
+ },
92
+ name,
93
+ trace(message: string, metadata?: LogMetadata): void {
94
+ log('trace', message, metadata);
95
+ },
96
+ warn(message: string, metadata?: LogMetadata): void {
97
+ log('warn', message, metadata);
98
+ },
99
+ };
100
+ };
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // createLogger
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Create a structured logger with hierarchical category filtering,
108
+ * automatic redaction, and pluggable sinks.
109
+ *
110
+ * This is the **only** API for creating loggers in `@ontrails/logging`.
111
+ */
112
+ export const createLogger = (config: LoggerConfig): Logger => {
113
+ const envLevel = resolveLogLevel();
114
+ const baseLevel: LogLevel = config.level ?? envLevel ?? 'info';
115
+ const effectiveLevel = resolveCategory(config.name, config.levels, baseLevel);
116
+
117
+ const redactor = createRedactor({
118
+ patterns: [...DEFAULT_PATTERNS, ...(config.redaction?.patterns ?? [])],
119
+ sensitiveKeys: [
120
+ ...DEFAULT_SENSITIVE_KEYS,
121
+ ...(config.redaction?.sensitiveKeys ?? []),
122
+ ],
123
+ });
124
+
125
+ const sinks: readonly LogSink[] = config.sinks ?? [createConsoleSink()];
126
+
127
+ return buildInstance(
128
+ config.name,
129
+ effectiveLevel,
130
+ redactor,
131
+ sinks,
132
+ { baseLevel, levels: config.levels },
133
+ {}
134
+ );
135
+ };
@@ -0,0 +1,68 @@
1
+ import type { LogRecord, LogSink } from '../types.js';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Minimal logtape logger interface (avoids importing @logtape/logtape)
5
+ // ---------------------------------------------------------------------------
6
+
7
+ /**
8
+ * Subset of the logtape Logger interface that we forward records to.
9
+ * Accepts any object that provides the standard log-level methods.
10
+ */
11
+ export interface LogtapeLoggerLike {
12
+ trace(message: string, props?: Record<string, unknown>): void;
13
+ debug(message: string, props?: Record<string, unknown>): void;
14
+ info(message: string, props?: Record<string, unknown>): void;
15
+ warn(message: string, props?: Record<string, unknown>): void;
16
+ error(message: string, props?: Record<string, unknown>): void;
17
+ fatal(message: string, props?: Record<string, unknown>): void;
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Options
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface LogtapeSinkOptions {
25
+ /** An existing logtape logger (or compatible object) to forward records to. */
26
+ readonly logger: LogtapeLoggerLike;
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // logtapeSink
31
+ // ---------------------------------------------------------------------------
32
+
33
+ type ForwardMethod = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
34
+
35
+ const LEVEL_MAP: Record<string, ForwardMethod> = {
36
+ debug: 'debug',
37
+ error: 'error',
38
+ fatal: 'fatal',
39
+ info: 'info',
40
+ trace: 'trace',
41
+ warn: 'warn',
42
+ };
43
+
44
+ /**
45
+ * Sink adapter that forwards `LogRecord` instances to an existing logtape
46
+ * logger. Redaction runs _before_ the sink receives records, so sensitive
47
+ * data is scrubbed regardless of the backend.
48
+ */
49
+ export const logtapeSink = (options: LogtapeSinkOptions): LogSink => {
50
+ const { logger } = options;
51
+
52
+ return {
53
+ name: 'logtape',
54
+ write(record: LogRecord): void {
55
+ const method = LEVEL_MAP[record.level];
56
+ if (method === undefined) {
57
+ return;
58
+ }
59
+
60
+ const props: Record<string, unknown> = {
61
+ category: record.category,
62
+ ...record.metadata,
63
+ };
64
+
65
+ logger[method](record.message, props);
66
+ },
67
+ };
68
+ };
package/src/sinks.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { createJsonFormatter, createPrettyFormatter } from './formatters.js';
2
+ import type {
3
+ ConsoleSinkOptions,
4
+ FileSinkOptions,
5
+ LogRecord,
6
+ LogSink,
7
+ } from './types.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Console Sink
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const CONSOLE_METHOD: Record<string, 'debug' | 'info' | 'warn' | 'error'> = {
14
+ debug: 'debug',
15
+ error: 'error',
16
+ fatal: 'error',
17
+ info: 'info',
18
+ trace: 'debug',
19
+ warn: 'warn',
20
+ };
21
+
22
+ /**
23
+ * Sink that writes to `console.*` methods.
24
+ *
25
+ * By default, uses `createPrettyFormatter()` when `TRAILS_ENV` is
26
+ * `"development"` and `createJsonFormatter()` otherwise.
27
+ */
28
+ export const createConsoleSink = (options?: ConsoleSinkOptions): LogSink => {
29
+ const isDev = process.env['TRAILS_ENV'] === 'development';
30
+ const formatter =
31
+ options?.formatter ??
32
+ (isDev ? createPrettyFormatter() : createJsonFormatter());
33
+ const allStderr = options?.stderr === true;
34
+
35
+ return {
36
+ name: 'console',
37
+ write(record: LogRecord): void {
38
+ const output = formatter.format(record);
39
+ const method = CONSOLE_METHOD[record.level] ?? 'info';
40
+
41
+ if (allStderr) {
42
+ console.error(output);
43
+ } else {
44
+ console[method](output);
45
+ }
46
+ },
47
+ };
48
+ };
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // File Sink
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * Sink that appends log records to a file using `Bun.file()`.
56
+ */
57
+ export const createFileSink = (options: FileSinkOptions): LogSink => {
58
+ const formatter = options.formatter ?? createJsonFormatter();
59
+ const writer = Bun.file(options.path).writer();
60
+
61
+ return {
62
+ async flush(): Promise<void> {
63
+ await writer.flush();
64
+ },
65
+ name: 'file',
66
+ write(record: LogRecord): void {
67
+ const output = formatter.format(record);
68
+ writer.write(`${output}\n`);
69
+ },
70
+ };
71
+ };
package/src/types.ts ADDED
@@ -0,0 +1,99 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Log Level
3
+ // ---------------------------------------------------------------------------
4
+
5
+ export type LogLevel =
6
+ | 'trace'
7
+ | 'debug'
8
+ | 'info'
9
+ | 'warn'
10
+ | 'error'
11
+ | 'fatal'
12
+ | 'silent';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Log Metadata & Record
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export type LogMetadata = Record<string, unknown>;
19
+
20
+ export interface LogRecord {
21
+ readonly level: LogLevel;
22
+ readonly message: string;
23
+ readonly category: string;
24
+ readonly timestamp: Date;
25
+ readonly metadata: Record<string, unknown>;
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Sink
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export interface LogSink {
33
+ readonly name: string;
34
+ write(record: LogRecord): void;
35
+ flush?(): Promise<void>;
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Formatter
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export interface LogFormatter {
43
+ format(record: LogRecord): string;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Sink Options
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export interface ConsoleSinkOptions {
51
+ /** Formatter to use. Defaults to createPrettyFormatter() in dev, createJsonFormatter() in production. */
52
+ readonly formatter?: LogFormatter | undefined;
53
+ /** Send all output to stderr. Defaults to false (stderr only for warn/error/fatal). */
54
+ readonly stderr?: boolean | undefined;
55
+ }
56
+
57
+ export interface FileSinkOptions {
58
+ /** Path to the log file. */
59
+ readonly path: string;
60
+ /** Formatter. Defaults to createJsonFormatter(). */
61
+ readonly formatter?: LogFormatter | undefined;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Pretty Formatter Options
66
+ // ---------------------------------------------------------------------------
67
+
68
+ export interface PrettyFormatterOptions {
69
+ /** Show timestamps. Defaults to true. */
70
+ readonly timestamps?: boolean | undefined;
71
+ /** Use colors (ANSI). Defaults to true when stdout is a TTY. */
72
+ readonly colors?: boolean | undefined;
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Logger Config
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export interface LoggerConfig {
80
+ /** Logger category name. Dot-separated for hierarchy: "app.db.queries" */
81
+ readonly name: string;
82
+
83
+ /** Base log level. Overridden by category-specific levels and env vars. */
84
+ readonly level?: LogLevel | undefined;
85
+
86
+ /** Category prefix -> level mapping for hierarchical filtering. */
87
+ readonly levels?: Record<string, LogLevel> | undefined;
88
+
89
+ /** Sinks to write log records to. Defaults to [createConsoleSink()]. */
90
+ readonly sinks?: readonly LogSink[] | undefined;
91
+
92
+ /** Redaction config. Defaults to core's DEFAULT_PATTERNS + DEFAULT_SENSITIVE_KEYS. */
93
+ readonly redaction?:
94
+ | {
95
+ readonly patterns?: RegExp[] | undefined;
96
+ readonly sensitiveKeys?: string[] | undefined;
97
+ }
98
+ | undefined;
99
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["**/__tests__/**", "**/*.test.ts", "dist"]
9
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/env.ts","./src/formatters.ts","./src/index.ts","./src/levels.ts","./src/logger.ts","./src/sinks.ts","./src/types.ts","./src/logtape/index.ts"],"version":"5.9.3"}