@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.
- package/.turbo/turbo-build.log +1 -0
- package/.turbo/turbo-lint.log +3 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/CHANGELOG.md +20 -0
- package/README.md +160 -0
- package/dist/env.d.ts +13 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +38 -0
- package/dist/env.js.map +1 -0
- package/dist/formatters.d.ts +8 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +72 -0
- package/dist/formatters.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/levels.d.ts +9 -0
- package/dist/levels.d.ts.map +1 -0
- package/dist/levels.js +52 -0
- package/dist/levels.js.map +1 -0
- package/dist/logger.d.ts +10 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +80 -0
- package/dist/logger.js.map +1 -0
- package/dist/logtape/index.d.ts +24 -0
- package/dist/logtape/index.d.ts.map +1 -0
- package/dist/logtape/index.js +31 -0
- package/dist/logtape/index.js.map +1 -0
- package/dist/sinks.d.ts +13 -0
- package/dist/sinks.d.ts.map +1 -0
- package/dist/sinks.js +58 -0
- package/dist/sinks.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +25 -0
- package/src/__tests__/env.test.ts +54 -0
- package/src/__tests__/formatters.test.ts +119 -0
- package/src/__tests__/levels.test.ts +82 -0
- package/src/__tests__/logger.test.ts +279 -0
- package/src/__tests__/sinks.test.ts +181 -0
- package/src/env.ts +49 -0
- package/src/formatters.ts +101 -0
- package/src/index.ts +28 -0
- package/src/levels.ts +69 -0
- package/src/logger.ts +135 -0
- package/src/logtape/index.ts +68 -0
- package/src/sinks.ts +71 -0
- package/src/types.ts +99 -0
- package/tsconfig.json +9 -0
- 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 @@
|
|
|
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"}
|