-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
349 lines (319 loc) Β· 8.8 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* The severity level of a log event.
*/
export enum LogLevel {
error = 0,
warn,
info,
debug,
}
/**
* Information supplied to the logger
* about a log event.
*/
export interface LogEvent {
message?: string
stack?: string
}
/**
* Data associated with a log event
*/
export interface LogData {
tag: string
level: LogLevel
message: string
stack?: string
}
/**
* A log event emitter
*/
export interface Logger {
error(message: string | LogEvent): void
warn(message: string | LogEvent): void
info(message: string | LogEvent): void
debug(message: string | LogEvent): void
}
/**
* A log event handler
*/
export interface LogHandler {
(data: LogData): void
}
// Global `logga` instance
const root: NodeJS.Global | Window =
typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: // Ignore in coverage because is expected to be unreachable
// istanbul ignore next
({} as typeof global)
function Logga(): LogHandler[] {
const name = '_logga'
if (name in root) {
return root[name] ?? []
}
root[name] = []
return root[name] ?? []
}
const logga: LogHandler[] = Logga()
/**
* Take a message `string`, or `LogInfo` object,
* and emit an event with a `LogData` object.
*
* @param info
* @param level
*/
function emitLogData(
info: LogEvent | string,
tag: string,
level: LogLevel
): void {
const message =
typeof info === 'string'
? info
: typeof info === 'object'
? info?.message ?? ''
: ''
const stack = typeof info === 'object' ? info?.stack : undefined
const data: LogData = { tag, level, message, stack }
for (const handler of logga) {
handler(data)
}
}
/**
* Get all handlers.
*/
export function handlers(): LogHandler[] {
return logga
}
/**
* Add a handler.
*
* @param handler A function that handles the log data.
* @param filter Options for filtering log data prior to sending to the handler.
* @param filter.tags A list of tags that the log data should match.
* @param filter.maxLevel The maximum log level.
* @param filter.messageRegex A regex that the log message should match.
* @param filter.func A function that determines if handler is called
* @returns The handler function that was added.
*/
export function addHandler(
handler: LogHandler,
filter: {
tags?: string[]
maxLevel?: LogLevel
messageRegex?: RegExp
func?: (logData: LogData) => boolean
} = {}
): LogHandler {
let listener = handler
const { tags, maxLevel, messageRegex, func } = filter
if (
tags !== undefined ||
maxLevel !== undefined ||
messageRegex !== undefined ||
func !== undefined
) {
listener = (logData: LogData) => {
if (tags !== undefined && !tags.includes(logData.tag)) return
if (maxLevel !== undefined && logData.level > maxLevel) return
if (messageRegex !== undefined && !messageRegex.test(logData.message))
return
if (func !== undefined && !func(logData)) return
handler(logData)
}
}
logga.push(listener)
return listener
}
/**
* Remove a handler.
*
* @param handler The handler function to remove.
*/
export function removeHandler(handler: LogHandler): void {
const index = logga.indexOf(handler)
if (index > -1) logga.splice(index, 1)
}
/**
* Remove all handlers.
*/
export function removeHandlers(): void {
logga.splice(0, logga.length)
}
/**
* Replace all existing handlers with a new handler.
*
* This is a convenience function that can be used to
* replace the default handler with a new one which logs
* to the console.
*/
export function replaceHandlers(handler: LogHandler): void {
removeHandlers()
addHandler(handler)
}
const defaultHandlerHistory = new Map<string, number>()
/**
* Escape a string for inclusion in JSON.
*
* Based on the list at https://www.json.org minus the backspace character (U+0008)
*
* @param value The string to escape
*/
export function escape(value: string): string {
return value.replace(/"|\\|\/|\f|\n|\r|\t/g, (char) => {
switch (char) {
case '"':
return '"'
case '\\':
return '\\\\'
case '/':
return '\\/'
case '\f':
return '\\f'
case '\n':
return '\\n'
case '\r':
return '\\r'
case '\t':
return '\\t'
}
// Ignore in coverage because is expected to be unreachable
// istanbul ignore next
return char
})
}
/**
* Default log event handler.
*
* Prints the event data to stderr:
*
* - with cutesy emoji, colours and stack (for errors) if stderr is TTY (for human consumption)
* - as JSON if stderr is not TTY (for machine consumption e.g. log files)
*
* If in Node.js, and the
*
* @param data The log data to handle
* @param options.maxLevel The maximum log level to print. Defaults to `info`.
* @param options.showStack Whether or not to show any stack traces for errors. Defaults to `false`.
* @param options.exitOnError Whether or not to exit the process on the first error. Defaults to `true`.
* @param options.throttle.signature The log event signature to use for throttling. Defaults to '' (i.e. all events)
* @param options.throttle.duration The duration for throttling (milliseconds). Defaults to 1000ms
*/
export function defaultHandler(
data: LogData,
options: {
maxLevel?: LogLevel
fastTime?: boolean
showStack?: boolean
exitOnError?: boolean
throttle?: {
signature?: string
duration?: number
}
} = {}
): void {
const { tag, level, message, stack } = data
// Skip if greater than desired reporting level
const { maxLevel = LogLevel.info } = options
if (level > maxLevel) return
// Skip if within throttling duration for the event signature
const { throttle } = options
if (throttle !== undefined) {
const signature = throttle.signature !== undefined ? throttle.signature : ''
const eventSignature = signature
.replace(/\${tag}/, tag)
.replace(/\${level}/, level.toString())
.replace(/\${message}/, message)
const lastTime = defaultHandlerHistory.get(eventSignature)
if (lastTime !== undefined) {
const duration =
throttle.duration !== undefined ? throttle.duration : 1000
if (Date.now() - lastTime < duration) return
}
defaultHandlerHistory.set(eventSignature, Date.now())
}
// Generate a human readable or machine readable log entry based on
// environment
if (
typeof process !== 'undefined' &&
process.stderr !== undefined &&
process.stderr.isTTY !== true
) {
const { fastTime = false } = options
let json = `{"time":${
fastTime ? Date.now() : `"${new Date().toISOString()}"`
},"tag":"${tag}","level":${level},"message":"${message}"`
if (stack !== undefined) {
json += `,"stack":"${escape(stack)}"`
}
json += '}\n'
process.stderr.write(json)
} else {
const index = level < 0 ? 0 : level > 3 ? 3 : level
const label = LogLevel[index].toUpperCase().padEnd(5, ' ')
let line
// istanbul ignore next
if (typeof window !== 'undefined') {
line = `${label} ${tag} ${message}`
} else {
const emoji = [
'π¨', // error
'β ', // warn
'π', // info
'π', // debug
][index]
const colour = [
'\u001b[31;1m', // red
'\u001b[33;1m', // yellow
'\u001b[34;1m', // blue
'\u001b[30;1m', // grey (bright black)
][index]
const cyan = '\u001b[36m'
const reset = '\u001b[0m'
line = `${emoji} ${colour}${label}${reset} ${cyan}${tag}${reset} ${message}`
}
const { showStack = false } = options
if (showStack && stack !== undefined) line += '\n ' + stack
console.error(line)
}
const { exitOnError = true } = options
if (
typeof process !== 'undefined' &&
exitOnError &&
level === LogLevel.error
) {
// Optional call to avoid exception if process does not have an exit method
// See https://github.com/stencila/logga/issues/68#issuecomment-710114596
process.exit?.(1)
}
}
// Enable the default handler if there no other handler
// already enabled e.g. by another package using `logga`
if (handlers().length === 0) addHandler(defaultHandler)
/**
* Get a logger for the specific application or package.
*
* Each of the returned logger functions are the public interface for
* posting log messages.
*
* @param tag The unique application or package name
*/
export function getLogger(tag: string): Logger {
return {
error(message: string | LogEvent) {
emitLogData(message, tag, LogLevel.error)
},
warn(message: string | LogEvent) {
emitLogData(message, tag, LogLevel.warn)
},
info(message: string | LogEvent) {
emitLogData(message, tag, LogLevel.info)
},
debug(message: string | LogEvent) {
emitLogData(message, tag, LogLevel.debug)
},
}
}