| author | |
| committer | |
| log | 72e9a94ec20202598734c0edc99e6f5cba3fd296 |
| tree | fffad0117dc21aa0d8089a8e74bfcdd0926cae36 |
| parent | bd99670fb91ecff5cbdb1a33ecbf927cdb3e6556 |
| signature | Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ |
closes #45. Message.custom was already serialized by tee consumers and
the progress streams, but nothing could set it. log.scoped takes an
optional custom object attached to every message the scope emits;
nested scopes merge their fields over inherited ones.3 files changed, 45 insertions(+), 5 deletions(-)
lib/log.test.ts+29| ... | ... | @@ -1,6 +1,35 @@ |
| 1 | 1 | // the namespace import is a valid Scope |
| 2 | 2 | log satisfies log.Scope; |
| 3 | 3 | |
| 4 | describe("log scopes", () => { | |
| 5 | test("scope metadata attaches to messages", () => { | |
| 6 | const messages: log.Message[] = []; | |
| 7 | const root = log.headlessScope((m) => messages.push(m)); | |
| 8 | ||
| 9 | const req = root.scoped("http", { method: "GET", path: "/" }); | |
| 10 | req.info("incoming"); | |
| 11 | req.write("chunk"); | |
| 12 | ASSERT(messages[0]?.custom?.method === "GET"); | |
| 13 | ASSERT(messages[1]?.custom?.path === "/"); | |
| 14 | ||
| 15 | // nested scopes merge their fields over inherited ones | |
| 16 | const child = req.scoped("auth", { method: "POST", user: "clo" }); | |
| 17 | child.warn("denied"); | |
| 18 | ASSERT(messages[2]?.custom?.method === "POST"); | |
| 19 | ASSERT(messages[2]?.custom?.path === "/"); | |
| 20 | ASSERT(messages[2]?.custom?.user === "clo"); | |
| 21 | ||
| 22 | // sibling scopes are unaffected by the child's overrides | |
| 23 | req.scoped("other").info("hi"); | |
| 24 | ASSERT(messages[3]?.custom?.method === "GET"); | |
| 25 | ASSERT(messages[3]?.custom?.user === undefined); | |
| 26 | ||
| 27 | // scopes without metadata emit none | |
| 28 | root.scoped("plain").info("hello"); | |
| 29 | ASSERT(messages[4]?.custom === undefined); | |
| 30 | }); | |
| 31 | }); | |
| 32 | ||
| 4 | 33 | // these tests for `startWidget` are built on a custom widget host |
| 5 | 34 | // implementation that buffers all bytes in memory. it is a great |
| 6 | 35 | // example of how modular the entire system is. |
lib/log.ts+12-5| ... | ... | @@ -80,7 +80,7 @@ export interface Scope { |
| 80 | 80 | writeMessage(message: Message): void; |
| 81 | 81 | |
| 82 | 82 | /** create a nested sub-scope */ |
| 83 | scoped(name: string): Scope; | |
| 83 | scoped(name: string, custom?: Message["custom"]): Scope; | |
| 84 | 84 | /** redirect the logging output of this scope somewhere else */ |
| 85 | 85 | tee(writer: (message: Message) => void): ts.Dispose; |
| 86 | 86 | } |
| ... | ... | @@ -154,9 +154,9 @@ export function log(...args: unknown[]) { |
| 154 | 154 | export function debug(...args: unknown[]) { |
| 155 | 155 | globalLog.debug(...args); |
| 156 | 156 | } |
| 157 | /** create a named logging scope */ | |
| 158 | export function scoped(name: string): Scope { | |
| 159 | return globalLog.scoped(name); | |
| 157 | /** create a named logging scope, optionally carrying custom message fields */ | |
| 158 | export function scoped(name: string, custom?: Message["custom"]): Scope { | |
| 159 | return globalLog.scoped(name, custom); | |
| 160 | 160 | } |
| 161 | 161 | /** redirect all log messages to another writer */ |
| 162 | 162 | export function tee(destination: DispatchFunction): ts.Dispose { |
| ... | ... | @@ -1058,6 +1058,8 @@ let withinStackCapture = false; |
| 1058 | 1058 | /** this class is an implementation detail */ |
| 1059 | 1059 | const ScopeImpl = class Scope implements RootScope { |
| 1060 | 1060 | name: string | undefined; |
| 1061 | /** attached to every emitted message; see `Scope.scoped` */ | |
| 1062 | #custom: Message["custom"]; | |
| 1061 | 1063 | // TODO: this abstraction implementation has low performance. making every |
| 1062 | 1064 | // scope define it's own dispatch is needed to correctly implement `tee`. |
| 1063 | 1065 | // since it is possible to implement this in simple and non-recursive way, i |
| ... | ... | @@ -1068,8 +1070,10 @@ const ScopeImpl = class Scope implements RootScope { |
| 1068 | 1070 | constructor( |
| 1069 | 1071 | dispatch: (m: Message) => void, |
| 1070 | 1072 | name: string | undefined = undefined, |
| 1073 | custom: Message["custom"] = undefined, | |
| 1071 | 1074 | ) { |
| 1072 | 1075 | this.name = name; |
| 1076 | this.#custom = custom; | |
| 1073 | 1077 | this.#dispatches = [dispatch]; |
| 1074 | 1078 | } |
| 1075 | 1079 | |
| ... | ... | @@ -1083,6 +1087,7 @@ const ScopeImpl = class Scope implements RootScope { |
| 1083 | 1087 | this.writeMessage({ |
| 1084 | 1088 | level, |
| 1085 | 1089 | scope: this.name, |
| 1090 | custom: this.#custom, | |
| 1086 | 1091 | get text() { |
| 1087 | 1092 | const value = formatLine(...args); |
| 1088 | 1093 | Object.defineProperty(this, "text", { value }); |
| ... | ... | @@ -1127,6 +1132,7 @@ const ScopeImpl = class Scope implements RootScope { |
| 1127 | 1132 | this.writeMessage({ |
| 1128 | 1133 | level: "info", |
| 1129 | 1134 | scope: "", |
| 1135 | custom: this.#custom, | |
| 1130 | 1136 | newline: false, |
| 1131 | 1137 | text, |
| 1132 | 1138 | time: Date.now(), |
| ... | ... | @@ -1134,11 +1140,12 @@ const ScopeImpl = class Scope implements RootScope { |
| 1134 | 1140 | }); |
| 1135 | 1141 | }; |
| 1136 | 1142 | |
| 1137 | scoped(name: string): Scope { | |
| 1143 | scoped(name: string, custom?: Message["custom"]): Scope { | |
| 1138 | 1144 | const current = this.name; |
| 1139 | 1145 | return new Scope( |
| 1140 | 1146 | this.#dispatch, |
| 1141 | 1147 | name ? current ? `${current}/${name}` : name : current, |
| 1148 | custom ? { ...this.#custom, ...custom } : this.#custom, | |
| 1142 | 1149 | ); |
| 1143 | 1150 | } |
| 1144 | 1151 |
lib/readme.changes.md+4| ... | ... | @@ -12,14 +12,18 @@ |
| 12 | 12 | - `meta.toTags` creates an object compatible with TanStack Router |
| 13 | 13 | - the `extra` object now takes an array of React-like elements among some |
| 14 | 14 | other supported forms. |
| 15 | - `log` has recieved so many bug-fix changes that it is worth ensuring your | |
| 16 | application edge cases still work as expected. | |
| 15 | 17 | |
| 16 | 18 | ### features |
| 17 | 19 | |
| 18 | 20 | - `mime`'s database contains `.eot` for embedded opentype fonts. |
| 19 | 21 | - `async.deferred` |
| 20 | 22 | - `log` |
| 23 | - many many many many rendering bug-fixes | |
| 21 | 24 | - `writeError` writes to stderr with proper logic. |
| 22 | 25 | - buffering auto-flushes synchronously when it grows past 64k. |
| 26 | - custom scopes | |
| 23 | 27 | |
| 24 | 28 | ## v4 |
| 25 | 29 |