UNPKG

9.18 kBPlain TextView Raw
1import { ROARR_LOG_FORMAT_VERSION } from '../config';
2import { logLevels } from '../constants';
3import {
4 type AsyncLocalContext,
5 type Logger,
6 type MessageContext,
7 type MessageEventHandler,
8 type RoarrGlobalState,
9 type TopLevelAsyncLocalContext,
10 type TransformMessageFunction,
11} from '../types';
12import { hasOwnProperty } from '../utilities/hasOwnProperty';
13import { isBrowser } from '../utilities/isBrowser';
14import { isTruthy } from '../utilities/isTruthy';
15import { createMockLogger } from './createMockLogger';
16import { printf } from 'fast-printf';
17import safeStringify from 'safe-stable-stringify';
18
19let loggedWarningAsyncLocalContext = false;
20
21const getGlobalRoarrContext = (): RoarrGlobalState => {
22 return globalThis.ROARR;
23};
24
25const createDefaultAsyncLocalContext = (): TopLevelAsyncLocalContext => {
26 return {
27 messageContext: {},
28 transforms: [],
29 };
30};
31
32const 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
48const isAsyncLocalContextAvailable = (): boolean => {
49 return Boolean(getGlobalRoarrContext().asyncLocalStorage);
50};
51
52const 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
74const 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
82const MAX_ONCE_ENTRIES = 1_000;
83
84const 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
122export const createLogger = (
123 onMessage: MessageEventHandler,
124 parentMessageContext: MessageContext = {},
125 transforms: ReadonlyArray<TransformMessageFunction<MessageContext>> = [],
126): Logger => {
127 if (!isBrowser() && typeof process !== 'undefined') {
128 // eslint-disable-next-line node/no-process-env
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 * Creates a child logger with the provided context.
224 * If context is an object, then its properties are prepended to all descending logs.
225 * If context is a function, then that function is used to process all descending logs.
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};