@kindgi/log 0.0.0-bootstrap.0 → 0.1.4-rc.5
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 +201 -0
- package/README.md +84 -0
- package/dist/env.d.ts +40 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +82 -0
- package/dist/env.js.map +1 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +81 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/levels.d.ts +34 -0
- package/dist/levels.d.ts.map +1 -0
- package/dist/levels.js +75 -0
- package/dist/levels.js.map +1 -0
- package/dist/logger.d.ts +28 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +143 -0
- package/dist/logger.js.map +1 -0
- package/dist/redact.d.ts +33 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +93 -0
- package/dist/redact.js.map +1 -0
- package/dist/trace.d.ts +40 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +68 -0
- package/dist/trace.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -4
- package/src/env.ts +116 -0
- package/src/format.ts +94 -0
- package/src/index.ts +40 -0
- package/src/levels.ts +88 -0
- package/src/logger.ts +188 -0
- package/src/redact.ts +99 -0
- package/src/trace.ts +93 -0
- package/src/types.ts +70 -0
package/dist/levels.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* Log levels, most severe first. A level says how much attention a line
|
|
5
|
+
* needs: `error` (an operator should look), `warn` (unexpected but
|
|
6
|
+
* handled), `info` (what happened, at the operator's grain), `debug` (the
|
|
7
|
+
* decisions behind it), `trace` (internals, briefly).
|
|
8
|
+
*/
|
|
9
|
+
export const LOG_LEVELS = ['error', 'warn', 'info', 'debug', 'trace'];
|
|
10
|
+
const RANK = {
|
|
11
|
+
error: 50,
|
|
12
|
+
warn: 40,
|
|
13
|
+
info: 30,
|
|
14
|
+
debug: 20,
|
|
15
|
+
trace: 10,
|
|
16
|
+
};
|
|
17
|
+
/** Cloud Logging's severity names: Cloud Run and GKE show levels with no agent configuration. */
|
|
18
|
+
export const SEVERITY = {
|
|
19
|
+
error: 'ERROR',
|
|
20
|
+
warn: 'WARNING',
|
|
21
|
+
info: 'INFO',
|
|
22
|
+
debug: 'DEBUG',
|
|
23
|
+
trace: 'DEBUG',
|
|
24
|
+
};
|
|
25
|
+
/** Whether a record at `level` is written by a logger at `threshold`. */
|
|
26
|
+
export function levelEnabled(level, threshold) {
|
|
27
|
+
return RANK[level] >= RANK[threshold];
|
|
28
|
+
}
|
|
29
|
+
/** A level's name, case and spaces aside; `undefined` when it isn't one. */
|
|
30
|
+
export function parseLogLevel(raw) {
|
|
31
|
+
const value = raw.trim().toLowerCase();
|
|
32
|
+
return LOG_LEVELS.includes(value) ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Per-subsystem levels, `KINDGI_LOG_LEVELS`'s form: a comma list of
|
|
36
|
+
* `subsystem=level`, such as `kernel=debug,http=warn`. Empty is none. An
|
|
37
|
+
* entry that isn't `name=level`, or names no known level, is an error that
|
|
38
|
+
* quotes it. Whether a subsystem exists is the caller's question.
|
|
39
|
+
*/
|
|
40
|
+
export function parseLogLevels(raw) {
|
|
41
|
+
const levels = {};
|
|
42
|
+
for (const entry of raw.split(',')) {
|
|
43
|
+
const trimmed = entry.trim();
|
|
44
|
+
if (trimmed === '')
|
|
45
|
+
continue;
|
|
46
|
+
const eq = trimmed.indexOf('=');
|
|
47
|
+
const subsystem = eq === -1 ? '' : trimmed.slice(0, eq).trim();
|
|
48
|
+
const level = eq === -1 ? undefined : parseLogLevel(trimmed.slice(eq + 1));
|
|
49
|
+
if (subsystem === '' || level === undefined) {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'err',
|
|
52
|
+
message: `"${trimmed}" isn't subsystem=level (levels: ${LOG_LEVELS.join(', ')})`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
levels[subsystem] = level;
|
|
56
|
+
}
|
|
57
|
+
return { kind: 'ok', levels };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The level for `subsystem`: its own entry, else its nearest dotted
|
|
61
|
+
* parent's (`kernel=debug` covers `kernel.sweeper`), else `fallback`.
|
|
62
|
+
*/
|
|
63
|
+
export function levelFor(subsystem, levels, fallback) {
|
|
64
|
+
let name = subsystem;
|
|
65
|
+
for (;;) {
|
|
66
|
+
const own = levels[name];
|
|
67
|
+
if (own !== undefined)
|
|
68
|
+
return own;
|
|
69
|
+
const dot = name.lastIndexOf('.');
|
|
70
|
+
if (dot === -1)
|
|
71
|
+
return fallback;
|
|
72
|
+
name = name.slice(0, dot);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=levels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"levels.js","sourceRoot":"","sources":["../src/levels.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAU,CAAC;AAI/E,MAAM,IAAI,GAAuC;IAC/C,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,EAAE;IACR,KAAK,EAAE,EAAE;IACT,KAAK,EAAE,EAAE;CACV,CAAC;AAEF,iGAAiG;AACjG,MAAM,CAAC,MAAM,QAAQ,GAAuC;IAC1D,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;CACf,CAAC;AAEF,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAAC,KAAe,EAAE,SAAmB;IAC/D,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC;AACxC,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACvC,OAAQ,UAAgC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAkB,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7F,CAAC;AAMD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,MAAM,GAA6B,EAAE,CAAC;IAC5C,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,OAAO,KAAK,EAAE;YAAE,SAAS;QAC7B,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,MAAM,SAAS,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/D,MAAM,KAAK,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3E,IAAI,SAAS,KAAK,EAAE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAC5C,OAAO;gBACL,IAAI,EAAE,KAAK;gBACX,OAAO,EAAE,IAAI,OAAO,oCAAoC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;aACjF,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC;IAC5B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,SAAiB,EACjB,MAA0C,EAC1C,QAAkB;IAElB,IAAI,IAAI,GAAG,SAAS,CAAC;IACrB,SAAS,CAAC;QACR,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,GAAG,CAAC;QAClC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,OAAO,QAAQ,CAAC;QAChC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type LogLevel } from './levels.js';
|
|
2
|
+
import type { LogFormat, Logger } from './types.js';
|
|
3
|
+
export interface CreateLoggerOptions {
|
|
4
|
+
/** Records below this level aren't written (`KINDGI_LOG_LEVEL`). Default `info`. */
|
|
5
|
+
readonly level?: LogLevel;
|
|
6
|
+
/** Per-subsystem levels; a dotted name inherits its parent's (`KINDGI_LOG_LEVELS`). */
|
|
7
|
+
readonly levels?: Readonly<Record<string, LogLevel>>;
|
|
8
|
+
/** `json` (one record per line) or `pretty` (for a terminal). Default `json`. */
|
|
9
|
+
readonly format?: LogFormat;
|
|
10
|
+
/** Colours in the pretty format (a terminal, `NO_COLOR` unset). Default off. */
|
|
11
|
+
readonly color?: boolean;
|
|
12
|
+
/** Where each line goes: one complete line, no trailing newline. */
|
|
13
|
+
readonly write: (line: string) => void;
|
|
14
|
+
/** More field keys to redact, on top of the built-in ones. */
|
|
15
|
+
readonly redact?: readonly string[];
|
|
16
|
+
/** The records' subsystem until a child names one. Default `root`. */
|
|
17
|
+
readonly subsystem?: string;
|
|
18
|
+
/** The clock (tests). */
|
|
19
|
+
readonly now?: () => Date;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A logger: records at or above its level (per subsystem, `levels`) are
|
|
23
|
+
* redacted and written to `write` as JSON or pretty lines. Logging never
|
|
24
|
+
* throws, and a failing sink is ignored.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createLogger(options: CreateLoggerOptions): Logger;
|
|
27
|
+
export declare const noopLogger: Logger;
|
|
28
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,QAAQ,EAAoC,MAAM,aAAa,CAAC;AAE9E,OAAO,KAAK,EAEV,SAAS,EAGT,MAAM,EAEP,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,mBAAmB;IAClC,oFAAoF;IACpF,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC;IAC1B,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IACrD,iFAAiF;IACjF,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB;IACzB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AA8HD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAWjE;AAKD,eAAO,MAAM,UAAU,EAAE,MAQxB,CAAC"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
import { formatJson, formatPretty } from './format.js';
|
|
4
|
+
import { SEVERITY, levelEnabled, levelFor } from './levels.js';
|
|
5
|
+
import { redactValue, scrubText } from './redact.js';
|
|
6
|
+
/** Correlation fields, in the order a record carries them, after the fixed five. */
|
|
7
|
+
const CORRELATION = [
|
|
8
|
+
'traceId',
|
|
9
|
+
'spanId',
|
|
10
|
+
'runTraceId',
|
|
11
|
+
'requestId',
|
|
12
|
+
'tenantId',
|
|
13
|
+
'projectId',
|
|
14
|
+
'orgId',
|
|
15
|
+
'runId',
|
|
16
|
+
'parentRunId',
|
|
17
|
+
'agentId',
|
|
18
|
+
'agentVersion',
|
|
19
|
+
'flowId',
|
|
20
|
+
'flowVersion',
|
|
21
|
+
'toolId',
|
|
22
|
+
'conversationId',
|
|
23
|
+
'approvalId',
|
|
24
|
+
];
|
|
25
|
+
const FIXED = new Set(['time', 'level', 'severity', 'subsystem', 'message']);
|
|
26
|
+
function serializeError(err, withStack, depth = 0) {
|
|
27
|
+
if (!(err instanceof Error))
|
|
28
|
+
return scrubText(String(err));
|
|
29
|
+
const code = err.code;
|
|
30
|
+
const cause = err.cause;
|
|
31
|
+
return {
|
|
32
|
+
name: err.name,
|
|
33
|
+
message: scrubText(err.message),
|
|
34
|
+
...(typeof code === 'string' || typeof code === 'number' ? { code: String(code) } : {}),
|
|
35
|
+
...(withStack && err.stack !== undefined && { stack: scrubText(err.stack) }),
|
|
36
|
+
...(cause !== undefined && depth < 3 && { cause: serializeError(cause, withStack, depth + 1) }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Copy a record's own fields into `out`, after the fixed five and the
|
|
41
|
+
* correlation ones; an `Error` among them is serialized. A field named
|
|
42
|
+
* like one of the fixed five can't replace it: it comes back to be kept
|
|
43
|
+
* under `fields`.
|
|
44
|
+
*/
|
|
45
|
+
function copyFields(out, merged, level) {
|
|
46
|
+
const reserved = {};
|
|
47
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
48
|
+
if (value === undefined || key === 'subsystem' || key === 'err')
|
|
49
|
+
continue;
|
|
50
|
+
if (CORRELATION.includes(key))
|
|
51
|
+
continue;
|
|
52
|
+
if (FIXED.has(key))
|
|
53
|
+
reserved[key] = value;
|
|
54
|
+
else
|
|
55
|
+
out[key] = value instanceof Error ? serializeError(value, level === 'error') : value;
|
|
56
|
+
}
|
|
57
|
+
return reserved;
|
|
58
|
+
}
|
|
59
|
+
function build(shared, bindings) {
|
|
60
|
+
const subsystem = typeof bindings.subsystem === 'string' && bindings.subsystem !== ''
|
|
61
|
+
? bindings.subsystem
|
|
62
|
+
: (shared.options.subsystem ?? 'root');
|
|
63
|
+
const threshold = levelFor(subsystem, shared.levels, shared.threshold);
|
|
64
|
+
const extra = shared.options.redact ?? [];
|
|
65
|
+
function record(level, message, fields) {
|
|
66
|
+
const merged = { ...bindings, ...fields };
|
|
67
|
+
const out = {
|
|
68
|
+
time: shared.now().toISOString(),
|
|
69
|
+
level,
|
|
70
|
+
severity: SEVERITY[level],
|
|
71
|
+
subsystem,
|
|
72
|
+
message: scrubText(message),
|
|
73
|
+
};
|
|
74
|
+
for (const key of CORRELATION) {
|
|
75
|
+
if (merged[key] !== undefined)
|
|
76
|
+
out[key] = redactValue(merged[key], extra);
|
|
77
|
+
}
|
|
78
|
+
const reserved = copyFields(out, merged, level);
|
|
79
|
+
const redacted = redactValue(out, extra);
|
|
80
|
+
// The fixed five are the record's own, never redacted as fields.
|
|
81
|
+
for (const key of FIXED)
|
|
82
|
+
redacted[key] = out[key];
|
|
83
|
+
if (Object.keys(reserved).length > 0)
|
|
84
|
+
redacted.fields = redactValue(reserved, extra);
|
|
85
|
+
if (merged.err !== undefined) {
|
|
86
|
+
const withStack = level === 'error' || levelEnabled('debug', threshold);
|
|
87
|
+
redacted.err = serializeError(merged.err, withStack);
|
|
88
|
+
}
|
|
89
|
+
return redacted;
|
|
90
|
+
}
|
|
91
|
+
function write(level, message, fields, how) {
|
|
92
|
+
if (!levelEnabled(level, threshold))
|
|
93
|
+
return;
|
|
94
|
+
const rec = record(level, message, fields);
|
|
95
|
+
const line = shared.format === 'pretty'
|
|
96
|
+
? formatPretty(rec, {
|
|
97
|
+
color: shared.options.color === true,
|
|
98
|
+
...(how?.inMessage !== undefined && { omit: how.inMessage }),
|
|
99
|
+
})
|
|
100
|
+
: formatJson(rec);
|
|
101
|
+
try {
|
|
102
|
+
shared.options.write(line);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// A sink that fails must never fail the code that logged.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
error: (message, fields, how) => write('error', message, fields, how),
|
|
110
|
+
warn: (message, fields, how) => write('warn', message, fields, how),
|
|
111
|
+
info: (message, fields, how) => write('info', message, fields, how),
|
|
112
|
+
debug: (message, fields, how) => write('debug', message, fields, how),
|
|
113
|
+
trace: (message, fields, how) => write('trace', message, fields, how),
|
|
114
|
+
child: (more) => build(shared, { ...bindings, ...more }),
|
|
115
|
+
isLevelEnabled: (level) => levelEnabled(level, threshold),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A logger: records at or above its level (per subsystem, `levels`) are
|
|
120
|
+
* redacted and written to `write` as JSON or pretty lines. Logging never
|
|
121
|
+
* throws, and a failing sink is ignored.
|
|
122
|
+
*/
|
|
123
|
+
export function createLogger(options) {
|
|
124
|
+
return build({
|
|
125
|
+
options,
|
|
126
|
+
threshold: options.level ?? 'info',
|
|
127
|
+
levels: options.levels ?? {},
|
|
128
|
+
format: options.format ?? 'json',
|
|
129
|
+
now: options.now ?? (() => new Date()),
|
|
130
|
+
}, {});
|
|
131
|
+
}
|
|
132
|
+
/** A logger that writes nothing: the default where one isn't given (`createApp`). */
|
|
133
|
+
const noop = () => undefined;
|
|
134
|
+
export const noopLogger = {
|
|
135
|
+
error: noop,
|
|
136
|
+
warn: noop,
|
|
137
|
+
info: noop,
|
|
138
|
+
debug: noop,
|
|
139
|
+
trace: noop,
|
|
140
|
+
child: () => noopLogger,
|
|
141
|
+
isLevelEnabled: () => false,
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction, applied to every record before it's written, at every level.
|
|
3
|
+
*
|
|
4
|
+
* 1. **Keys.** A field whose key looks secret has its value replaced
|
|
5
|
+
* with `[redacted]`, at any depth: its key, lowercased with `-` and
|
|
6
|
+
* `_` removed, ends with `authorization`, `cookie`, `token`,
|
|
7
|
+
* `password`, `passwd`, `secret`, `secrets`, `apikey`, `privatekey`,
|
|
8
|
+
* `passphrase` or `credential(s)` (`accessToken`, `x-api-key`,
|
|
9
|
+
* `secrets`), plus any the caller adds. `tokenCount` and
|
|
10
|
+
* `promptTokens` are not secret, and stay.
|
|
11
|
+
* 2. **Values.** Every string (the message, fields, an error's message
|
|
12
|
+
* and stack) is scrubbed of known shapes only, so ids are never
|
|
13
|
+
* mangled: a Kindgi token (`kgi_bt_…`, `kgi_pt_…`) keeps its prefix
|
|
14
|
+
* and its last four characters; `Bearer <anything>` becomes
|
|
15
|
+
* `Bearer [redacted]`; a URL's password (`postgres://user:pass@`)
|
|
16
|
+
* becomes `user:***@`.
|
|
17
|
+
*
|
|
18
|
+
* The first rule is the code rule's net: secret values are never fields
|
|
19
|
+
* to begin with (pass a secret's name, not its value).
|
|
20
|
+
*/
|
|
21
|
+
export declare const REDACTED = "[redacted]";
|
|
22
|
+
/** Whether a field's key looks secret (see rule 1). */
|
|
23
|
+
export declare function isSecretKey(key: string, extra?: readonly string[]): boolean;
|
|
24
|
+
/** A string with known secret shapes masked (see rule 2). */
|
|
25
|
+
export declare function scrubText(text: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* A value with both rules applied, recursively: secret keys redacted,
|
|
28
|
+
* strings scrubbed. Plain objects and arrays are copied; anything deeper
|
|
29
|
+
* than a few levels, or a cycle (an object inside itself), becomes a
|
|
30
|
+
* marker. An object referenced twice side by side is copied twice.
|
|
31
|
+
*/
|
|
32
|
+
export declare function redactValue(value: unknown, extraKeys?: readonly string[], depth?: number, seen?: WeakSet<object>): unknown;
|
|
33
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,QAAQ,eAAe,CAAC;AAmBrC,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,SAAS,MAAM,EAAO,GAAG,OAAO,CAG/E;AAOD,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,OAAO,EACd,SAAS,GAAE,SAAS,MAAM,EAAO,EACjC,KAAK,SAAI,EACT,IAAI,GAAE,OAAO,CAAC,MAAM,CAAiB,GACpC,OAAO,CAoBT"}
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* Redaction, applied to every record before it's written, at every level.
|
|
5
|
+
*
|
|
6
|
+
* 1. **Keys.** A field whose key looks secret has its value replaced
|
|
7
|
+
* with `[redacted]`, at any depth: its key, lowercased with `-` and
|
|
8
|
+
* `_` removed, ends with `authorization`, `cookie`, `token`,
|
|
9
|
+
* `password`, `passwd`, `secret`, `secrets`, `apikey`, `privatekey`,
|
|
10
|
+
* `passphrase` or `credential(s)` (`accessToken`, `x-api-key`,
|
|
11
|
+
* `secrets`), plus any the caller adds. `tokenCount` and
|
|
12
|
+
* `promptTokens` are not secret, and stay.
|
|
13
|
+
* 2. **Values.** Every string (the message, fields, an error's message
|
|
14
|
+
* and stack) is scrubbed of known shapes only, so ids are never
|
|
15
|
+
* mangled: a Kindgi token (`kgi_bt_…`, `kgi_pt_…`) keeps its prefix
|
|
16
|
+
* and its last four characters; `Bearer <anything>` becomes
|
|
17
|
+
* `Bearer [redacted]`; a URL's password (`postgres://user:pass@`)
|
|
18
|
+
* becomes `user:***@`.
|
|
19
|
+
*
|
|
20
|
+
* The first rule is the code rule's net: secret values are never fields
|
|
21
|
+
* to begin with (pass a secret's name, not its value).
|
|
22
|
+
*/
|
|
23
|
+
export const REDACTED = '[redacted]';
|
|
24
|
+
const SECRET_KEY_ENDINGS = [
|
|
25
|
+
'authorization',
|
|
26
|
+
'cookie',
|
|
27
|
+
'token',
|
|
28
|
+
'password',
|
|
29
|
+
'passwd',
|
|
30
|
+
'secret',
|
|
31
|
+
'secrets',
|
|
32
|
+
'apikey',
|
|
33
|
+
'privatekey',
|
|
34
|
+
'passphrase',
|
|
35
|
+
'credential',
|
|
36
|
+
'credentials',
|
|
37
|
+
];
|
|
38
|
+
const normalize = (key) => key.toLowerCase().replace(/[-_]/g, '');
|
|
39
|
+
/** Whether a field's key looks secret (see rule 1). */
|
|
40
|
+
export function isSecretKey(key, extra = []) {
|
|
41
|
+
const k = normalize(key);
|
|
42
|
+
return [...SECRET_KEY_ENDINGS, ...extra.map(normalize)].some((end) => k.endsWith(end));
|
|
43
|
+
}
|
|
44
|
+
/** A Kindgi token: `kgi_` and a two-letter kind, then its body. */
|
|
45
|
+
const KINDGI_TOKEN = /\bkgi_([a-z]{2})_([A-Za-z0-9._~+/=-]+)/g;
|
|
46
|
+
const BEARER = /\b(Bearer)\s+[^\s"',;]+/gi;
|
|
47
|
+
const URL_PASSWORD = /\b([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+):[^\s@/]+@/gi;
|
|
48
|
+
/** A string with known secret shapes masked (see rule 2). */
|
|
49
|
+
export function scrubText(text) {
|
|
50
|
+
return text
|
|
51
|
+
.replace(KINDGI_TOKEN, (_m, kind, body) => {
|
|
52
|
+
const tail = body.length >= 16 ? body.slice(-4) : '';
|
|
53
|
+
return `kgi_${kind}_…${tail}`;
|
|
54
|
+
})
|
|
55
|
+
.replace(BEARER, '$1 [redacted]')
|
|
56
|
+
.replace(URL_PASSWORD, '$1:***@');
|
|
57
|
+
}
|
|
58
|
+
const MAX_DEPTH = 8;
|
|
59
|
+
/**
|
|
60
|
+
* A value with both rules applied, recursively: secret keys redacted,
|
|
61
|
+
* strings scrubbed. Plain objects and arrays are copied; anything deeper
|
|
62
|
+
* than a few levels, or a cycle (an object inside itself), becomes a
|
|
63
|
+
* marker. An object referenced twice side by side is copied twice.
|
|
64
|
+
*/
|
|
65
|
+
export function redactValue(value, extraKeys = [], depth = 0, seen = new WeakSet()) {
|
|
66
|
+
if (typeof value === 'string')
|
|
67
|
+
return scrubText(value);
|
|
68
|
+
if (value === null || typeof value !== 'object')
|
|
69
|
+
return value;
|
|
70
|
+
if (value instanceof Date)
|
|
71
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
72
|
+
if (seen.has(value))
|
|
73
|
+
return '[circular]';
|
|
74
|
+
if (depth >= MAX_DEPTH)
|
|
75
|
+
return '[too deep]';
|
|
76
|
+
seen.add(value);
|
|
77
|
+
try {
|
|
78
|
+
if (Array.isArray(value))
|
|
79
|
+
return value.map((v) => redactValue(v, extraKeys, depth + 1, seen));
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const [key, v] of Object.entries(value)) {
|
|
82
|
+
out[key] = isSecretKey(key, extraKeys)
|
|
83
|
+
? REDACTED
|
|
84
|
+
: redactValue(v, extraKeys, depth + 1, seen);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
// `seen` holds the path from the root: a cycle is an object inside itself.
|
|
90
|
+
seen.delete(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"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"}
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W3C Trace Context (`traceparent`), the ids every record written inside a
|
|
3
|
+
* request or a run carries: honour a caller's trace, or start one, and give
|
|
4
|
+
* each request its own span. Pure functions on Web Crypto, so they run in
|
|
5
|
+
* Node and in workers alike. No spans are recorded or exported here.
|
|
6
|
+
*/
|
|
7
|
+
export interface TraceContext {
|
|
8
|
+
/** 32 lowercase hex characters, not all zero. */
|
|
9
|
+
readonly traceId: string;
|
|
10
|
+
/** 16 lowercase hex characters, not all zero: this span. */
|
|
11
|
+
readonly spanId: string;
|
|
12
|
+
/** The span this one is a child of, when there is one. */
|
|
13
|
+
readonly parentSpanId?: string;
|
|
14
|
+
/** Trace flags, two hex characters; `01` is sampled. */
|
|
15
|
+
readonly flags: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* An incoming `traceparent`'s trace id and parent span, or `undefined`
|
|
19
|
+
* when the header is absent or malformed: then the caller starts a fresh
|
|
20
|
+
* trace (a bad header is replaced, never trusted). Version `ff` is
|
|
21
|
+
* invalid; a future version is read by its first four fields, as the
|
|
22
|
+
* spec says. All-zero ids are invalid.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseTraceparent(header: string | null | undefined): {
|
|
25
|
+
readonly traceId: string;
|
|
26
|
+
readonly parentSpanId: string;
|
|
27
|
+
readonly flags: string;
|
|
28
|
+
} | undefined;
|
|
29
|
+
/** A fresh trace: a new trace id and a first span, sampled. */
|
|
30
|
+
export declare function newTraceContext(): TraceContext;
|
|
31
|
+
/** A new span in the same trace, whose parent is `parent`'s span. */
|
|
32
|
+
export declare function childSpan(parent: TraceContext): TraceContext;
|
|
33
|
+
/**
|
|
34
|
+
* The context for a request: a child span of the caller's trace when its
|
|
35
|
+
* `traceparent` is valid, else a fresh trace.
|
|
36
|
+
*/
|
|
37
|
+
export declare function traceFromHeader(header: string | null | undefined): TraceContext;
|
|
38
|
+
/** The `traceparent` (and `traceresponse`) value for a context: `00-<trace>-<span>-<flags>`. */
|
|
39
|
+
export declare function formatTraceparent(ctx: TraceContext): string;
|
|
40
|
+
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAID;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAChC;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAgBjG;AAWD,+DAA+D;AAC/D,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAO5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAS/E;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAE3D"}
|
package/dist/trace.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
const TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(?:-.*)?$/;
|
|
4
|
+
/**
|
|
5
|
+
* An incoming `traceparent`'s trace id and parent span, or `undefined`
|
|
6
|
+
* when the header is absent or malformed: then the caller starts a fresh
|
|
7
|
+
* trace (a bad header is replaced, never trusted). Version `ff` is
|
|
8
|
+
* invalid; a future version is read by its first four fields, as the
|
|
9
|
+
* spec says. All-zero ids are invalid.
|
|
10
|
+
*/
|
|
11
|
+
export function parseTraceparent(header) {
|
|
12
|
+
if (typeof header !== 'string')
|
|
13
|
+
return undefined;
|
|
14
|
+
// Hex must be lowercase (the spec): an uppercase header is malformed.
|
|
15
|
+
const m = TRACEPARENT.exec(header.trim());
|
|
16
|
+
if (m === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
const [, version, traceId, parentSpanId, flags] = m;
|
|
19
|
+
if (version === 'ff')
|
|
20
|
+
return undefined;
|
|
21
|
+
if (version === '00' && header.trim().length !== 55)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId))
|
|
24
|
+
return undefined;
|
|
25
|
+
return { traceId, parentSpanId, flags };
|
|
26
|
+
}
|
|
27
|
+
function randomHex(bytes) {
|
|
28
|
+
for (;;) {
|
|
29
|
+
const buf = new Uint8Array(bytes);
|
|
30
|
+
globalThis.crypto.getRandomValues(buf);
|
|
31
|
+
const hex = [...buf].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
32
|
+
if (!/^0+$/.test(hex))
|
|
33
|
+
return hex;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** A fresh trace: a new trace id and a first span, sampled. */
|
|
37
|
+
export function newTraceContext() {
|
|
38
|
+
return { traceId: randomHex(16), spanId: randomHex(8), flags: '01' };
|
|
39
|
+
}
|
|
40
|
+
/** A new span in the same trace, whose parent is `parent`'s span. */
|
|
41
|
+
export function childSpan(parent) {
|
|
42
|
+
return {
|
|
43
|
+
traceId: parent.traceId,
|
|
44
|
+
spanId: randomHex(8),
|
|
45
|
+
parentSpanId: parent.spanId,
|
|
46
|
+
flags: parent.flags,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The context for a request: a child span of the caller's trace when its
|
|
51
|
+
* `traceparent` is valid, else a fresh trace.
|
|
52
|
+
*/
|
|
53
|
+
export function traceFromHeader(header) {
|
|
54
|
+
const incoming = parseTraceparent(header);
|
|
55
|
+
if (incoming === undefined)
|
|
56
|
+
return newTraceContext();
|
|
57
|
+
return {
|
|
58
|
+
traceId: incoming.traceId,
|
|
59
|
+
spanId: randomHex(8),
|
|
60
|
+
parentSpanId: incoming.parentSpanId,
|
|
61
|
+
flags: incoming.flags,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** The `traceparent` (and `traceresponse`) value for a context: `00-<trace>-<span>-<flags>`. */
|
|
65
|
+
export function formatTraceparent(ctx) {
|
|
66
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${ctx.flags}`;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=trace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.js","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAoBjC,MAAM,WAAW,GAAG,qEAAqE,CAAC;AAE1F;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAiC;IAEjC,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACjD,sEAAsE;IACtE,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1C,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,GAAG,CAMjD,CAAC;IACF,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACvC,IAAI,OAAO,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACtE,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC;QAAE,OAAO,SAAS,CAAC;IACxE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;AAC1C,CAAC;AAED,SAAS,SAAS,CAAC,KAAa;IAC9B,SAAS,CAAC;QACR,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;QAClC,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,GAAG,CAAC;IACpC,CAAC;AACH,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,eAAe;IAC7B,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACvE,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,SAAS,CAAC,MAAoB;IAC5C,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC,MAAM;QAC3B,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAiC;IAC/D,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,eAAe,EAAE,CAAC;IACrD,OAAO;QACL,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;QACpB,YAAY,EAAE,QAAQ,CAAC,YAAY;QACnC,KAAK,EAAE,QAAQ,CAAC,KAAK;KACtB,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,iBAAiB,CAAC,GAAiB;IACjD,OAAO,MAAM,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;AACxD,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LogLevel } from './levels.js';
|
|
2
|
+
/** A record's fields: values, never text pasted into the message. */
|
|
3
|
+
export type LogFields = Readonly<Record<string, unknown>>;
|
|
4
|
+
/**
|
|
5
|
+
* One log record, the JSON format's line. `time` (RFC 3339), `level`,
|
|
6
|
+
* `severity` (Cloud Logging's name for the level), `subsystem` and
|
|
7
|
+
* `message` are always there; the correlation fields come next when
|
|
8
|
+
* known, then the event's own fields. An error is `err`.
|
|
9
|
+
*/
|
|
10
|
+
export interface LogRecord {
|
|
11
|
+
readonly time: string;
|
|
12
|
+
readonly level: LogLevel;
|
|
13
|
+
readonly severity: string;
|
|
14
|
+
readonly subsystem: string;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly traceId?: string;
|
|
17
|
+
readonly spanId?: string;
|
|
18
|
+
readonly requestId?: string;
|
|
19
|
+
readonly tenantId?: string;
|
|
20
|
+
readonly projectId?: string;
|
|
21
|
+
readonly runId?: string;
|
|
22
|
+
readonly err?: SerializedError;
|
|
23
|
+
readonly [field: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
/** An error, as a record carries it. `stack` at `error`, or when the logger is at `debug` or below. */
|
|
26
|
+
export interface SerializedError {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly message: string;
|
|
29
|
+
readonly code?: string;
|
|
30
|
+
readonly stack?: string;
|
|
31
|
+
readonly cause?: SerializedError | string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* How one record is written, beyond its fields.
|
|
35
|
+
*
|
|
36
|
+
* - `inMessage`: the fields its message already states. The access
|
|
37
|
+
* line `GET /v1/runs/:runId 200 12ms` states `method`, `route`,
|
|
38
|
+
* `status` and `durationMs`. The pretty format leaves them out, so
|
|
39
|
+
* a terminal line doesn't say everything twice; JSON keeps every
|
|
40
|
+
* field, for queries.
|
|
41
|
+
*/
|
|
42
|
+
export interface LogOptions {
|
|
43
|
+
readonly inMessage?: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A logger. `child` returns one whose records all carry `bindings`
|
|
47
|
+
* (`log.child({ subsystem: 'leases' })`, `log.child({ runId })`); a
|
|
48
|
+
* child's `subsystem` replaces its parent's, so name it in full
|
|
49
|
+
* (`kernel.sweeper`). Pass an error as `{ err: error }`.
|
|
50
|
+
*/
|
|
51
|
+
export interface Logger {
|
|
52
|
+
error(message: string, fields?: LogFields, options?: LogOptions): void;
|
|
53
|
+
warn(message: string, fields?: LogFields, options?: LogOptions): void;
|
|
54
|
+
info(message: string, fields?: LogFields, options?: LogOptions): void;
|
|
55
|
+
debug(message: string, fields?: LogFields, options?: LogOptions): void;
|
|
56
|
+
trace(message: string, fields?: LogFields, options?: LogOptions): void;
|
|
57
|
+
child(bindings: LogFields): Logger;
|
|
58
|
+
/** Whether a record at `level` would be written: skip building costly fields when it wouldn't. */
|
|
59
|
+
isLevelEnabled(level: LogLevel): boolean;
|
|
60
|
+
}
|
|
61
|
+
export type LogFormat = 'json' | 'pretty';
|
|
62
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,qEAAqE;AACrE,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,eAAe,CAAC;IAC/B,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACnC;AAED,uGAAuG;AACvG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,MAAM,CAAC;CAC3C;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;;;GAKG;AACH,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACvE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACtE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACtE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACvE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACvE,KAAK,CAAC,QAAQ,EAAE,SAAS,GAAG,MAAM,CAAC;IACnC,kGAAkG;IAClG,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC1C;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC"}
|