@kitr/logger 0.0.0-stage → 0.3.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/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
- # Temporary Holding Version
1
+ # @kitr/logger
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -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/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/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/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/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.
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.3.0",
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.3.0"
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
  }