@logtape/logtape 1.4.0-dev.409 → 1.4.0-dev.413

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.
@@ -1,341 +0,0 @@
1
- import { suite } from "@alinea/suite";
2
- import { assertEquals } from "@std/assert/equals";
3
- import { delay } from "@std/async/delay";
4
- import { AsyncLocalStorage } from "node:async_hooks";
5
- import { configure, reset } from "./config.ts";
6
- import { withCategoryPrefix, withContext } from "./context.ts";
7
- import { getLogger } from "./logger.ts";
8
- import type { LogRecord } from "./record.ts";
9
-
10
- const test = suite(import.meta);
11
-
12
- test("withContext()", async () => {
13
- const buffer: LogRecord[] = [];
14
-
15
- { // set up
16
- await configure({
17
- sinks: {
18
- buffer: buffer.push.bind(buffer),
19
- },
20
- loggers: [
21
- { category: "my-app", sinks: ["buffer"], lowestLevel: "debug" },
22
- { category: ["logtape", "meta"], sinks: [], lowestLevel: "warning" },
23
- ],
24
- contextLocalStorage: new AsyncLocalStorage(),
25
- reset: true,
26
- });
27
- }
28
-
29
- try {
30
- // test
31
- getLogger("my-app").debug("hello", { foo: 1, bar: 2 });
32
- assertEquals(buffer, [
33
- {
34
- category: ["my-app"],
35
- level: "debug",
36
- message: ["hello"],
37
- rawMessage: "hello",
38
- properties: { foo: 1, bar: 2 },
39
- timestamp: buffer[0].timestamp,
40
- },
41
- ]);
42
- buffer.pop();
43
- const rv = withContext({ foo: 3, baz: 4 }, () => {
44
- getLogger("my-app").debug("world", { foo: 1, bar: 2 });
45
- return 123;
46
- });
47
- assertEquals(rv, 123);
48
- assertEquals(buffer, [
49
- {
50
- category: ["my-app"],
51
- level: "debug",
52
- message: ["world"],
53
- rawMessage: "world",
54
- properties: { foo: 1, bar: 2, baz: 4 },
55
- timestamp: buffer[0].timestamp,
56
- },
57
- ]);
58
- buffer.pop();
59
- getLogger("my-app").debug("hello", { foo: 1, bar: 2 });
60
- assertEquals(buffer, [
61
- {
62
- category: ["my-app"],
63
- level: "debug",
64
- message: ["hello"],
65
- rawMessage: "hello",
66
- properties: { foo: 1, bar: 2 },
67
- timestamp: buffer[0].timestamp,
68
- },
69
- ]);
70
-
71
- // nesting
72
- while (buffer.length > 0) buffer.pop();
73
- withContext({ foo: 1, bar: 2 }, () => {
74
- withContext({ foo: 3, baz: 4 }, () => {
75
- getLogger("my-app").debug("hello");
76
- });
77
- });
78
- assertEquals(buffer, [
79
- {
80
- category: ["my-app"],
81
- level: "debug",
82
- message: ["hello"],
83
- rawMessage: "hello",
84
- properties: { foo: 3, bar: 2, baz: 4 },
85
- timestamp: buffer[0].timestamp,
86
- },
87
- ]);
88
-
89
- // concurrent runs
90
- while (buffer.length > 0) buffer.pop();
91
- await Promise.all([
92
- (async () => {
93
- await delay(Math.random() * 100);
94
- withContext({ foo: 1 }, () => {
95
- getLogger("my-app").debug("foo");
96
- });
97
- })(),
98
- (async () => {
99
- await delay(Math.random() * 100);
100
- withContext({ bar: 2 }, () => {
101
- getLogger("my-app").debug("bar");
102
- });
103
- })(),
104
- (async () => {
105
- await delay(Math.random() * 100);
106
- withContext({ baz: 3 }, () => {
107
- getLogger("my-app").debug("baz");
108
- });
109
- })(),
110
- (async () => {
111
- await delay(Math.random() * 100);
112
- withContext({ qux: 4 }, () => {
113
- getLogger("my-app").debug("qux");
114
- });
115
- })(),
116
- ]);
117
- assertEquals(buffer.length, 4);
118
- for (const log of buffer) {
119
- if (log.message[0] === "foo") {
120
- assertEquals(log.properties, { foo: 1 });
121
- } else if (log.message[0] === "bar") {
122
- assertEquals(log.properties, { bar: 2 });
123
- } else if (log.message[0] === "baz") {
124
- assertEquals(log.properties, { baz: 3 });
125
- } else {
126
- assertEquals(log.properties, { qux: 4 });
127
- }
128
- }
129
- } finally {
130
- await reset();
131
- }
132
-
133
- const metaBuffer: LogRecord[] = [];
134
-
135
- { // set up
136
- await configure({
137
- sinks: {
138
- buffer: buffer.push.bind(buffer),
139
- metaBuffer: metaBuffer.push.bind(metaBuffer),
140
- },
141
- loggers: [
142
- { category: "my-app", sinks: ["buffer"], lowestLevel: "debug" },
143
- {
144
- category: ["logtape", "meta"],
145
- sinks: ["metaBuffer"],
146
- lowestLevel: "warning",
147
- },
148
- ],
149
- reset: true,
150
- });
151
- }
152
-
153
- try { // without settings
154
- while (buffer.length > 0) buffer.pop();
155
- const rv = withContext({ foo: 1 }, () => {
156
- getLogger("my-app").debug("hello", { bar: 2 });
157
- return 123;
158
- });
159
- assertEquals(rv, 123);
160
- assertEquals(buffer, [
161
- {
162
- category: ["my-app"],
163
- level: "debug",
164
- message: ["hello"],
165
- rawMessage: "hello",
166
- properties: { bar: 2 },
167
- timestamp: buffer[0].timestamp,
168
- },
169
- ]);
170
- assertEquals(metaBuffer, [
171
- {
172
- category: ["logtape", "meta"],
173
- level: "warning",
174
- message: [
175
- "Context-local storage is not configured. " +
176
- "Specify contextLocalStorage option in the configure() function.",
177
- ],
178
- properties: {},
179
- rawMessage: "Context-local storage is not configured. " +
180
- "Specify contextLocalStorage option in the configure() function.",
181
- timestamp: metaBuffer[0].timestamp,
182
- },
183
- ]);
184
- } finally {
185
- await reset();
186
- }
187
- });
188
-
189
- test("withCategoryPrefix()", async () => {
190
- const buffer: LogRecord[] = [];
191
-
192
- { // set up
193
- await configure({
194
- sinks: {
195
- buffer: buffer.push.bind(buffer),
196
- },
197
- loggers: [
198
- { category: [], sinks: ["buffer"], lowestLevel: "debug" },
199
- { category: ["logtape", "meta"], sinks: [], lowestLevel: "warning" },
200
- ],
201
- contextLocalStorage: new AsyncLocalStorage(),
202
- reset: true,
203
- });
204
- }
205
-
206
- try {
207
- // basic prefix with array
208
- getLogger(["core-lib"]).debug("without prefix");
209
- assertEquals(buffer[0].category, ["core-lib"]);
210
- buffer.pop();
211
-
212
- const rv = withCategoryPrefix(["sdk-1"], () => {
213
- getLogger(["core-lib"]).debug("with prefix");
214
- return 123;
215
- });
216
- assertEquals(rv, 123);
217
- assertEquals(buffer[0].category, ["sdk-1", "core-lib"]);
218
- buffer.pop();
219
-
220
- // prefix should not persist after callback
221
- getLogger(["core-lib"]).debug("after prefix");
222
- assertEquals(buffer[0].category, ["core-lib"]);
223
- buffer.pop();
224
-
225
- // basic prefix with string
226
- withCategoryPrefix("sdk-2", () => {
227
- getLogger(["core-lib"]).debug("string prefix");
228
- });
229
- assertEquals(buffer[0].category, ["sdk-2", "core-lib"]);
230
- buffer.pop();
231
-
232
- // nesting prefixes
233
- withCategoryPrefix(["app"], () => {
234
- withCategoryPrefix(["sdk-1"], () => {
235
- getLogger(["core-lib"]).debug("nested");
236
- });
237
- });
238
- assertEquals(buffer[0].category, ["app", "sdk-1", "core-lib"]);
239
- buffer.pop();
240
-
241
- // combining with withContext
242
- withCategoryPrefix(["my-sdk"], () => {
243
- withContext({ requestId: "abc-123" }, () => {
244
- getLogger(["internal"]).debug("combined");
245
- });
246
- });
247
- assertEquals(buffer[0].category, ["my-sdk", "internal"]);
248
- assertEquals(buffer[0].properties, { requestId: "abc-123" });
249
- buffer.pop();
250
-
251
- // withContext inside withCategoryPrefix
252
- withContext({ userId: 42 }, () => {
253
- withCategoryPrefix(["sdk"], () => {
254
- getLogger(["lib"]).debug("context then prefix");
255
- });
256
- });
257
- assertEquals(buffer[0].category, ["sdk", "lib"]);
258
- assertEquals(buffer[0].properties, { userId: 42 });
259
- buffer.pop();
260
-
261
- // concurrent runs - each should have isolated prefix
262
- await Promise.all([
263
- (async () => {
264
- await delay(Math.random() * 100);
265
- withCategoryPrefix(["sdk-a"], () => {
266
- getLogger(["lib"]).debug("a");
267
- });
268
- })(),
269
- (async () => {
270
- await delay(Math.random() * 100);
271
- withCategoryPrefix(["sdk-b"], () => {
272
- getLogger(["lib"]).debug("b");
273
- });
274
- })(),
275
- (async () => {
276
- await delay(Math.random() * 100);
277
- withCategoryPrefix(["sdk-c"], () => {
278
- getLogger(["lib"]).debug("c");
279
- });
280
- })(),
281
- (async () => {
282
- await delay(Math.random() * 100);
283
- withCategoryPrefix(["sdk-d"], () => {
284
- getLogger(["lib"]).debug("d");
285
- });
286
- })(),
287
- ]);
288
- assertEquals(buffer.length, 4);
289
- for (const log of buffer) {
290
- if (log.message[0] === "a") {
291
- assertEquals(log.category, ["sdk-a", "lib"]);
292
- } else if (log.message[0] === "b") {
293
- assertEquals(log.category, ["sdk-b", "lib"]);
294
- } else if (log.message[0] === "c") {
295
- assertEquals(log.category, ["sdk-c", "lib"]);
296
- } else {
297
- assertEquals(log.category, ["sdk-d", "lib"]);
298
- }
299
- }
300
- } finally {
301
- await reset();
302
- }
303
-
304
- // without contextLocalStorage configured
305
- const metaBuffer: LogRecord[] = [];
306
-
307
- { // set up
308
- await configure({
309
- sinks: {
310
- buffer: buffer.push.bind(buffer),
311
- metaBuffer: metaBuffer.push.bind(metaBuffer),
312
- },
313
- loggers: [
314
- { category: "my-app", sinks: ["buffer"], lowestLevel: "debug" },
315
- {
316
- category: ["logtape", "meta"],
317
- sinks: ["metaBuffer"],
318
- lowestLevel: "warning",
319
- },
320
- ],
321
- reset: true,
322
- });
323
- }
324
-
325
- try {
326
- while (buffer.length > 0) buffer.pop();
327
- const rv = withCategoryPrefix(["sdk-1"], () => {
328
- getLogger(["my-app"]).debug("no storage");
329
- return 456;
330
- });
331
- assertEquals(rv, 456);
332
- // Without storage, category should not have prefix
333
- assertEquals(buffer[0].category, ["my-app"]);
334
- // Warning should be logged to meta logger
335
- assertEquals(metaBuffer.length, 1);
336
- assertEquals(metaBuffer[0].category, ["logtape", "meta"]);
337
- assertEquals(metaBuffer[0].level, "warning");
338
- } finally {
339
- await reset();
340
- }
341
- });
package/src/context.ts DELETED
@@ -1,150 +0,0 @@
1
- import { LoggerImpl } from "./logger.ts";
2
-
3
- /**
4
- * Internal symbol for storing category prefix in context.
5
- */
6
- const categoryPrefixSymbol: unique symbol = Symbol.for(
7
- "logtape.categoryPrefix",
8
- ) as typeof categoryPrefixSymbol;
9
-
10
- /**
11
- * A generic interface for a context-local storage. It resembles
12
- * the {@link AsyncLocalStorage} API from Node.js.
13
- * @template T The type of the context-local store.
14
- * @since 0.7.0
15
- */
16
- export interface ContextLocalStorage<T> {
17
- /**
18
- * Runs a callback with the given store as the context-local store.
19
- * @param store The store to use as the context-local store.
20
- * @param callback The callback to run.
21
- * @returns The return value of the callback.
22
- */
23
- run<R>(store: T, callback: () => R): R;
24
-
25
- /**
26
- * Returns the current context-local store.
27
- * @returns The current context-local store, or `undefined` if there is no
28
- * store.
29
- */
30
- getStore(): T | undefined;
31
- }
32
-
33
- /**
34
- * Runs a callback with the given implicit context. Every single log record
35
- * in the callback will have the given context.
36
- *
37
- * If no `contextLocalStorage` is configured, this function does nothing and
38
- * just returns the return value of the callback. It also logs a warning to
39
- * the `["logtape", "meta"]` logger in this case.
40
- * @param context The context to inject.
41
- * @param callback The callback to run.
42
- * @returns The return value of the callback.
43
- * @since 0.7.0
44
- */
45
- export function withContext<T>(
46
- context: Record<string, unknown>,
47
- callback: () => T,
48
- ): T {
49
- const rootLogger = LoggerImpl.getLogger();
50
- if (rootLogger.contextLocalStorage == null) {
51
- LoggerImpl.getLogger(["logtape", "meta"]).warn(
52
- "Context-local storage is not configured. " +
53
- "Specify contextLocalStorage option in the configure() function.",
54
- );
55
- return callback();
56
- }
57
- const parentContext = rootLogger.contextLocalStorage.getStore() ?? {};
58
- return rootLogger.contextLocalStorage.run(
59
- { ...parentContext, ...context },
60
- callback,
61
- );
62
- }
63
-
64
- /**
65
- * Gets the current category prefix from context local storage.
66
- * @returns The current category prefix, or an empty array if not set.
67
- * @since 1.3.0
68
- */
69
- export function getCategoryPrefix(): readonly string[] {
70
- const rootLogger = LoggerImpl.getLogger();
71
- const store = rootLogger.contextLocalStorage?.getStore();
72
- if (store == null) return [];
73
- const prefix = store[categoryPrefixSymbol as unknown as string];
74
- return Array.isArray(prefix) ? prefix : [];
75
- }
76
-
77
- /**
78
- * Gets the current implicit context from context local storage, excluding
79
- * internal symbol keys (like category prefix).
80
- * @returns The current implicit context without internal symbol keys.
81
- * @since 1.3.0
82
- */
83
- export function getImplicitContext(): Record<string, unknown> {
84
- const rootLogger = LoggerImpl.getLogger();
85
- const store = rootLogger.contextLocalStorage?.getStore();
86
- if (store == null) return {};
87
- // Filter out symbol keys (like categoryPrefixSymbol)
88
- const result: Record<string, unknown> = {};
89
- for (const key of Object.keys(store)) {
90
- result[key] = store[key];
91
- }
92
- return result;
93
- }
94
-
95
- /**
96
- * Runs a callback with the given category prefix prepended to all log
97
- * categories within the callback context.
98
- *
99
- * This is useful for SDKs or libraries that want to add their own category
100
- * as a prefix to logs from their internal dependencies.
101
- *
102
- * If no `contextLocalStorage` is configured, this function does nothing and
103
- * just returns the return value of the callback. It also logs a warning to
104
- * the `["logtape", "meta"]` logger in this case.
105
- *
106
- * @example Basic usage
107
- * ```typescript
108
- * import { getLogger, withCategoryPrefix } from "@logtape/logtape";
109
- *
110
- * export function sdkFunction() {
111
- * return withCategoryPrefix(["my-sdk"], () => {
112
- * // Any logs from internal libraries within this context
113
- * // will have ["my-sdk"] prepended to their category
114
- * return internalLibraryFunction();
115
- * });
116
- * }
117
- * ```
118
- *
119
- * @param prefix The category prefix to prepend. Can be a string or an array
120
- * of strings.
121
- * @param callback The callback to run.
122
- * @returns The return value of the callback.
123
- * @since 1.3.0
124
- */
125
- export function withCategoryPrefix<T>(
126
- prefix: string | readonly string[],
127
- callback: () => T,
128
- ): T {
129
- const rootLogger = LoggerImpl.getLogger();
130
- if (rootLogger.contextLocalStorage == null) {
131
- LoggerImpl.getLogger(["logtape", "meta"]).warn(
132
- "Context-local storage is not configured. " +
133
- "Specify contextLocalStorage option in the configure() function.",
134
- );
135
- return callback();
136
- }
137
- const parentContext = rootLogger.contextLocalStorage.getStore() ?? {};
138
- const parentPrefix = getCategoryPrefix();
139
- const newPrefix = typeof prefix === "string" ? [prefix] : [...prefix];
140
- return rootLogger.contextLocalStorage.run(
141
- {
142
- ...parentContext,
143
- [categoryPrefixSymbol as unknown as string]: [
144
- ...parentPrefix,
145
- ...newPrefix,
146
- ],
147
- },
148
- callback,
149
- );
150
- }
@@ -1,84 +0,0 @@
1
- import { suite } from "@alinea/suite";
2
- import { assert } from "@std/assert/assert";
3
- import { assertFalse } from "@std/assert/false";
4
- import { assertStrictEquals } from "@std/assert/strict-equals";
5
- import { assertThrows } from "@std/assert/throws";
6
- import { type Filter, getLevelFilter, toFilter } from "./filter.ts";
7
- import { debug, error, fatal, info, trace, warning } from "./fixtures.ts";
8
- import type { LogLevel } from "./level.ts";
9
-
10
- const test = suite(import.meta);
11
-
12
- test("getLevelFilter()", () => {
13
- const noneFilter = getLevelFilter(null);
14
- assertFalse(noneFilter(fatal));
15
- assertFalse(noneFilter(error));
16
- assertFalse(noneFilter(warning));
17
- assertFalse(noneFilter(info));
18
- assertFalse(noneFilter(debug));
19
- assertFalse(noneFilter(trace));
20
-
21
- const fatalFilter = getLevelFilter("fatal");
22
- assert(fatalFilter(fatal));
23
- assertFalse(fatalFilter(error));
24
- assertFalse(fatalFilter(warning));
25
- assertFalse(fatalFilter(info));
26
- assertFalse(fatalFilter(debug));
27
- assertFalse(fatalFilter(trace));
28
-
29
- const errorFilter = getLevelFilter("error");
30
- assert(errorFilter(fatal));
31
- assert(errorFilter(error));
32
- assertFalse(errorFilter(warning));
33
- assertFalse(errorFilter(info));
34
- assertFalse(errorFilter(debug));
35
- assertFalse(errorFilter(trace));
36
-
37
- const warningFilter = getLevelFilter("warning");
38
- assert(warningFilter(fatal));
39
- assert(warningFilter(error));
40
- assert(warningFilter(warning));
41
- assertFalse(warningFilter(info));
42
- assertFalse(warningFilter(debug));
43
- assertFalse(warningFilter(trace));
44
-
45
- const infoFilter = getLevelFilter("info");
46
- assert(infoFilter(fatal));
47
- assert(infoFilter(error));
48
- assert(infoFilter(warning));
49
- assert(infoFilter(info));
50
- assertFalse(infoFilter(debug));
51
- assertFalse(infoFilter(trace));
52
-
53
- const debugFilter = getLevelFilter("debug");
54
- assert(debugFilter(fatal));
55
- assert(debugFilter(error));
56
- assert(debugFilter(warning));
57
- assert(debugFilter(info));
58
- assert(debugFilter(debug));
59
- assertFalse(debugFilter(trace));
60
-
61
- const traceFilter = getLevelFilter("trace");
62
- assert(traceFilter(fatal));
63
- assert(traceFilter(error));
64
- assert(traceFilter(warning));
65
- assert(traceFilter(info));
66
- assert(traceFilter(debug));
67
- assert(traceFilter(trace));
68
-
69
- assertThrows(
70
- () => getLevelFilter("invalid" as LogLevel),
71
- TypeError,
72
- "Invalid log level: invalid.",
73
- );
74
- });
75
-
76
- test("toFilter()", () => {
77
- const hasJunk: Filter = (record) => record.category.includes("junk");
78
- assertStrictEquals(toFilter(hasJunk), hasJunk);
79
-
80
- const infoFilter = toFilter("info");
81
- assertFalse(infoFilter(debug));
82
- assert(infoFilter(info));
83
- assert(infoFilter(warning));
84
- });
package/src/filter.ts DELETED
@@ -1,64 +0,0 @@
1
- import type { LogLevel } from "./level.ts";
2
- import type { LogRecord } from "./record.ts";
3
-
4
- /**
5
- * A filter is a function that accepts a log record and returns `true` if the
6
- * record should be passed to the sink.
7
- *
8
- * @param record The log record to filter.
9
- * @returns `true` if the record should be passed to the sink.
10
- */
11
- export type Filter = (record: LogRecord) => boolean;
12
-
13
- /**
14
- * A filter-like value is either a {@link Filter} or a {@link LogLevel}.
15
- * `null` is also allowed to represent a filter that rejects all records.
16
- */
17
- export type FilterLike = Filter | LogLevel | null;
18
-
19
- /**
20
- * Converts a {@link FilterLike} value to an actual {@link Filter}.
21
- *
22
- * @param filter The filter-like value to convert.
23
- * @returns The actual filter.
24
- */
25
- export function toFilter(filter: FilterLike): Filter {
26
- if (typeof filter === "function") return filter;
27
- return getLevelFilter(filter);
28
- }
29
-
30
- /**
31
- * Returns a filter that accepts log records with the specified level.
32
- *
33
- * @param level The level to filter by. If `null`, the filter will reject all
34
- * records.
35
- * @returns The filter.
36
- */
37
- export function getLevelFilter(level: LogLevel | null): Filter {
38
- if (level == null) return () => false;
39
- if (level === "fatal") {
40
- return (record: LogRecord) => record.level === "fatal";
41
- } else if (level === "error") {
42
- return (record: LogRecord) =>
43
- record.level === "fatal" || record.level === "error";
44
- } else if (level === "warning") {
45
- return (record: LogRecord) =>
46
- record.level === "fatal" ||
47
- record.level === "error" ||
48
- record.level === "warning";
49
- } else if (level === "info") {
50
- return (record: LogRecord) =>
51
- record.level === "fatal" ||
52
- record.level === "error" ||
53
- record.level === "warning" ||
54
- record.level === "info";
55
- } else if (level === "debug") {
56
- return (record: LogRecord) =>
57
- record.level === "fatal" ||
58
- record.level === "error" ||
59
- record.level === "warning" ||
60
- record.level === "info" ||
61
- record.level === "debug";
62
- } else if (level === "trace") return () => true;
63
- throw new TypeError(`Invalid log level: ${level}.`);
64
- }
package/src/fixtures.ts DELETED
@@ -1,35 +0,0 @@
1
- import type { LogRecord } from "./record.ts";
2
-
3
- export const info: LogRecord = {
4
- level: "info",
5
- category: ["my-app", "junk"],
6
- message: ["Hello, ", 123, " & ", 456, "!"],
7
- rawMessage: "Hello, {a} & {b}!",
8
- timestamp: 1700000000000,
9
- properties: {},
10
- };
11
-
12
- export const trace: LogRecord = {
13
- ...info,
14
- level: "trace",
15
- };
16
-
17
- export const debug: LogRecord = {
18
- ...info,
19
- level: "debug",
20
- };
21
-
22
- export const warning: LogRecord = {
23
- ...info,
24
- level: "warning",
25
- };
26
-
27
- export const error: LogRecord = {
28
- ...info,
29
- level: "error",
30
- };
31
-
32
- export const fatal: LogRecord = {
33
- ...info,
34
- level: "fatal",
35
- };