1 | import { ROARR_LOG_FORMAT_VERSION } from '../config';
|
2 | import { logLevels } from '../constants';
|
3 | import {
|
4 | type AsyncLocalContext,
|
5 | type Logger,
|
6 | type MessageContext,
|
7 | type MessageEventHandler,
|
8 | type RoarrGlobalState,
|
9 | type TopLevelAsyncLocalContext,
|
10 | type TransformMessageFunction,
|
11 | } from '../types';
|
12 | import { hasOwnProperty } from '../utilities/hasOwnProperty';
|
13 | import { isBrowser } from '../utilities/isBrowser';
|
14 | import { isTruthy } from '../utilities/isTruthy';
|
15 | import { createMockLogger } from './createMockLogger';
|
16 | import { printf } from 'fast-printf';
|
17 | import safeStringify from 'safe-stable-stringify';
|
18 |
|
19 | let loggedWarningAsyncLocalContext = false;
|
20 |
|
21 | const getGlobalRoarrContext = (): RoarrGlobalState => {
|
22 | return globalThis.ROARR;
|
23 | };
|
24 |
|
25 | const createDefaultAsyncLocalContext = (): TopLevelAsyncLocalContext => {
|
26 | return {
|
27 | messageContext: {},
|
28 | transforms: [],
|
29 | };
|
30 | };
|
31 |
|
32 | const getAsyncLocalContext = (): AsyncLocalContext => {
|
33 | const asyncLocalStorage = getGlobalRoarrContext().asyncLocalStorage;
|
34 |
|
35 | if (!asyncLocalStorage) {
|
36 | throw new Error('AsyncLocalContext is unavailable.');
|
37 | }
|
38 |
|
39 | const asyncLocalContext = asyncLocalStorage.getStore();
|
40 |
|
41 | if (asyncLocalContext) {
|
42 | return asyncLocalContext;
|
43 | }
|
44 |
|
45 | return createDefaultAsyncLocalContext();
|
46 | };
|
47 |
|
48 | const isAsyncLocalContextAvailable = (): boolean => {
|
49 | return Boolean(getGlobalRoarrContext().asyncLocalStorage);
|
50 | };
|
51 |
|
52 | const getSequence = () => {
|
53 | if (isAsyncLocalContextAvailable()) {
|
54 | const asyncLocalContext = getAsyncLocalContext();
|
55 |
|
56 | if (
|
57 | hasOwnProperty(asyncLocalContext, 'sequenceRoot') &&
|
58 | hasOwnProperty(asyncLocalContext, 'sequence') &&
|
59 | typeof asyncLocalContext.sequence === 'number'
|
60 | ) {
|
61 | return (
|
62 | String(asyncLocalContext.sequenceRoot) +
|
63 | '.' +
|
64 | String(asyncLocalContext.sequence++)
|
65 | );
|
66 | }
|
67 |
|
68 | return String(getGlobalRoarrContext().sequence++);
|
69 | }
|
70 |
|
71 | return String(getGlobalRoarrContext().sequence++);
|
72 | };
|
73 |
|
74 | const createChildLogger = (log: Logger, logLevel: number) => {
|
75 | return (a, b, c, d, e, f, g, h, index, index_) => {
|
76 | log.child({
|
77 | logLevel,
|
78 | })(a, b, c, d, e, f, g, h, index, index_);
|
79 | };
|
80 | };
|
81 |
|
82 | const MAX_ONCE_ENTRIES = 1_000;
|
83 |
|
84 | const createOnceChildLogger = (log: Logger, logLevel: number) => {
|
85 | return (a, b, c, d, e, f, g, h, index, index_) => {
|
86 | const key = safeStringify({
|
87 | a,
|
88 | b,
|
89 | c,
|
90 | d,
|
91 | e,
|
92 | f,
|
93 | g,
|
94 | h,
|
95 | i: index,
|
96 | j: index_,
|
97 | logLevel,
|
98 | });
|
99 |
|
100 | if (!key) {
|
101 | throw new Error('Expected key to be a string');
|
102 | }
|
103 |
|
104 | const onceLog = getGlobalRoarrContext().onceLog;
|
105 |
|
106 | if (onceLog.has(key)) {
|
107 | return;
|
108 | }
|
109 |
|
110 | onceLog.add(key);
|
111 |
|
112 | if (onceLog.size > MAX_ONCE_ENTRIES) {
|
113 | onceLog.clear();
|
114 | }
|
115 |
|
116 | log.child({
|
117 | logLevel,
|
118 | })(a, b, c, d, e, f, g, h, index, index_);
|
119 | };
|
120 | };
|
121 |
|
122 | export const createLogger = (
|
123 | onMessage: MessageEventHandler,
|
124 | parentMessageContext: MessageContext = {},
|
125 | transforms: ReadonlyArray<TransformMessageFunction<MessageContext>> = [],
|
126 | ): Logger => {
|
127 | if (!isBrowser() && typeof process !== 'undefined') {
|
128 |
|
129 | const enabled = isTruthy(process.env.ROARR_LOG ?? '');
|
130 |
|
131 | if (!enabled) {
|
132 | return createMockLogger(onMessage, parentMessageContext);
|
133 | }
|
134 | }
|
135 |
|
136 | const log = (
|
137 | a: any,
|
138 | b: any,
|
139 | c: any,
|
140 | d: any,
|
141 | e: any,
|
142 | f: any,
|
143 | g: any,
|
144 | h: any,
|
145 | index: any,
|
146 | index_: any,
|
147 | ) => {
|
148 | const time = Date.now();
|
149 | const sequence = getSequence();
|
150 |
|
151 | let asyncLocalContext: AsyncLocalContext;
|
152 |
|
153 | if (isAsyncLocalContextAvailable()) {
|
154 | asyncLocalContext = getAsyncLocalContext();
|
155 | } else {
|
156 | asyncLocalContext = createDefaultAsyncLocalContext();
|
157 | }
|
158 |
|
159 | let context;
|
160 | let message;
|
161 |
|
162 | if (typeof a === 'string') {
|
163 | context = {
|
164 | ...asyncLocalContext.messageContext,
|
165 | ...parentMessageContext,
|
166 | };
|
167 | } else {
|
168 | context = {
|
169 | ...asyncLocalContext.messageContext,
|
170 | ...parentMessageContext,
|
171 | ...a,
|
172 | };
|
173 | }
|
174 |
|
175 | if (typeof a === 'string' && b === undefined) {
|
176 | message = a;
|
177 | } else if (typeof a === 'string') {
|
178 | if (!a.includes('%')) {
|
179 | throw new Error(
|
180 | 'When a string parameter is followed by other arguments, then it is assumed that you are attempting to format a message using printf syntax. You either forgot to add printf bindings or if you meant to add context to the log message, pass them in an object as the first parameter.',
|
181 | );
|
182 | }
|
183 |
|
184 | message = printf(a, b, c, d, e, f, g, h, index, index_);
|
185 | } else {
|
186 | let fallbackMessage = b;
|
187 |
|
188 | if (typeof b !== 'string') {
|
189 | if (b === undefined) {
|
190 | fallbackMessage = '';
|
191 | } else {
|
192 | throw new TypeError(
|
193 | 'Message must be a string. Received ' + typeof b + '.',
|
194 | );
|
195 | }
|
196 | }
|
197 |
|
198 | message = printf(fallbackMessage, c, d, e, f, g, h, index, index_);
|
199 | }
|
200 |
|
201 | let packet = {
|
202 | context,
|
203 | message,
|
204 | sequence,
|
205 | time,
|
206 | version: ROARR_LOG_FORMAT_VERSION,
|
207 | };
|
208 |
|
209 | for (const transform of [...asyncLocalContext.transforms, ...transforms]) {
|
210 | packet = transform(packet);
|
211 |
|
212 | if (typeof packet !== 'object' || packet === null) {
|
213 | throw new Error(
|
214 | 'Message transform function must return a message object.',
|
215 | );
|
216 | }
|
217 | }
|
218 |
|
219 | onMessage(packet);
|
220 | };
|
221 |
|
222 | |
223 |
|
224 |
|
225 |
|
226 |
|
227 | log.child = (context) => {
|
228 | let asyncLocalContext: AsyncLocalContext;
|
229 |
|
230 | if (isAsyncLocalContextAvailable()) {
|
231 | asyncLocalContext = getAsyncLocalContext();
|
232 | } else {
|
233 | asyncLocalContext = createDefaultAsyncLocalContext();
|
234 | }
|
235 |
|
236 | if (typeof context === 'function') {
|
237 | return createLogger(
|
238 | onMessage,
|
239 | {
|
240 | ...asyncLocalContext.messageContext,
|
241 | ...parentMessageContext,
|
242 | ...context,
|
243 | },
|
244 | [context, ...transforms],
|
245 | );
|
246 | }
|
247 |
|
248 | return createLogger(
|
249 | onMessage,
|
250 | {
|
251 | ...asyncLocalContext.messageContext,
|
252 | ...parentMessageContext,
|
253 | ...context,
|
254 | },
|
255 | transforms,
|
256 | );
|
257 | };
|
258 |
|
259 | log.getContext = () => {
|
260 | let asyncLocalContext: AsyncLocalContext;
|
261 |
|
262 | if (isAsyncLocalContextAvailable()) {
|
263 | asyncLocalContext = getAsyncLocalContext();
|
264 | } else {
|
265 | asyncLocalContext = createDefaultAsyncLocalContext();
|
266 | }
|
267 |
|
268 | return {
|
269 | ...asyncLocalContext.messageContext,
|
270 | ...parentMessageContext,
|
271 | };
|
272 | };
|
273 |
|
274 | log.adopt = async (routine, context) => {
|
275 | if (!isAsyncLocalContextAvailable()) {
|
276 | if (loggedWarningAsyncLocalContext === false) {
|
277 | loggedWarningAsyncLocalContext = true;
|
278 |
|
279 | onMessage({
|
280 | context: {
|
281 | logLevel: logLevels.warn,
|
282 | package: 'roarr',
|
283 | },
|
284 | message:
|
285 | 'async_hooks are unavailable; Roarr.adopt will not function as expected',
|
286 | sequence: getSequence(),
|
287 | time: Date.now(),
|
288 | version: ROARR_LOG_FORMAT_VERSION,
|
289 | });
|
290 | }
|
291 |
|
292 | return routine();
|
293 | }
|
294 |
|
295 | const asyncLocalContext = getAsyncLocalContext();
|
296 |
|
297 | let sequenceRoot;
|
298 |
|
299 | if (
|
300 | hasOwnProperty(asyncLocalContext, 'sequenceRoot') &&
|
301 | hasOwnProperty(asyncLocalContext, 'sequence') &&
|
302 | typeof asyncLocalContext.sequence === 'number'
|
303 | ) {
|
304 | sequenceRoot =
|
305 | asyncLocalContext.sequenceRoot +
|
306 | '.' +
|
307 | String(asyncLocalContext.sequence++);
|
308 | } else {
|
309 | sequenceRoot = String(getGlobalRoarrContext().sequence++);
|
310 | }
|
311 |
|
312 | let nextContext = {
|
313 | ...asyncLocalContext.messageContext,
|
314 | };
|
315 |
|
316 | const nextTransforms = [...asyncLocalContext.transforms];
|
317 |
|
318 | if (typeof context === 'function') {
|
319 | nextTransforms.push(context);
|
320 | } else {
|
321 | nextContext = {
|
322 | ...nextContext,
|
323 | ...context,
|
324 | };
|
325 | }
|
326 |
|
327 | const asyncLocalStorage = getGlobalRoarrContext().asyncLocalStorage;
|
328 |
|
329 | if (!asyncLocalStorage) {
|
330 | throw new Error('Async local context unavailable.');
|
331 | }
|
332 |
|
333 | return asyncLocalStorage.run(
|
334 | {
|
335 | messageContext: nextContext,
|
336 | sequence: 0,
|
337 | sequenceRoot,
|
338 | transforms: nextTransforms,
|
339 | },
|
340 | () => {
|
341 | return routine();
|
342 | },
|
343 | );
|
344 | };
|
345 |
|
346 | log.debug = createChildLogger(log, logLevels.debug);
|
347 | log.debugOnce = createOnceChildLogger(log, logLevels.debug);
|
348 | log.error = createChildLogger(log, logLevels.error);
|
349 | log.errorOnce = createOnceChildLogger(log, logLevels.error);
|
350 | log.fatal = createChildLogger(log, logLevels.fatal);
|
351 | log.fatalOnce = createOnceChildLogger(log, logLevels.fatal);
|
352 | log.info = createChildLogger(log, logLevels.info);
|
353 | log.infoOnce = createOnceChildLogger(log, logLevels.info);
|
354 | log.trace = createChildLogger(log, logLevels.trace);
|
355 | log.traceOnce = createOnceChildLogger(log, logLevels.trace);
|
356 | log.warn = createChildLogger(log, logLevels.warn);
|
357 | log.warnOnce = createOnceChildLogger(log, logLevels.warn);
|
358 |
|
359 | return log;
|
360 | };
|