@logtape/fastify 1.3.0-dev.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/src/mod.ts ADDED
@@ -0,0 +1,252 @@
1
+ import { getLogger, type Logger } from "@logtape/logtape";
2
+
3
+ /**
4
+ * Pino log levels as strings.
5
+ * @since 1.3.0
6
+ */
7
+ export type PinoLevel =
8
+ | "trace"
9
+ | "debug"
10
+ | "info"
11
+ | "warn"
12
+ | "error"
13
+ | "fatal"
14
+ | "silent";
15
+
16
+ /**
17
+ * Options for configuring the Fastify LogTape logger.
18
+ * @since 1.3.0
19
+ */
20
+ export interface FastifyLogTapeOptions {
21
+ /**
22
+ * The LogTape category to use for logging.
23
+ * @default ["fastify"]
24
+ */
25
+ readonly category?: string | readonly string[];
26
+
27
+ /**
28
+ * The initial log level. This is tracked internally to satisfy
29
+ * Pino's level property requirement.
30
+ * Note: Actual filtering is controlled by LogTape configuration.
31
+ * @default "info"
32
+ */
33
+ readonly level?: PinoLevel;
34
+ }
35
+
36
+ /**
37
+ * Pino-style log method supporting multiple signatures.
38
+ *
39
+ * Supports the following calling conventions:
40
+ * - `logger.info("message")` - Simple message
41
+ * - `logger.info("message %s", arg)` - Printf-style interpolation
42
+ * - `logger.info({ key: "value" }, "message")` - Object with message
43
+ * - `logger.info({ key: "value" })` - Object only
44
+ * - `logger.info({ msg: "message", key: "value" })` - Object with msg property
45
+ *
46
+ * @since 1.3.0
47
+ */
48
+ export interface PinoLogMethod {
49
+ /** Log with object and optional message */
50
+ (obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
51
+ /** Log with just a message and optional interpolation args */
52
+ (msg: string, ...args: unknown[]): void;
53
+ }
54
+
55
+ /**
56
+ * A Pino-compatible logger interface that wraps LogTape.
57
+ * This interface satisfies Fastify's `loggerInstance` requirements.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * import Fastify from "fastify";
62
+ * import { getLogTapeFastifyLogger } from "@logtape/fastify";
63
+ *
64
+ * const fastify = Fastify({
65
+ * loggerInstance: getLogTapeFastifyLogger(),
66
+ * });
67
+ * ```
68
+ *
69
+ * @since 1.3.0
70
+ */
71
+ export interface PinoLikeLogger {
72
+ /** Log at trace level */
73
+ trace: PinoLogMethod;
74
+ /** Log at debug level */
75
+ debug: PinoLogMethod;
76
+ /** Log at info level */
77
+ info: PinoLogMethod;
78
+ /** Log at warn level */
79
+ warn: PinoLogMethod;
80
+ /** Log at error level */
81
+ error: PinoLogMethod;
82
+ /** Log at fatal level */
83
+ fatal: PinoLogMethod;
84
+ /** No-op silent method for Pino compatibility */
85
+ silent: () => void;
86
+ /** Create a child logger with additional bindings */
87
+ child: (bindings: Record<string, unknown>) => PinoLikeLogger;
88
+ /** Current log level (readable/writable for Pino compatibility) */
89
+ level: string;
90
+ }
91
+
92
+ /**
93
+ * Creates a Pino-compatible logger that wraps LogTape.
94
+ * This logger can be used as Fastify's `loggerInstance`.
95
+ *
96
+ * @example Basic usage
97
+ * ```typescript
98
+ * import Fastify from "fastify";
99
+ * import { configure } from "@logtape/logtape";
100
+ * import { getLogTapeFastifyLogger } from "@logtape/fastify";
101
+ *
102
+ * await configure({
103
+ * // ... LogTape configuration
104
+ * });
105
+ *
106
+ * const fastify = Fastify({
107
+ * loggerInstance: getLogTapeFastifyLogger(),
108
+ * });
109
+ * ```
110
+ *
111
+ * @example With custom category
112
+ * ```typescript
113
+ * const fastify = Fastify({
114
+ * loggerInstance: getLogTapeFastifyLogger({
115
+ * category: ["myapp", "http"],
116
+ * }),
117
+ * });
118
+ * ```
119
+ *
120
+ * @param options Configuration options for the logger.
121
+ * @returns A Pino-compatible logger wrapping LogTape.
122
+ * @since 1.3.0
123
+ */
124
+ export function getLogTapeFastifyLogger(
125
+ options: FastifyLogTapeOptions = {},
126
+ ): PinoLikeLogger {
127
+ const category = normalizeCategory(options.category ?? ["fastify"]);
128
+ const logger = getLogger(category);
129
+ const initialLevel = options.level ?? "info";
130
+
131
+ return createPinoLikeLogger(logger, initialLevel);
132
+ }
133
+
134
+ /**
135
+ * Normalize category to array format.
136
+ */
137
+ function normalizeCategory(
138
+ category: string | readonly string[],
139
+ ): readonly string[] {
140
+ return typeof category === "string" ? [category] : category;
141
+ }
142
+
143
+ /**
144
+ * Format message with printf-style interpolation.
145
+ * Supports: %s (string), %d (number), %j (JSON), %o/%O (object), %% (escaped %)
146
+ */
147
+ function formatMessage(template: string, ...args: unknown[]): string {
148
+ let argIndex = 0;
149
+ return template.replace(/%[sdjoO%]/g, (match) => {
150
+ if (match === "%%") return "%";
151
+ if (argIndex >= args.length) return match;
152
+
153
+ const arg = args[argIndex++];
154
+ switch (match) {
155
+ case "%s":
156
+ return String(arg);
157
+ case "%d":
158
+ return Number(arg).toString();
159
+ case "%j":
160
+ case "%o":
161
+ case "%O":
162
+ return JSON.stringify(arg);
163
+ default:
164
+ return match;
165
+ }
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Creates a Pino-like logger wrapper around a LogTape logger.
171
+ */
172
+ function createPinoLikeLogger(
173
+ logger: Logger,
174
+ initialLevel: PinoLevel,
175
+ bindings: Record<string, unknown> = {},
176
+ ): PinoLikeLogger {
177
+ // Track level internally for Pino compatibility
178
+ let _level: string = initialLevel;
179
+
180
+ // If there are bindings, create a contextual logger
181
+ const contextLogger = Object.keys(bindings).length > 0
182
+ ? logger.with(bindings)
183
+ : logger;
184
+
185
+ /**
186
+ * Create a log method for a specific level.
187
+ */
188
+ function createLogMethod(
189
+ logFn: (msg: string, props?: Record<string, unknown>) => void,
190
+ ): PinoLogMethod {
191
+ return function pinoLogMethod(
192
+ objOrMsg: Record<string, unknown> | string,
193
+ ...restArgs: unknown[]
194
+ ): void {
195
+ // Detect calling convention
196
+ if (typeof objOrMsg === "string") {
197
+ // Called as: logger.info("message") or logger.info("message %s", arg)
198
+ const message = formatMessage(objOrMsg, ...restArgs);
199
+ logFn(message);
200
+ } else if (typeof objOrMsg === "object" && objOrMsg !== null) {
201
+ // Called as: logger.info({ key: value }, "message") or logger.info({ key: value })
202
+ const properties = { ...objOrMsg };
203
+ const [msgOrArg, ...args] = restArgs;
204
+
205
+ if (typeof msgOrArg === "string") {
206
+ // Has message string: logger.info({ foo: 1 }, "message %s", arg)
207
+ const message = formatMessage(msgOrArg, ...args);
208
+ logFn(message, properties);
209
+ } else if ("msg" in properties && typeof properties.msg === "string") {
210
+ // Extract message from object: logger.info({ msg: "hello", foo: 1 })
211
+ const message = properties.msg as string;
212
+ delete properties.msg;
213
+ logFn(message, properties);
214
+ } else {
215
+ // Object-only logging: logger.info({ foo: 1 })
216
+ logFn("{*}", properties);
217
+ }
218
+ }
219
+ } as PinoLogMethod;
220
+ }
221
+
222
+ const pinoLogger: PinoLikeLogger = {
223
+ trace: createLogMethod((msg, props) => contextLogger.trace(msg, props)),
224
+ debug: createLogMethod((msg, props) => contextLogger.debug(msg, props)),
225
+ info: createLogMethod((msg, props) => contextLogger.info(msg, props)),
226
+ warn: createLogMethod((msg, props) => contextLogger.warn(msg, props)),
227
+ error: createLogMethod((msg, props) => contextLogger.error(msg, props)),
228
+ fatal: createLogMethod((msg, props) => contextLogger.fatal(msg, props)),
229
+
230
+ silent: () => {
231
+ // No-op for silent level
232
+ },
233
+
234
+ child: (childBindings: Record<string, unknown>) => {
235
+ // Merge parent bindings with child bindings
236
+ const mergedBindings = { ...bindings, ...childBindings };
237
+ return createPinoLikeLogger(logger, _level as PinoLevel, mergedBindings);
238
+ },
239
+
240
+ get level() {
241
+ return _level;
242
+ },
243
+
244
+ set level(newLevel: string) {
245
+ // Store level for Pino compatibility
246
+ // Note: Actual filtering is handled by LogTape configuration
247
+ _level = newLevel;
248
+ },
249
+ };
250
+
251
+ return pinoLogger;
252
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: "src/mod.ts",
5
+ dts: {
6
+ sourcemap: true,
7
+ },
8
+ format: ["esm", "cjs"],
9
+ platform: "node",
10
+ unbundle: true,
11
+ });