Suite of AT Protocol TypeScript libraries built on web standards
1import {
2 configure,
3 getConsoleSink,
4 getLogger,
5 type Logger,
6 type LogLevel,
7 type LogMethod,
8 type Sink,
9} from "@logtape/logtape";
10import { getFileSink } from "@logtape/file";
11import process from "node:process";
12
13const isDeno = typeof Deno !== "undefined";
14type LoggingConfig = {
15 allSystemsEnabled: boolean;
16 enabledSystems: string[];
17 enabled: boolean;
18 level: LogLevel;
19 logDestination?: string;
20};
21
22const getEnv = (key: string): string | undefined => {
23 if (isDeno) {
24 try {
25 return Deno.env.get(key);
26 } catch {
27 return undefined;
28 }
29 }
30
31 return process.env[key];
32};
33
34let config: LoggingConfig | undefined;
35let configurePromise: Promise<void> | undefined;
36
37async function ensureConfigured() {
38 const {
39 enabled,
40 level,
41 logDestination,
42 } = getLoggingConfig();
43 if (!enabled) return;
44 if (configurePromise) {
45 await configurePromise;
46 return;
47 }
48
49 const sinks: Record<string, Sink> = {
50 console: getConsoleSink(),
51 };
52
53 if (logDestination) {
54 sinks.file = getFileSink(logDestination);
55 }
56
57 configurePromise = configure({
58 sinks,
59 loggers: [
60 {
61 category: [],
62 lowestLevel: level,
63 sinks: logDestination ? ["console", "file"] : ["console"],
64 },
65 ],
66 }).catch((error) => {
67 configurePromise = undefined;
68 throw error;
69 });
70
71 await configurePromise;
72}
73
74const subsystemLoggers: Record<string, Logger> = {};
75
76export const subsystemLogger = (name: string): Logger => {
77 if (subsystemLoggers[name]) return subsystemLoggers[name];
78
79 subsystemLoggers[name] = wrapLogger(name, getLogger([name]));
80 return subsystemLoggers[name];
81};
82
83export function _resetLoggerStateForTest(): void {
84 config = undefined;
85 configurePromise = undefined;
86 for (const name in subsystemLoggers) {
87 delete subsystemLoggers[name];
88 }
89}
90
91function getLoggingConfig(): LoggingConfig {
92 if (config) {
93 return config;
94 }
95
96 const logSystems = getEnv("LOG_SYSTEMS") || "";
97 const enabledSystems = logSystems.replace(",", " ")
98 .split(" ")
99 .filter(Boolean);
100 const enabledEnv = getEnv("LOG_ENABLED");
101
102 config = {
103 allSystemsEnabled: !logSystems,
104 enabledSystems,
105 enabled: enabledEnv === "true" || enabledEnv === "t" || enabledEnv === "1",
106 level: (getEnv("LOG_LEVEL") || "info") as LogLevel,
107 logDestination: getEnv("LOG_DESTINATION"),
108 };
109 return config;
110}
111
112function isSubsystemEnabled(name: string): boolean {
113 const { allSystemsEnabled, enabled, enabledSystems } = getLoggingConfig();
114 return enabled && (allSystemsEnabled || enabledSystems.includes(name));
115}
116
117function wrapLogger(name: string, logger: Logger): Logger {
118 return new Proxy(logger, {
119 get(target, property, receiver) {
120 if (property === "parent") {
121 return target.parent === null ? null : wrapLogger(name, target.parent);
122 }
123
124 if (property === "getChild") {
125 return (subcategory: Parameters<Logger["getChild"]>[0]) =>
126 wrapLogger(name, target.getChild(subcategory));
127 }
128
129 if (property === "with") {
130 return (properties: Parameters<Logger["with"]>[0]) =>
131 wrapLogger(name, target.with(properties));
132 }
133
134 if (
135 property === "trace" ||
136 property === "debug" ||
137 property === "info" ||
138 property === "warn" ||
139 property === "warning" ||
140 property === "error" ||
141 property === "fatal"
142 ) {
143 return wrapLogMethod(
144 name,
145 Reflect.get(target, property, receiver).bind(target) as LogMethod,
146 );
147 }
148
149 if (property === "emit") {
150 return wrapEmitMethod(
151 name,
152 Reflect.get(target, property, receiver).bind(
153 target,
154 ) as Logger["emit"],
155 );
156 }
157
158 return Reflect.get(target, property, receiver);
159 },
160 });
161}
162
163function wrapLogMethod(name: string, method: LogMethod): LogMethod {
164 return ((...args: unknown[]) => {
165 if (!isSubsystemEnabled(name)) {
166 return;
167 }
168
169 ensureConfigured().catch(console.error);
170 Reflect.apply(method as (...args: unknown[]) => void, undefined, args);
171 }) as LogMethod;
172}
173
174function wrapEmitMethod(name: string, method: Logger["emit"]): Logger["emit"] {
175 return ((record) => {
176 if (!isSubsystemEnabled(name)) {
177 return;
178 }
179
180 ensureConfigured().catch(console.error);
181 method(record);
182 }) as Logger["emit"];
183}