@kitr/logger 0.0.0-stage → 0.2.1
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 +21 -0
- package/README.md +12 -2
- package/dist/index.d.ts +71 -0
- package/dist/index.js +146 -0
- package/dist/kitr.d.ts +7 -0
- package/dist/kitr.js +16 -0
- package/docs/api.md +127 -0
- package/docs/index.md +455 -0
- package/docs/overview.md +72 -0
- package/package.json +55 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Platon Podkopov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kitr/logger
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Logger.** Write readable logs in development and structured logs in production.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm kitr add logger
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`kitr add` installs the package and wires it into your app: the provider in `kitrrc.ts`, its config file and its `AGENTS.md` section. Import it from `@kitr/logger`.
|
|
10
|
+
|
|
11
|
+
Read the guide at https://kitr.dev/packages/logger.
|
|
12
|
+
|
|
13
|
+
Part of [kitr](https://kitr.dev), the TypeScript framework where everything is an add-on. MIT licensed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** A log level, lowest to highest; `silent` drops everything and is for configuration only. */
|
|
2
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
|
|
3
|
+
/** One log line as an object: what `write` receives and what `json` format serialises. */
|
|
4
|
+
export interface LogRecord {
|
|
5
|
+
/** The level it was written at. */
|
|
6
|
+
level: LogLevel;
|
|
7
|
+
/** When it was written, as an ISO 8601 string. */
|
|
8
|
+
time: string;
|
|
9
|
+
/** The logger's name, when it has one. */
|
|
10
|
+
name?: string;
|
|
11
|
+
/** The message, or the error's message. */
|
|
12
|
+
msg: string;
|
|
13
|
+
/** Bindings, fields, `err` for errors and `durationMs` from `time()`. */
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** Options for `createLogger`, and the shape of `config/logger.ts`. */
|
|
17
|
+
export interface LoggerOptions {
|
|
18
|
+
/** The lowest level written. Default `info`. */
|
|
19
|
+
level?: LogLevel;
|
|
20
|
+
/** Shown as `[name]` in pretty output and as `name` in JSON. */
|
|
21
|
+
name?: string;
|
|
22
|
+
/** 'pretty' for terminals, 'json' for production. Default: pretty in dev, json otherwise. */
|
|
23
|
+
format?: 'pretty' | 'json';
|
|
24
|
+
/** Fields merged into every record (e.g. requestId). */
|
|
25
|
+
bindings?: Record<string, unknown>;
|
|
26
|
+
/** Where lines go. Default: console. */
|
|
27
|
+
write?: (line: string, record: LogRecord) => void;
|
|
28
|
+
}
|
|
29
|
+
/** A logger: one method per level, child loggers that carry fields, and timing. Bound as `logger` by the provider. */
|
|
30
|
+
export interface Logger {
|
|
31
|
+
/** The lowest level written. Settable at run time; children copy it when created. */
|
|
32
|
+
level: LogLevel;
|
|
33
|
+
/** Write at `trace`: step-by-step detail. */
|
|
34
|
+
trace(msg: string, fields?: Record<string, unknown>): void;
|
|
35
|
+
/** Write at `debug`: detail for development. */
|
|
36
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
37
|
+
/** Write at `info`: something that happened. */
|
|
38
|
+
info(msg: string, fields?: Record<string, unknown>): void;
|
|
39
|
+
/** Write at `warn`: something odd that did not fail. */
|
|
40
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
41
|
+
/** Write at `error`. Pass an `Error` to keep its name, message, stack and code under `err`. */
|
|
42
|
+
error(msg: string | Error, fields?: Record<string, unknown>): void;
|
|
43
|
+
/** Write at `fatal`. Pass an `Error` to keep its name, message, stack and code under `err`. */
|
|
44
|
+
fatal(msg: string | Error, fields?: Record<string, unknown>): void;
|
|
45
|
+
/** A logger that adds `bindings` to every record, optionally under another name. */
|
|
46
|
+
child(bindings: Record<string, unknown>, name?: string): Logger;
|
|
47
|
+
/** Run `fn`, then log `msg` at info with `durationMs`, even when it throws; returns its result. */
|
|
48
|
+
time<T>(msg: string, fn: () => Promise<T>, fields?: Record<string, unknown>): Promise<T>;
|
|
49
|
+
}
|
|
50
|
+
/** Create a logger without the provider, for scripts, packages and tests. */
|
|
51
|
+
export declare function createLogger(options?: LoggerOptions): Logger;
|
|
52
|
+
/** The options in `config/logger.ts`; the same as `LoggerOptions`. */
|
|
53
|
+
export type LoggerConfig = LoggerOptions;
|
|
54
|
+
declare module '@kitr/core' {
|
|
55
|
+
interface ConfigRegistry {
|
|
56
|
+
logger: LoggerConfig;
|
|
57
|
+
}
|
|
58
|
+
interface ContainerBindings {
|
|
59
|
+
logger: Logger;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Types `config/logger.ts`. Returns its argument unchanged. */
|
|
63
|
+
export declare const defineConfig: <const C extends LoggerOptions>(config: C) => C;
|
|
64
|
+
/**
|
|
65
|
+
* Binds `logger` into the container from `config/logger.ts`, on the server and in the browser.
|
|
66
|
+
* In development with devtools, every record is also mirrored to the Log panel.
|
|
67
|
+
*/
|
|
68
|
+
export declare const loggerProvider: import("@kitr/core").KitrProvider;
|
|
69
|
+
/** A logger that discards everything, for tests. */
|
|
70
|
+
export declare const silentLogger: Logger;
|
|
71
|
+
export { loggerProvider as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createDefineConfig, defineProvider, getDevtools } from "@kitr/core";
|
|
2
|
+
// src/index.ts
|
|
3
|
+
const levels = {
|
|
4
|
+
trace: 10,
|
|
5
|
+
debug: 20,
|
|
6
|
+
info: 30,
|
|
7
|
+
warn: 40,
|
|
8
|
+
error: 50,
|
|
9
|
+
fatal: 60,
|
|
10
|
+
silent: 100
|
|
11
|
+
};
|
|
12
|
+
const colors = {
|
|
13
|
+
trace: "\x1B[90m",
|
|
14
|
+
debug: "\x1B[36m",
|
|
15
|
+
info: "\x1B[32m",
|
|
16
|
+
warn: "\x1B[33m",
|
|
17
|
+
error: "\x1B[31m",
|
|
18
|
+
fatal: "\x1B[35m",
|
|
19
|
+
silent: ""
|
|
20
|
+
};
|
|
21
|
+
/** Create a logger without the provider, for scripts, packages and tests. */
|
|
22
|
+
function createLogger(options = {}) {
|
|
23
|
+
const isDev = globalThis.process?.env?.NODE_ENV !== "production";
|
|
24
|
+
const browserConsole = typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined" && !options.format && !options.write;
|
|
25
|
+
const format = options.format ?? (isDev ? "pretty" : "json");
|
|
26
|
+
const write = options.write ?? (browserConsole ? (_line, record) => browserWrite(record) : (line, record) => {
|
|
27
|
+
if (levels[record.level] >= levels.error) console.error(line);
|
|
28
|
+
else console.log(line);
|
|
29
|
+
});
|
|
30
|
+
const build = (bindings, name, level = options.level ?? "info") => {
|
|
31
|
+
const emit = (lvl, msg, fields) => {
|
|
32
|
+
if (levels[lvl] < levels[logger.level]) return;
|
|
33
|
+
const record = {
|
|
34
|
+
level: lvl,
|
|
35
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
36
|
+
...name ? { name } : {},
|
|
37
|
+
...withoutReserved(bindings),
|
|
38
|
+
...withoutReserved(fields),
|
|
39
|
+
msg: ""
|
|
40
|
+
};
|
|
41
|
+
if (msg instanceof Error) {
|
|
42
|
+
record.msg = msg.message;
|
|
43
|
+
record.err = {
|
|
44
|
+
name: msg.name,
|
|
45
|
+
message: msg.message,
|
|
46
|
+
stack: msg.stack,
|
|
47
|
+
...msg.code ? { code: msg.code } : {}
|
|
48
|
+
};
|
|
49
|
+
} else record.msg = msg;
|
|
50
|
+
write(format === "json" ? JSON.stringify(record) : pretty(record), record);
|
|
51
|
+
};
|
|
52
|
+
const logger = {
|
|
53
|
+
level,
|
|
54
|
+
trace: (m, f) => emit("trace", m, f),
|
|
55
|
+
debug: (m, f) => emit("debug", m, f),
|
|
56
|
+
info: (m, f) => emit("info", m, f),
|
|
57
|
+
warn: (m, f) => emit("warn", m, f),
|
|
58
|
+
error: (m, f) => emit("error", m, f),
|
|
59
|
+
fatal: (m, f) => emit("fatal", m, f),
|
|
60
|
+
child: (extra, childName) => build({
|
|
61
|
+
...bindings,
|
|
62
|
+
...extra
|
|
63
|
+
}, childName ?? name, logger.level),
|
|
64
|
+
time: async (msg, fn, fields) => {
|
|
65
|
+
const start = performance.now();
|
|
66
|
+
try {
|
|
67
|
+
return await fn();
|
|
68
|
+
} finally {
|
|
69
|
+
emit("info", msg, {
|
|
70
|
+
...fields,
|
|
71
|
+
durationMs: Math.round(performance.now() - start)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return logger;
|
|
77
|
+
};
|
|
78
|
+
return build(options.bindings ?? {}, options.name);
|
|
79
|
+
}
|
|
80
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
81
|
+
"level",
|
|
82
|
+
"time",
|
|
83
|
+
"msg"
|
|
84
|
+
]);
|
|
85
|
+
/** Fields may not overwrite `level`, `time` or `msg`: filtering and formatting rely on them. */
|
|
86
|
+
function withoutReserved(fields) {
|
|
87
|
+
if (!fields) return {};
|
|
88
|
+
let out;
|
|
89
|
+
for (const key of Object.keys(fields)) {
|
|
90
|
+
if (!reserved.has(key)) continue;
|
|
91
|
+
out ??= { ...fields };
|
|
92
|
+
delete out[key];
|
|
93
|
+
}
|
|
94
|
+
return out ?? fields;
|
|
95
|
+
}
|
|
96
|
+
function browserWrite(r) {
|
|
97
|
+
const { level, time: _time, name, msg, ...rest } = r;
|
|
98
|
+
const method = level === "error" || level === "fatal" ? "error" : level === "warn" ? "warn" : level === "info" ? "info" : "debug";
|
|
99
|
+
const text = `${name ? `[${name}] ` : ""}${msg}`;
|
|
100
|
+
if (Object.keys(rest).length) console[method](text, rest);
|
|
101
|
+
else console[method](text);
|
|
102
|
+
}
|
|
103
|
+
function pretty(r) {
|
|
104
|
+
const { level, time, name, msg, err, ...rest } = r;
|
|
105
|
+
const tag = `${colors[level]}${level.toUpperCase().padEnd(5)}\x1b[0m`;
|
|
106
|
+
const extras = Object.keys(rest).length ? ` \x1b[90m${JSON.stringify(rest)}\x1b[0m` : "";
|
|
107
|
+
const stack = err && typeof err === "object" && "stack" in err ? `\n${err.stack}` : "";
|
|
108
|
+
return `${tag} \x1b[90m${time.slice(11, 23)}\x1b[0m ${name ? `[${name}] ` : ""}${msg}${extras}${stack}`;
|
|
109
|
+
}
|
|
110
|
+
/** Types `config/logger.ts`. Returns its argument unchanged. */
|
|
111
|
+
const defineConfig = createDefineConfig();
|
|
112
|
+
/**
|
|
113
|
+
* Binds `logger` into the container from `config/logger.ts`, on the server and in the browser.
|
|
114
|
+
* In development with devtools, every record is also mirrored to the Log panel.
|
|
115
|
+
*/
|
|
116
|
+
const loggerProvider = defineProvider({
|
|
117
|
+
name: "logger",
|
|
118
|
+
register(app) {
|
|
119
|
+
app.container.singleton("logger", () => {
|
|
120
|
+
const config = app.config.get("logger", {});
|
|
121
|
+
const hub = getDevtools(app);
|
|
122
|
+
if (!hub) return createLogger(config);
|
|
123
|
+
const write = config.write ?? ((line) => console.log(line));
|
|
124
|
+
return createLogger({
|
|
125
|
+
...config,
|
|
126
|
+
write: (line, record) => {
|
|
127
|
+
write(line, record);
|
|
128
|
+
const { level, msg, time, name, ...fields } = record;
|
|
129
|
+
hub.log("logger", msg, Object.keys(fields).length ? fields : void 0, levelOf(level));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
function levelOf(level) {
|
|
136
|
+
if (level === "trace" || level === "debug") return "debug";
|
|
137
|
+
if (level === "warn") return "warn";
|
|
138
|
+
if (level === "error" || level === "fatal") return "error";
|
|
139
|
+
return "info";
|
|
140
|
+
}
|
|
141
|
+
/** A logger that discards everything, for tests. */
|
|
142
|
+
const silentLogger = createLogger({
|
|
143
|
+
level: "silent",
|
|
144
|
+
write: () => {}
|
|
145
|
+
});
|
|
146
|
+
export { createLogger, loggerProvider as default, loggerProvider, defineConfig, silentLogger };
|
package/dist/kitr.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** `kitr add logger`: the structured logger with levels, children and request ids. */
|
|
2
|
+
declare const _default: {
|
|
3
|
+
readonly name: "logger";
|
|
4
|
+
readonly add: (ctx: import("@kitr/core/package").AddContext) => void;
|
|
5
|
+
readonly remove: (ctx: import("@kitr/core/package").AddContext) => void;
|
|
6
|
+
};
|
|
7
|
+
export { _default as default };
|
package/dist/kitr.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { definePackage } from "@kitr/core/package";
|
|
2
|
+
// src/kitr.ts
|
|
3
|
+
/** `kitr add logger`: the structured logger with levels, children and request ids. */
|
|
4
|
+
var kitr_default = definePackage({
|
|
5
|
+
name: "logger",
|
|
6
|
+
add(ctx) {
|
|
7
|
+
ctx.provider.add("@kitr/logger");
|
|
8
|
+
ctx.config("logger", `import { defineConfig } from '@kitr/logger'\n\nexport default defineConfig({\n name: 'app',\n level: (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') ?? 'info',\n // 'pretty' in dev, 'json' in production by default.\n})\n`);
|
|
9
|
+
ctx.agents.add("logger", `- Log through \`ctx.logger\` (request-scoped, carries the request id) or \`app.container.use('logger')\`. Never \`console.log\` in server code.`);
|
|
10
|
+
},
|
|
11
|
+
remove(ctx) {
|
|
12
|
+
ctx.provider.remove("@kitr/logger");
|
|
13
|
+
ctx.agents.remove("logger");
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
export { kitr_default as default };
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: API reference
|
|
3
|
+
description: Every export of @kitr/logger, with its signature.
|
|
4
|
+
order: 2
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# API reference
|
|
8
|
+
|
|
9
|
+
Every export of `@kitr/logger`, read from the published type definitions. The [guide](/packages/logger/guide) shows them in use.
|
|
10
|
+
|
|
11
|
+
## @kitr/logger
|
|
12
|
+
|
|
13
|
+
### createLogger
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
function createLogger(options?: LoggerOptions): Logger;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Create a logger without the provider, for scripts, packages and tests.
|
|
20
|
+
|
|
21
|
+
### defineConfig
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const defineConfig: <const C extends LoggerOptions>(config: C) => C;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Types `config/logger.ts`. Returns its argument unchanged.
|
|
28
|
+
|
|
29
|
+
### loggerProvider
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const loggerProvider: import("@kitr/core").KitrProvider;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Binds `logger` into the container from `config/logger.ts`, on the server and in the browser.
|
|
36
|
+
In development with devtools, every record is also mirrored to the Log panel.
|
|
37
|
+
|
|
38
|
+
### silentLogger
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const silentLogger: Logger;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A logger that discards everything, for tests.
|
|
45
|
+
|
|
46
|
+
### Logger
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
interface Logger {
|
|
50
|
+
/** The lowest level written. Settable at run time; children copy it when created. */
|
|
51
|
+
level: LogLevel;
|
|
52
|
+
/** Write at `trace`: step-by-step detail. */
|
|
53
|
+
trace(msg: string, fields?: Record<string, unknown>): void;
|
|
54
|
+
/** Write at `debug`: detail for development. */
|
|
55
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
56
|
+
/** Write at `info`: something that happened. */
|
|
57
|
+
info(msg: string, fields?: Record<string, unknown>): void;
|
|
58
|
+
/** Write at `warn`: something odd that did not fail. */
|
|
59
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
60
|
+
/** Write at `error`. Pass an `Error` to keep its name, message, stack and code under `err`. */
|
|
61
|
+
error(msg: string | Error, fields?: Record<string, unknown>): void;
|
|
62
|
+
/** Write at `fatal`. Pass an `Error` to keep its name, message, stack and code under `err`. */
|
|
63
|
+
fatal(msg: string | Error, fields?: Record<string, unknown>): void;
|
|
64
|
+
/** A logger that adds `bindings` to every record, optionally under another name. */
|
|
65
|
+
child(bindings: Record<string, unknown>, name?: string): Logger;
|
|
66
|
+
/** Run `fn`, then log `msg` at info with `durationMs`, even when it throws; returns its result. */
|
|
67
|
+
time<T>(msg: string, fn: () => Promise<T>, fields?: Record<string, unknown>): Promise<T>;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A logger: one method per level, child loggers that carry fields, and timing. Bound as `logger` by the provider.
|
|
72
|
+
|
|
73
|
+
### LoggerOptions
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
interface LoggerOptions {
|
|
77
|
+
/** The lowest level written. Default `info`. */
|
|
78
|
+
level?: LogLevel;
|
|
79
|
+
/** Shown as `[name]` in pretty output and as `name` in JSON. */
|
|
80
|
+
name?: string;
|
|
81
|
+
/** 'pretty' for terminals, 'json' for production. Default: pretty in dev, json otherwise. */
|
|
82
|
+
format?: 'pretty' | 'json';
|
|
83
|
+
/** Fields merged into every record (e.g. requestId). */
|
|
84
|
+
bindings?: Record<string, unknown>;
|
|
85
|
+
/** Where lines go. Default: console. */
|
|
86
|
+
write?: (line: string, record: LogRecord) => void;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Options for `createLogger`, and the shape of `config/logger.ts`.
|
|
91
|
+
|
|
92
|
+
### LogRecord
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
interface LogRecord {
|
|
96
|
+
/** The level it was written at. */
|
|
97
|
+
level: LogLevel;
|
|
98
|
+
/** When it was written, as an ISO 8601 string. */
|
|
99
|
+
time: string;
|
|
100
|
+
/** The logger's name, when it has one. */
|
|
101
|
+
name?: string;
|
|
102
|
+
/** The message, or the error's message. */
|
|
103
|
+
msg: string;
|
|
104
|
+
/** Bindings, fields, `err` for errors and `durationMs` from `time()`. */
|
|
105
|
+
[key: string]: unknown;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
One log line as an object: what `write` receives and what `json` format serialises.
|
|
110
|
+
|
|
111
|
+
### LoggerConfig
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
type LoggerConfig = LoggerOptions;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The options in `config/logger.ts`; the same as `LoggerOptions`.
|
|
118
|
+
|
|
119
|
+
### LogLevel
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A log level, lowest to highest; `silent` drops everything and is for configuration only.
|
|
126
|
+
|
|
127
|
+
<!-- GENERATED by scripts/api-docs.mjs from the type definitions. Do not edit. -->
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Logger
|
|
3
|
+
description: Write readable logs in development and structured logs in production.
|
|
4
|
+
order: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Logger
|
|
8
|
+
|
|
9
|
+
When Ada reports that her order never shipped, the logs are where you find out why. In development you want lines you can read at a glance; in production you want one JSON object per line, so your log service can filter by `orderId` or `requestId`. `@kitr/logger` writes both from the same calls.
|
|
10
|
+
|
|
11
|
+
> [!SUMMARY]
|
|
12
|
+
> Readable logs in development and JSON logs in production, with levels, child loggers and a request id on every server line. Every kitr app with a server includes it; the overview is at [/packages/logger](/packages/logger).
|
|
13
|
+
|
|
14
|
+
It runs on the server and in the browser and is for lines a developer reads. For alerts with the user and the steps that led to an error, use [monitoring](/packages/monitoring); to count what users do, use [analytics](/packages/analytics).
|
|
15
|
+
|
|
16
|
+
The examples follow Acme's app at `app.acme.com`, where Ada, Grace and Linus read the logs when something goes wrong.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
kitr add logger
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
<details>
|
|
25
|
+
<summary>Everything kitr add changes</summary>
|
|
26
|
+
|
|
27
|
+
What `kitr add` does:
|
|
28
|
+
|
|
29
|
+
- Installs `@kitr/logger`.
|
|
30
|
+
- Adds `'@kitr/logger'` to `packages` and `() => import('@kitr/logger')` to `providers` in `kitrrc.ts`.
|
|
31
|
+
- Writes `config/logger.ts` (shown in [Configure](#configure)) and regenerates `config/index.ts`.
|
|
32
|
+
- Adds a `## kitr: logger` section to `AGENTS.md` telling coding agents to log through `ctx.logger` or `app.container.use('logger')`, never `console.log` in server code.
|
|
33
|
+
|
|
34
|
+
The [server](/packages/server) package requires the logger, so `kitr add server` adds it first and adds `LOG_LEVEL=info` to `.env.example`. Every kit with a server platform already has it.
|
|
35
|
+
|
|
36
|
+
`kitr remove logger` reverses these steps and deletes `config/logger.ts`.
|
|
37
|
+
|
|
38
|
+
</details>
|
|
39
|
+
|
|
40
|
+
## Configure
|
|
41
|
+
|
|
42
|
+
`kitr add logger` writes this file:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// config/logger.ts
|
|
46
|
+
import { defineConfig } from '@kitr/logger'
|
|
47
|
+
|
|
48
|
+
export default defineConfig({
|
|
49
|
+
name: 'app',
|
|
50
|
+
level: (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') ?? 'info',
|
|
51
|
+
// 'pretty' in dev, 'json' in production by default.
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Option | Default | Description |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `level` | `'info'` | The lowest level written: `trace`, `debug`, `info`, `warn`, `error`, `fatal` or `silent`. |
|
|
58
|
+
| `name` | none | Shown in brackets before each message in pretty output, and as `name` in JSON. |
|
|
59
|
+
| `format` | `'pretty'` unless `NODE_ENV` is `production`, then `'json'` | How each line is written. |
|
|
60
|
+
| `bindings` | `{}` | Fields added to every record, such as `{ region: 'eu-west-1' }`. |
|
|
61
|
+
| `write(line, record)` | the console | Where lines go. The default writes `error` and `fatal` to `console.error` (stderr) and the rest to `console.log` (stdout). |
|
|
62
|
+
|
|
63
|
+
| Variable | Used for |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `LOG_LEVEL` | The lowest level to print, such as `info`. |
|
|
66
|
+
|
|
67
|
+
The default `format` reads `process.env.NODE_ENV`, not the app's mode. In the browser the logger calls the console directly; see [Log in the browser](#log-in-the-browser).
|
|
68
|
+
|
|
69
|
+
## Write your first log line
|
|
70
|
+
|
|
71
|
+
In a controller, the logger is `ctx.logger`. It is a child of the app's logger that adds the request id to every line:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// server/controllers/orders-controller.ts
|
|
75
|
+
import type { HttpContext } from '@kitr/api'
|
|
76
|
+
|
|
77
|
+
export default class OrdersController {
|
|
78
|
+
async ship({ logger, params }: HttpContext<{ id: string }>) {
|
|
79
|
+
logger.info('order shipped', { orderId: params.id })
|
|
80
|
+
return { orderId: params.id, status: 'shipped' }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Call `POST /api/orders/1042/ship` in development and the server prints:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
INFO 14:03:22.418 [app] order shipped {"requestId":"9b2f6c1e-4f3a-4c1d-8e7a-2b5d0c9e1f44","orderId":"1042"}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
In production, where `NODE_ENV=production`, the same call writes one JSON object:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{"level":"info","time":"2026-11-07T14:03:22.418Z","name":"app","requestId":"9b2f6c1e-4f3a-4c1d-8e7a-2b5d0c9e1f44","orderId":"1042","msg":"order shipped"}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Keep the message fixed and put the variable parts in fields: `'order shipped'` with `{ orderId }`, not `` `order ${id} shipped` ``. Your log service can then count and filter the lines.
|
|
98
|
+
|
|
99
|
+
## Write at a level
|
|
100
|
+
|
|
101
|
+
Each level has a method. Lines below the configured `level` are dropped before formatting, so a `debug` call in a hot path is nearly free in production.
|
|
102
|
+
|
|
103
|
+
| Level | Method | Use it for |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `trace` | `logger.trace(msg, fields?)` | Step-by-step detail you only turn on while debugging one thing. |
|
|
106
|
+
| `debug` | `logger.debug(msg, fields?)` | Detail for development: the server's request lines, cache hits. |
|
|
107
|
+
| `info` | `logger.info(msg, fields?)` | Things that happened: an order shipped, a job finished. |
|
|
108
|
+
| `warn` | `logger.warn(msg, fields?)` | Something odd that did not fail: a slow query, a retry. |
|
|
109
|
+
| `error` | `logger.error(msgOrError, fields?)` | A failure someone should look at. |
|
|
110
|
+
| `fatal` | `logger.fatal(msgOrError, fields?)` | The process cannot continue. |
|
|
111
|
+
|
|
112
|
+
`silent` is a level for configuration only: it drops everything.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// server/services/project-service.ts
|
|
116
|
+
import type { KitrApp } from 'kitr/app'
|
|
117
|
+
|
|
118
|
+
export async function syncProject(app: KitrApp, projectId: string) {
|
|
119
|
+
const logger = app.container.use('logger')
|
|
120
|
+
logger.debug('loading project', { projectId })
|
|
121
|
+
logger.warn('slow query', { projectId, ms: 1840 })
|
|
122
|
+
logger.info('project synced', { projectId, tasks: 42 })
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Log an error
|
|
127
|
+
|
|
128
|
+
Pass the `Error` itself to `error()` or `fatal()`. Its message becomes `msg`, and its name, message, stack and `code` are kept under `err`:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// server/services/invoice-service.ts
|
|
132
|
+
import type { KitrApp } from 'kitr/app'
|
|
133
|
+
|
|
134
|
+
export async function generateInvoice(app: KitrApp, invoiceNumber: string) {
|
|
135
|
+
const logger = app.container.use('logger')
|
|
136
|
+
try {
|
|
137
|
+
throw Object.assign(new Error('connect ECONNREFUSED 10.0.0.5:5432'), { code: 'ECONNREFUSED' })
|
|
138
|
+
} catch (error) {
|
|
139
|
+
logger.error(error as Error, { invoiceNumber })
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
In JSON that becomes:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{"level":"error","time":"2026-11-07T14:05:10.002Z","name":"app","invoiceNumber":"INV-2026-007","msg":"connect ECONNREFUSED 10.0.0.5:5432","err":{"name":"Error","message":"connect ECONNREFUSED 10.0.0.5:5432","stack":"Error: connect ECONNREFUSED ...","code":"ECONNREFUSED"}}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
> [!WARNING] Pass the error as the first argument
|
|
152
|
+
> `logger.error('failed', { error })` writes `{}`, because an `Error`'s message and stack are not enumerable.
|
|
153
|
+
|
|
154
|
+
The server already logs every 5xx response at `error` level, so log only errors you catch and handle yourself.
|
|
155
|
+
|
|
156
|
+
## Add fields to every line
|
|
157
|
+
|
|
158
|
+
`child(bindings, name?)` returns a logger that adds `bindings` to every record, so an id travels with a piece of work:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// server/services/order-service.ts
|
|
162
|
+
import type { KitrApp } from 'kitr/app'
|
|
163
|
+
|
|
164
|
+
export async function shipOrder(app: KitrApp, orderId: string) {
|
|
165
|
+
const logger = app.container.use('logger').child({ orderId }, 'orders')
|
|
166
|
+
logger.info('picking items')
|
|
167
|
+
logger.info('label printed', { carrier: 'DHL' })
|
|
168
|
+
logger.info('order shipped')
|
|
169
|
+
return { orderId, status: 'shipped' as const }
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
INFO 14:03:22.418 [orders] picking items {"orderId":"1042"}
|
|
175
|
+
INFO 14:03:22.419 [orders] label printed {"orderId":"1042","carrier":"DHL"}
|
|
176
|
+
INFO 14:03:22.420 [orders] order shipped {"orderId":"1042"}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A child keeps its parent's bindings; fields passed in a call win over bindings with the same key. For fields every line should carry, such as the region, set `bindings` in `config/logger.ts`:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// config/logger.ts
|
|
183
|
+
import { defineConfig } from '@kitr/logger'
|
|
184
|
+
|
|
185
|
+
export default defineConfig({
|
|
186
|
+
name: 'app',
|
|
187
|
+
level: (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') ?? 'info',
|
|
188
|
+
bindings: { region: process.env.FLY_REGION ?? 'local', release: process.env.RELEASE ?? 'dev' },
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
> [!WARNING] Reserved names
|
|
193
|
+
> `level`, `time` and `msg` are reserved. A field or binding with one of those names is dropped.
|
|
194
|
+
|
|
195
|
+
## Log during a request
|
|
196
|
+
|
|
197
|
+
Every request gets its own child logger with `requestId`, taken from the `x-request-id` header or a new UUID and echoed in the response header. Grep for it to see every line one request wrote.
|
|
198
|
+
|
|
199
|
+
| Where | How |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
| A controller | `ctx.logger` |
|
|
202
|
+
| Middleware, an action or a service during a request | `useRequest().logger`, with `useRequest` from `@kitr/server` |
|
|
203
|
+
| Code that may run inside or outside a request | `tryUseRequest()?.logger ?? app.container.use('logger')` |
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// server/services/team-service.ts
|
|
207
|
+
import { useRequest } from '@kitr/server'
|
|
208
|
+
|
|
209
|
+
export function inviteToTeam(teamId: string, email: string) {
|
|
210
|
+
const { logger } = useRequest()
|
|
211
|
+
logger.info('team invite sent', { teamId, invitee: email })
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`useRequest()` throws outside a request, such as in a job or at startup.
|
|
216
|
+
|
|
217
|
+
When a request finishes, the server writes one `debug` line with the method, path, status, client IP and duration:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
DEBUG 14:03:22.431 [app] POST /api/orders/1042/ship {"requestId":"9b2f6c1e-...","status":200,"ip":"127.0.0.1","durationMs":13}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Set `LOG_LEVEL=debug` to see them.
|
|
224
|
+
|
|
225
|
+
## Log outside a request
|
|
226
|
+
|
|
227
|
+
The provider binds the logger as `logger` in the container. Jobs, scripts and providers read it from there:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// server/jobs/prune-sessions.ts
|
|
231
|
+
import { defineJob } from '@kitr/jobs'
|
|
232
|
+
|
|
233
|
+
export const pruneSessions = defineJob('prune_sessions', {
|
|
234
|
+
schedule: '0 3 * * *',
|
|
235
|
+
async run(_payload, { app, jobId }) {
|
|
236
|
+
const logger = app.container.use('logger').child({ jobId }, 'jobs')
|
|
237
|
+
logger.info('pruning expired sessions')
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The server keeps the same logger as `server.logger`, set once `server.boot()` has run.
|
|
243
|
+
|
|
244
|
+
## Log in the browser
|
|
245
|
+
|
|
246
|
+
In a component, read the logger with `useService` from [`@kitr/react`](/packages/react):
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
// app/components/projects/project-actions.tsx
|
|
250
|
+
import { useService } from '@kitr/react'
|
|
251
|
+
|
|
252
|
+
export function ArchiveProjectButton({ projectId }: { projectId: string }) {
|
|
253
|
+
const logger = useService('logger')
|
|
254
|
+
return (
|
|
255
|
+
<button type="button" onClick={() => logger.info('project archived', { projectId })}>
|
|
256
|
+
Archive
|
|
257
|
+
</button>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Browser lines go to the matching console method with the fields as an expandable object, for example `console.info('[app] project archived', { projectId })`. Set `format` or `write` for formatted lines instead. Nothing is sent to your server; to collect client-side failures, use [monitoring](/packages/monitoring).
|
|
263
|
+
|
|
264
|
+
## Time an operation
|
|
265
|
+
|
|
266
|
+
`logger.time(msg, fn, fields?)` runs an async function, logs `msg` at `info` with `durationMs` when it settles, and returns its result:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
// server/services/invoice-service.ts
|
|
270
|
+
import type { KitrApp } from 'kitr/app'
|
|
271
|
+
|
|
272
|
+
export async function renderInvoicePdf(app: KitrApp, invoiceNumber: string) {
|
|
273
|
+
const logger = app.container.use('logger').child({ invoiceNumber }, 'billing')
|
|
274
|
+
const bytes = await logger.time('rendered invoice PDF', async () => new Uint8Array(48_213), {
|
|
275
|
+
pages: 2,
|
|
276
|
+
})
|
|
277
|
+
return bytes.length
|
|
278
|
+
// => 48213
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
INFO 14:07:41.006 [billing] rendered invoice PDF {"invoiceNumber":"INV-2026-007","pages":2,"durationMs":212}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The line is written even when the function throws, and the error is rethrown. The line doesn't say it failed, so log the error separately.
|
|
287
|
+
|
|
288
|
+
## Change the level at run time
|
|
289
|
+
|
|
290
|
+
Set `level` to turn on debug output in a running process:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
// server/controllers/admin-controller.ts
|
|
294
|
+
import type { HttpContext } from '@kitr/api'
|
|
295
|
+
|
|
296
|
+
export default class AdminController {
|
|
297
|
+
async setLogLevel({ app, body }: HttpContext<{}, {}, { level: 'debug' | 'info' }>) {
|
|
298
|
+
const logger = app.container.use('logger')
|
|
299
|
+
logger.level = body.level
|
|
300
|
+
return { level: logger.level }
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A child copies its parent's level when created, so a change affects only children created afterwards, such as the next request's logger. Protect a route like this with [auth](/packages/auth).
|
|
306
|
+
|
|
307
|
+
## Choose the output format
|
|
308
|
+
|
|
309
|
+
`pretty` is for terminals: a coloured level, the UTC time, `[name]`, the message, fields as dim JSON and any stack. `json` is one record per line, with keys in this order: `level`, `time`, `name`, bindings, fields, `msg`, `err`.
|
|
310
|
+
|
|
311
|
+
Force one format everywhere, for example JSON in a staging environment that runs with `NODE_ENV=development`:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
// config/logger.ts
|
|
315
|
+
import { defineConfig } from '@kitr/logger'
|
|
316
|
+
|
|
317
|
+
export default defineConfig({
|
|
318
|
+
name: 'app',
|
|
319
|
+
level: (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') ?? 'info',
|
|
320
|
+
format: process.env.LOG_FORMAT === 'json' ? 'json' : undefined,
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Most hosting platforms collect stdout and stderr and parse JSON lines into searchable fields, so `json` in production needs nothing else.
|
|
325
|
+
|
|
326
|
+
## Send logs to a file or a service
|
|
327
|
+
|
|
328
|
+
`write(line, record)` replaces the console. It receives the formatted line and the record for every line at or above the level:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
// config/logger.ts
|
|
332
|
+
import { appendFileSync } from 'node:fs'
|
|
333
|
+
import { defineConfig } from '@kitr/logger'
|
|
334
|
+
|
|
335
|
+
export default defineConfig({
|
|
336
|
+
name: 'app',
|
|
337
|
+
format: 'json',
|
|
338
|
+
write(line, record) {
|
|
339
|
+
appendFileSync('storage/logs/app.log', `${line}\n`)
|
|
340
|
+
if (record.level === 'error' || record.level === 'fatal') console.error(line)
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
To ship to an HTTP endpoint, buffer records and send them in batches:
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
// server/log_shipper.ts
|
|
349
|
+
import type { LogRecord } from '@kitr/logger'
|
|
350
|
+
|
|
351
|
+
const buffer: LogRecord[] = []
|
|
352
|
+
|
|
353
|
+
export function shipLog(_line: string, record: LogRecord) {
|
|
354
|
+
buffer.push(record)
|
|
355
|
+
if (buffer.length >= 100) void flushLogs()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function flushLogs() {
|
|
359
|
+
const batch = buffer.splice(0)
|
|
360
|
+
if (!batch.length) return
|
|
361
|
+
await fetch('https://logs.acme.com/ingest', {
|
|
362
|
+
method: 'POST',
|
|
363
|
+
headers: { 'content-type': 'application/json' },
|
|
364
|
+
body: JSON.stringify(batch),
|
|
365
|
+
}).catch(() => {
|
|
366
|
+
// Never let logging take the app down.
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
setInterval(flushLogs, 5_000).unref()
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
// config/logger.ts
|
|
375
|
+
import { defineConfig } from '@kitr/logger'
|
|
376
|
+
import { shipLog } from '#server/log_shipper'
|
|
377
|
+
|
|
378
|
+
export default defineConfig({ name: 'app', format: 'json', write: shipLog })
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
> [!WARNING] Keep write fast and safe
|
|
382
|
+
> `write` runs synchronously on every log call, and a throw escapes the `logger.info()` that triggered it. Catch inside it.
|
|
383
|
+
|
|
384
|
+
## Create a logger without the provider
|
|
385
|
+
|
|
386
|
+
`createLogger(options)` takes the same options as `config/logger.ts`. Use it in scripts or anywhere no app exists:
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
// scripts/import_projects.ts
|
|
390
|
+
import { createLogger } from '@kitr/logger'
|
|
391
|
+
|
|
392
|
+
const logger = createLogger({ name: 'import', level: 'debug' })
|
|
393
|
+
logger.info('importing projects', { file: 'projects.csv' })
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`silentLogger` writes nothing; pass it where an API wants a logger and you want no output.
|
|
397
|
+
|
|
398
|
+
## See logs in devtools
|
|
399
|
+
|
|
400
|
+
With [devtools](/packages/devtools) installed, every record is copied to the Log panel under the `logger` channel, so server and client lines sit in one place.
|
|
401
|
+
|
|
402
|
+
## Testing
|
|
403
|
+
|
|
404
|
+
`createLogger({ write })` lets a test collect records. For code that reads the logger from the container, boot an app with the provider and a collecting `write`:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
// test/ship-order.test.ts
|
|
408
|
+
import { type LogRecord, loggerProvider } from '@kitr/logger'
|
|
409
|
+
import { createTestApp } from '@kitr/testing'
|
|
410
|
+
import { describe, expect, it } from 'vitest'
|
|
411
|
+
import { shipOrder } from '#server/services/order-service'
|
|
412
|
+
|
|
413
|
+
describe('shipOrder', () => {
|
|
414
|
+
it('logs each step with the order id', async () => {
|
|
415
|
+
const records: LogRecord[] = []
|
|
416
|
+
const app = await createTestApp({
|
|
417
|
+
providers: [loggerProvider],
|
|
418
|
+
config: {
|
|
419
|
+
logger: { level: 'debug', format: 'json', write: (_line, record) => records.push(record) },
|
|
420
|
+
},
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
await shipOrder(app, '1042')
|
|
424
|
+
|
|
425
|
+
expect(records.map((r) => r.msg)).toEqual(['picking items', 'label printed', 'order shipped'])
|
|
426
|
+
expect(records[1]).toMatchObject({
|
|
427
|
+
level: 'info',
|
|
428
|
+
name: 'orders',
|
|
429
|
+
orderId: '1042',
|
|
430
|
+
carrier: 'DHL',
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Each record is a plain object with `level`, `msg`, `name`, your fields and bindings, `err` and `durationMs`, so assert with `toMatchObject`. To check a line's exact text, assert on the first argument of `write`. To keep a noisy test quiet, use `level: 'silent'` or `silentLogger`:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// test/quiet-server.test.ts
|
|
440
|
+
import { silentLogger } from '@kitr/logger'
|
|
441
|
+
import { createServer } from '@kitr/server'
|
|
442
|
+
import { expect, it } from 'vitest'
|
|
443
|
+
|
|
444
|
+
it('answers the health check without printing', async () => {
|
|
445
|
+
const server = createServer({ logger: silentLogger })
|
|
446
|
+
const res = await server.fetch(new Request('http://localhost/healthz'))
|
|
447
|
+
expect(await res.json()).toEqual({ ok: true })
|
|
448
|
+
})
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Next steps
|
|
452
|
+
|
|
453
|
+
- [Logger API reference](/packages/logger/api): every export with its signature.
|
|
454
|
+
- [Server](/packages/server): request ids, the request logger and error logging.
|
|
455
|
+
- [Monitoring](/packages/monitoring): alerts with user and breadcrumbs when something breaks.
|
package/docs/overview.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Overview
|
|
3
|
+
description: Write readable logs in development and structured logs in production.
|
|
4
|
+
order: 0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Logger
|
|
8
|
+
|
|
9
|
+
## What it is
|
|
10
|
+
|
|
11
|
+
`@kitr/logger` is a small logger with levels, child loggers that carry fields, and two output formats: readable in development, JSON in production. On the server every request gets a child logger with its request id.
|
|
12
|
+
|
|
13
|
+
Runs on the server and in the browser. For alerts about errors, use [monitoring](/packages/monitoring).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
kitr add logger
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Adds `@kitr/logger`, its provider and `config/logger.ts`. [server](/packages/server) adds it for you. [Every change](/packages/logger/guide#install).
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
In a controller, the logger is `ctx.logger`, a child of the app's logger that adds the request id to every line:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// server/controllers/orders_controller.ts
|
|
29
|
+
import type { HttpContext } from '@kitr/api'
|
|
30
|
+
|
|
31
|
+
export default class OrdersController {
|
|
32
|
+
async ship({ logger, params }: HttpContext<{ id: string }>) {
|
|
33
|
+
logger.info('order shipped', { orderId: params.id })
|
|
34
|
+
return { orderId: params.id, status: 'shipped' }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Call `POST /api/orders/1042/ship` in development and the server prints:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
INFO 14:03:22.418 [app] order shipped {"requestId":"9b2f6c1e-4f3a-4c1d-8e7a-2b5d0c9e1f44","orderId":"1042"}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
In production, where `NODE_ENV=production`, the same call writes one JSON object per line, ready for your log service:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{"level":"info","time":"2026-11-07T14:03:22.418Z","name":"app","requestId":"9b2f6c1e-4f3a-4c1d-8e7a-2b5d0c9e1f44","orderId":"1042","msg":"order shipped"}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Keep the message fixed and put the variable parts in fields, so a log service can count every `order shipped` line. Next in the guide: [Write your first log line](/packages/logger/guide#write-your-first-log-line).
|
|
52
|
+
|
|
53
|
+
## Built on
|
|
54
|
+
|
|
55
|
+
kitr adds the request-scoped child logger, the pretty format, timing, run-time level changes and the devtools mirror.
|
|
56
|
+
|
|
57
|
+
## Problems it solves
|
|
58
|
+
|
|
59
|
+
- **Unreadable logs in development, unsearchable logs in production.** [Choose the output format](/packages/logger/guide#choose-the-output-format).
|
|
60
|
+
- **Repeating the same fields on every line.** [Add fields to every line](/packages/logger/guide#add-fields-to-every-line).
|
|
61
|
+
- **Lines you cannot tie to a request.** [Log during a request](/packages/logger/guide#log-during-a-request).
|
|
62
|
+
- **Errors logged as `[object Object]`.** [Log an error](/packages/logger/guide#log-an-error).
|
|
63
|
+
- **Timing by hand.** [Time an operation](/packages/logger/guide#time-an-operation).
|
|
64
|
+
- **Restarting to see debug output.** [Change the level at run time](/packages/logger/guide#change-the-level-at-run-time).
|
|
65
|
+
|
|
66
|
+
## Further reading
|
|
67
|
+
|
|
68
|
+
- [Guide](/packages/logger/guide).
|
|
69
|
+
- [API reference](/packages/logger/api).
|
|
70
|
+
- [Server](/packages/server).
|
|
71
|
+
- [Monitoring](/packages/monitoring) and [Devtools](/packages/devtools).
|
|
72
|
+
- [Configuration](/docs/configuration).
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitr/logger",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "@kitr/logger: a tiny structured logger with levels, children and pretty/JSON output. No dependencies.",
|
|
5
|
+
"kitr": {
|
|
6
|
+
"name": "logger",
|
|
7
|
+
"title": "Logger",
|
|
8
|
+
"summary": "Write readable logs in development and structured logs in production.",
|
|
9
|
+
"category": "dev-tools",
|
|
10
|
+
"runsOn": "both",
|
|
11
|
+
"docs": "docs",
|
|
12
|
+
"setup": "./kitr"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/kitrjs/kitr.git",
|
|
19
|
+
"directory": "packages/logger"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://kitr.dev/packages/logger",
|
|
22
|
+
"bugs": "https://github.com/kitrjs/kitr/issues",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./kitr": {
|
|
33
|
+
"types": "./dist/kitr.d.ts",
|
|
34
|
+
"default": "./dist/kitr.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"docs"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@kitr/core": "0.2.1"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"tsdown": "^0.23.7",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"kitr",
|
|
52
|
+
"kitr-package"
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsdown",
|
|
56
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
57
|
+
}
|
|
6
58
|
}
|