Skip to main content
This release is 2 versions behind 0.8.1 — the latest version of @logtape/logtape. Jump to latest

Built and signed on GitHub Actions

Simple logging library with zero dependencies for Deno/Node.js/Bun/browsers

This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Bun
This package works with Browsers
JSR Score
100%
Published
3 months ago (0.7.1)
Package root>logtape>logger.test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
import { assert } from "jsr:/@std/assert@^0.222.1/assert"; import { assertEquals } from "jsr:/@std/assert@^0.222.1/assert-equals"; import { assertFalse } from "jsr:/@std/assert@^0.222.1/assert-false"; import { assertGreaterOrEqual } from "jsr:/@std/assert@^0.222.1/assert-greater-or-equal"; import { assertLessOrEqual } from "jsr:/@std/assert@^0.222.1/assert-less-or-equal"; import { assertStrictEquals } from "jsr:/@std/assert@^0.222.1/assert-strict-equals"; import { toFilter } from "./filter.ts"; import { debug, error, info, warning } from "./fixtures.ts"; import { getLogger, LoggerCtx, LoggerImpl, parseMessageTemplate, renderMessage, } from "./logger.ts"; import type { LogRecord } from "./record.ts"; import type { Sink } from "./sink.ts"; function templateLiteral(tpl: TemplateStringsArray, ..._: unknown[]) { return tpl; } Deno.test("getLogger()", () => { assertEquals(getLogger().category, []); assertStrictEquals(getLogger(), getLogger()); assertStrictEquals(getLogger([]), getLogger()); assertEquals(getLogger("foo").category, ["foo"]); assertStrictEquals(getLogger("foo"), getLogger("foo")); assertStrictEquals(getLogger("foo"), getLogger(["foo"])); assertStrictEquals(getLogger("foo"), getLogger().getChild("foo")); assertEquals(getLogger(["foo", "bar"]).category, ["foo", "bar"]); assertStrictEquals( getLogger(["foo", "bar"]), getLogger().getChild(["foo", "bar"]), ); assertStrictEquals( getLogger(["foo", "bar"]), getLogger().getChild("foo").getChild("bar"), ); }); Deno.test("Logger.getChild()", () => { const foo = getLogger("foo"); const fooBar = foo.getChild("bar"); assertEquals(fooBar.category, ["foo", "bar"]); assertStrictEquals(fooBar.parent, foo); const fooBarBaz = foo.getChild(["bar", "baz"]); assertEquals(fooBarBaz.category, ["foo", "bar", "baz"]); assertEquals(fooBarBaz.parent, fooBar); const fooCtx = foo.with({ a: 1, b: 2 }); const fooBarCtx = fooCtx.getChild("bar"); assertEquals(fooBarCtx.category, ["foo", "bar"]); // @ts-ignore: internal attribute: assertEquals(fooBarCtx.properties, { a: 1, b: 2 }); }); Deno.test("Logger.with()", () => { const foo = getLogger("foo"); const ctx = foo.with({ a: 1, b: 2 }); assertEquals(ctx.parent, getLogger()); assertEquals(ctx.category, ["foo"]); // @ts-ignore: internal attribute: assertEquals(ctx.properties, { a: 1, b: 2 }); // @ts-ignore: internal attribute: assertEquals(ctx.with({ c: 3 }).properties, { a: 1, b: 2, c: 3 }); }); Deno.test("LoggerImpl.filter()", async (t) => { const root = LoggerImpl.getLogger([]); const foo = LoggerImpl.getLogger("foo"); const fooBar = foo.getChild("bar"); const fooBaz = foo.getChild("baz"); const fooBarQux = fooBar.getChild("qux"); const fooQuux = foo.getChild("quux"); await t.step("test", () => { foo.filters.push((log) => log.level === "info"); fooBar.filters.push((log) => log.message.includes("!")); fooBaz.filters.push((log) => log.message.includes(".")); fooBarQux.filters.push(() => true); assert(root.filter(info)); assert(foo.filter(info)); assert(fooBar.filter(info)); assertFalse(fooBaz.filter(info)); assert(fooBarQux.filter(info)); assert(fooQuux.filter(info)); assert(root.filter(debug)); assertFalse(foo.filter(debug)); assert(fooBar.filter(debug)); assertFalse(fooBaz.filter(debug)); assert(fooBarQux.filter(debug)); assertFalse(fooQuux.filter(debug)); }); await t.step("tear down", () => { root.resetDescendants(); }); }); Deno.test("LoggerImpl.getSinks()", async (t) => { const root = LoggerImpl.getLogger([]); const foo = LoggerImpl.getLogger("foo"); const fooBar = foo.getChild("bar"); const fooBaz = foo.getChild("baz"); const fooBarQux = fooBar.getChild("qux"); await t.step("test", () => { const sinkA: Sink = () => {}; foo.sinks.push(sinkA); const sinkB: Sink = () => {}; fooBar.sinks.push(sinkB); const sinkC: Sink = () => {}; fooBaz.sinks.push(sinkC); const sinkD: Sink = () => {}; fooBarQux.sinks.push(sinkD); assertEquals([...root.getSinks()], []); assertEquals([...foo.getSinks()], [sinkA]); assertEquals([...fooBar.getSinks()], [sinkA, sinkB]); assertEquals([...fooBaz.getSinks()], [sinkA, sinkC]); assertEquals([...fooBarQux.getSinks()], [sinkA, sinkB, sinkD]); fooBarQux.parentSinks = "override"; assertEquals([...fooBarQux.getSinks()], [sinkD]); }); await t.step("tear down", () => { root.resetDescendants(); }); }); Deno.test("LoggerImpl.emit()", async (t) => { const root = LoggerImpl.getLogger([]); const foo = root.getChild("foo"); const fooBar = foo.getChild("bar"); const fooBarBaz = fooBar.getChild("baz"); await t.step("test", () => { const rootRecords: LogRecord[] = []; root.sinks.push(rootRecords.push.bind(rootRecords)); root.filters.push(toFilter("warning")); const fooRecords: LogRecord[] = []; foo.sinks.push(fooRecords.push.bind(fooRecords)); foo.filters.push(toFilter("info")); const fooBarRecords: LogRecord[] = []; fooBar.sinks.push(fooBarRecords.push.bind(fooBarRecords)); fooBar.filters.push(toFilter("error")); root.emit(info); assertEquals(rootRecords, []); assertEquals(fooRecords, []); assertEquals(fooBarRecords, []); root.emit(warning); assertEquals(rootRecords, [warning]); assertEquals(fooRecords, []); assertEquals(fooBarRecords, []); foo.emit(debug); assertEquals(rootRecords, [warning]); assertEquals(fooRecords, []); assertEquals(fooBarRecords, []); foo.emit(info); assertEquals(rootRecords, [warning, info]); assertEquals(fooRecords, [info]); assertEquals(fooBarRecords, []); fooBar.emit(warning); assertEquals(rootRecords, [warning, info]); assertEquals(fooRecords, [info]); assertEquals(fooBarRecords, []); fooBar.emit(error); assertEquals(rootRecords, [warning, info, error]); assertEquals(fooRecords, [info, error]); assertEquals(fooBarRecords, [error]); const errorSink: Sink = () => { throw new Error("This is an error"); }; fooBarBaz.sinks.push(errorSink); fooBarBaz.emit(error); assertEquals(rootRecords.length, 5); assertEquals(rootRecords.slice(0, 4), [warning, info, error, error]); assertEquals(fooRecords, [info, error, error]); assertEquals(fooBarRecords, [error, error]); assertEquals(rootRecords[4].category, ["logtape", "meta"]); assertEquals(rootRecords[4].level, "fatal"); assertEquals(rootRecords[4].message, [ "Failed to emit a log record to sink ", errorSink, ": ", rootRecords[4].properties.error, "", ]); assertEquals(rootRecords[4].properties, { record: error, sink: errorSink, error: rootRecords[4].properties.error, }); root.sinks.push(errorSink); fooBarBaz.emit(error); }); await t.step("tear down", () => { root.resetDescendants(); }); }); Deno.test("LoggerImpl.log()", async (t) => { const logger = LoggerImpl.getLogger("foo"); await t.step("test", () => { const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); const before = Date.now(); logger.log("info", "Hello, {foo}!", { foo: 123 }); const after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: "info", message: ["Hello, ", 123, "!"], rawMessage: "Hello, {foo}!", timestamp: logs[0].timestamp, properties: { foo: 123 }, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); logs.shift(); logger.filters.push(toFilter("error")); let called = 0; logger.log("warning", "Hello, {foo}!", () => { called++; return { foo: 123 }; }); assertEquals(logs, []); assertEquals(called, 0); logger.log("error", "Hello, {foo}!", () => { called++; return { foo: 123 }; }); assertEquals(logs, [ { category: ["foo"], level: "error", message: ["Hello, ", 123, "!"], rawMessage: "Hello, {foo}!", timestamp: logs[0].timestamp, properties: { foo: 123 }, }, ]); assertEquals(called, 1); }); await t.step("tear down", () => { logger.resetDescendants(); }); }); Deno.test("LoggerImpl.logLazily()", async (t) => { const logger = LoggerImpl.getLogger("foo"); await t.step("test", () => { let called = 0; function calc() { called++; return 123; } const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); logger.filters.push(toFilter("error")); logger.logLazily("warning", (l) => l`Hello, ${calc()}!`); assertEquals(logs, []); assertEquals(called, 0); const before = Date.now(); logger.logLazily("error", (l) => l`Hello, ${calc()}!`); const after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: "error", message: ["Hello, ", 123, "!"], rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: {}, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); assertEquals(called, 1); }); await t.step("tear down", () => { logger.resetDescendants(); }); }); Deno.test("LoggerImpl.logTemplate()", async (t) => { const logger = LoggerImpl.getLogger("foo"); await t.step("test", () => { function info(tpl: TemplateStringsArray, ...values: unknown[]) { logger.logTemplate("info", tpl, values); } const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); const before = Date.now(); info`Hello, ${123}!`; const after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: "info", message: ["Hello, ", 123, "!"], rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: {}, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); }); await t.step("tear down", () => { logger.resetDescendants(); }); }); Deno.test("LoggerCtx.log()", async (t) => { const logger = LoggerImpl.getLogger("foo"); const ctx = new LoggerCtx(logger, { a: 1, b: 2 }); await t.step("test", () => { const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); const before = Date.now(); ctx.log("info", "Hello, {a} {b} {c}!", { c: 3 }); const after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: "info", message: ["Hello, ", 1, " ", 2, " ", 3, "!"], rawMessage: "Hello, {a} {b} {c}!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2, c: 3 }, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); logs.shift(); logger.filters.push(toFilter("error")); let called = 0; ctx.log("warning", "Hello, {a} {b} {c}!", () => { called++; return { c: 3 }; }); assertEquals(logs, []); assertEquals(called, 0); ctx.log("error", "Hello, {a} {b} {c}!", () => { called++; return { c: 3 }; }); assertEquals(logs, [ { category: ["foo"], level: "error", message: ["Hello, ", 1, " ", 2, " ", 3, "!"], rawMessage: "Hello, {a} {b} {c}!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2, c: 3 }, }, ]); assertEquals(called, 1); }); await t.step("tear down", () => { logger.resetDescendants(); }); }); Deno.test("LoggerCtx.logLazily()", async (t) => { const logger = LoggerImpl.getLogger("foo"); const ctx = new LoggerCtx(logger, { a: 1, b: 2 }); await t.step("test", () => { let called = 0; function calc() { called++; return 123; } const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); logger.filters.push(toFilter("error")); logger.logLazily("warning", (l) => l`Hello, ${calc()}!`); assertEquals(logs, []); assertEquals(called, 0); const before = Date.now(); ctx.logLazily("error", (l) => l`Hello, ${calc()}!`); const after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: "error", message: ["Hello, ", 123, "!"], rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); assertEquals(called, 1); }); await t.step("tear down", () => { logger.resetDescendants(); }); }); Deno.test("LoggerCtx.logTemplate()", async (t) => { const logger = LoggerImpl.getLogger("foo"); const ctx = new LoggerCtx(logger, { a: 1, b: 2 }); await t.step("test", () => { function info(tpl: TemplateStringsArray, ...values: unknown[]) { ctx.logTemplate("info", tpl, values); } const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); const before = Date.now(); info`Hello, ${123}!`; const after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: "info", message: ["Hello, ", 123, "!"], rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); }); await t.step("tear down", () => { logger.resetDescendants(); }); }); const methods = ["debug", "info", "warn", "error", "fatal"] as const; for (const method of methods) { Deno.test(`Logger.${method}()`, async (t) => { const logger = LoggerImpl.getLogger("foo"); const ctx = new LoggerCtx(logger, { a: 1, b: 2 }); await t.step("template", () => { function tpl(tpl: TemplateStringsArray, ...values: unknown[]) { logger[method](tpl, ...values); } const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); let before = Date.now(); tpl`Hello, ${123}!`; let after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: {}, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); function ctxTpl(tpl: TemplateStringsArray, ...values: unknown[]) { ctx[method](tpl, ...values); } logs.shift(); before = Date.now(); ctxTpl`Hello, ${123}!`; after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); }); await t.step("tear down", () => { logger.resetDescendants(); }); await t.step("lazy template", () => { const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); let before = Date.now(); logger[method]((l) => l`Hello, ${123}!`); let after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: {}, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); logs.shift(); before = Date.now(); ctx[method]((l) => l`Hello, ${123}!`); after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); }); await t.step("tear down", () => { logger.resetDescendants(); }); await t.step("eager", () => { const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); let before = Date.now(); logger[method]("Hello, {foo}!", { foo: 123 }); let after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], rawMessage: "Hello, {foo}!", timestamp: logs[0].timestamp, properties: { foo: 123 }, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); logs.shift(); logger[method]("Hello, world!"); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, world!"], rawMessage: "Hello, world!", timestamp: logs[0].timestamp, properties: {}, }, ]); logs.shift(); before = Date.now(); ctx[method]("Hello, {a} {b} {c}!", { c: 3 }); after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 1, " ", 2, " ", 3, "!"], rawMessage: "Hello, {a} {b} {c}!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2, c: 3 }, }, ]); logs.shift(); ctx[method]("Hello, world!"); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, world!"], rawMessage: "Hello, world!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, ]); }); await t.step("tear down", () => { logger.resetDescendants(); }); await t.step("lazy", () => { const logs: LogRecord[] = []; logger.sinks.push(logs.push.bind(logs)); let before = Date.now(); logger[method]("Hello, {foo}!", () => { return { foo: 123 }; }); let after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], rawMessage: "Hello, {foo}!", timestamp: logs[0].timestamp, properties: { foo: 123 }, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); logs.shift(); before = Date.now(); ctx[method]("Hello, {a} {b} {c}!", () => { return { c: 3 }; }); after = Date.now(); assertEquals(logs, [ { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 1, " ", 2, " ", 3, "!"], rawMessage: "Hello, {a} {b} {c}!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2, c: 3 }, }, ]); assertGreaterOrEqual(logs[0].timestamp, before); assertLessOrEqual(logs[0].timestamp, after); }); await t.step("tear down", () => { logger.resetDescendants(); }); }); } Deno.test("parseMessageTemplate()", () => { assertEquals(parseMessageTemplate("Hello, world!", {}), ["Hello, world!"]); assertEquals( parseMessageTemplate("Hello, world!", { foo: 123 }), ["Hello, world!"], ); assertEquals( parseMessageTemplate("Hello, {{world}}!", { foo: 123 }), ["Hello, {world}!"], ); assertEquals( parseMessageTemplate("Hello, {foo}!", { foo: 123 }), ["Hello, ", 123, "!"], ); assertEquals( parseMessageTemplate("Hello, { foo\t}!", { " foo\t": 123, foo: 456 }), ["Hello, ", 123, "!"], ); assertEquals( parseMessageTemplate("Hello, { foo\t}!", { foo: 456 }), ["Hello, ", 456, "!"], ); assertEquals( parseMessageTemplate("Hello, { foo\t}!", { " foo": 456 }), ["Hello, ", undefined, "!"], ); assertEquals( parseMessageTemplate("Hello, {{foo}}!", { foo: 123 }), ["Hello, {foo}!"], ); assertEquals( parseMessageTemplate("Hello, {bar}!", { foo: 123 }), ["Hello, ", undefined, "!"], ); assertEquals( parseMessageTemplate("Hello, {bar}!", { foo: 123, bar: 456 }), ["Hello, ", 456, "!"], ); assertEquals( parseMessageTemplate("Hello, {foo}, {bar}!", { foo: 123, bar: 456 }), ["Hello, ", 123, ", ", 456, "!"], ); assertEquals( parseMessageTemplate("Hello, {foo}, {bar}", { foo: 123, bar: 456 }), ["Hello, ", 123, ", ", 456, ""], ); assertEquals( parseMessageTemplate("Hello, {{world!", { foo: 123 }), ["Hello, {world!"], ); }); Deno.test("renderMessage()", () => { function rm(tpl: TemplateStringsArray, ...values: unknown[]) { return renderMessage(tpl, values); } assertEquals(rm`Hello, world!`, ["Hello, world!"]); assertEquals(rm`Hello, ${123}!`, ["Hello, ", 123, "!"]); assertEquals(rm`Hello, ${123}, ${456}!`, ["Hello, ", 123, ", ", 456, "!"]); assertEquals(rm`Hello, ${123}, ${456}`, ["Hello, ", 123, ", ", 456, ""]); });