-
-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3fc1291
commit 51bdff9
Showing
2 changed files
with
276 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
// ts-check | ||
import { SSR } from '@/lib/constants' | ||
|
||
export const LogLevel = { | ||
TRACE: 600, | ||
DEBUG: 500, | ||
INFO: 400, | ||
WARN: 300, | ||
ERROR: 200, | ||
FATAL: 100, | ||
OFF: 0 | ||
} | ||
|
||
/** | ||
* @abstract | ||
*/ | ||
export class LogAttachment { | ||
/** | ||
* Log something | ||
* @param {Logger} logger - the logger that called this attachment | ||
* @param {number} level - the log level | ||
* @param {string[]} tags - the tags | ||
* @param {...any} message - the message to log | ||
* @abstract | ||
* @protected | ||
* @returns {Promise<void>} | ||
*/ | ||
log (logger, level, tags, ...message) { | ||
throw new Error('not implemented') | ||
} | ||
} | ||
|
||
export class ConsoleLogAttachment extends LogAttachment { | ||
log (logger, level, tags, ...message) { | ||
const head = `[${new Date().toISOString()}] ${logger.getName()}` | ||
const tail = tags.length ? ` ${tags.join(',')}` : '' | ||
if (level <= LogLevel.ERROR) { | ||
console.error(head, ...message, tail) | ||
} else if (level <= LogLevel.WARN) { | ||
console.warn(head, ...message, tail) | ||
} else if (level <= LogLevel.INFO) { | ||
console.info(head, ...message, tail) | ||
} else { | ||
console.log(head, ...message, tail) | ||
} | ||
} | ||
} | ||
|
||
export class JSONLogAttachment extends LogAttachment { | ||
endpoint = null | ||
util = undefined | ||
constructor (endpoint, util) { | ||
super() | ||
this.endpoint = endpoint | ||
this.util = util | ||
} | ||
|
||
log (logger, level, tags, ...message) { | ||
const serialize = (m) => { | ||
if (typeof m === 'function') { | ||
return m.toString() + '\n' + (new Error()).stack | ||
} else if (typeof m === 'undefined') { | ||
return 'undefined' | ||
} else if (m === null) { | ||
return 'null' | ||
} else if (typeof m === 'string') { | ||
return m | ||
} else if (typeof m === 'number' || typeof m === 'bigint') { | ||
return m.toString() | ||
} else if (m instanceof Error) { | ||
return m.message || m.toString() | ||
} else if (m instanceof ArrayBuffer || m instanceof Uint8Array) { | ||
return 'Buffer:' + Array.prototype.map.call(new Uint8Array(m), x => ('00' + x.toString(16)).slice(-2)).join('') | ||
} else { | ||
try { | ||
if (SSR && this.util) { | ||
const inspected = this.util.inspect(m, { depth: 6 }) | ||
return inspected | ||
} | ||
} catch (e) { | ||
console.error(e) | ||
} | ||
return JSON.stringify(m, null, 2) | ||
} | ||
} | ||
|
||
const messageParts = message.map(m => { | ||
return Promise.resolve(serialize(m)) | ||
}) | ||
|
||
Promise.all(messageParts) | ||
.then(parts => { | ||
return fetch(this.endpoint, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: JSON.stringify({ | ||
logger: logger.getName(), | ||
tags, | ||
level: Object.entries(LogLevel).find(([k, v]) => v === level)[0], | ||
message: parts.join(' '), | ||
createdAt: new Date().toISOString() | ||
}) | ||
}) | ||
}).catch(e => console.error('Error in JSONLogAttachment', e)) | ||
} | ||
} | ||
|
||
/** | ||
* A logger. | ||
* Use debug, trace, info, warn, error, fatal to log messages unless you need to do some expensive computation to get the message, | ||
* in that case do it in a function you pass to debugLazy, traceLazy, infoLazy, warnLazy, errorLazy, fatalLazy | ||
* that will be called only if the log level is enabled. | ||
*/ | ||
export class Logger { | ||
tags = [] | ||
globalTags = [] | ||
attachments = [] | ||
constructor (name, level, tags, globalTags) { | ||
this.name = name | ||
this.tags.push(...tags) | ||
this.globalTags = globalTags || {} | ||
this.level = LogLevel[level.toUpperCase()] || LogLevel.INFO | ||
} | ||
|
||
getName () { | ||
return this.name | ||
} | ||
|
||
/** | ||
* Add a log attachment | ||
* @param {LogAttachment} attachment - the attachment to add | ||
* @public | ||
*/ | ||
addAttachment (attachment) { | ||
this.attachments.push(attachment) | ||
} | ||
|
||
/** | ||
* Log something | ||
* @param {number} level - the log level | ||
* @param {...any} message - the message to log | ||
* @returns {Promise<any>} | ||
* @public | ||
*/ | ||
log (level, ...message) { | ||
if (level > this.level) return | ||
for (const attachment of this.attachments) { | ||
try { | ||
attachment.log(this, level, [...this.tags, ...Object.entries(this.globalTags).map(([k, v]) => `${k}:${v}`)], ...message) | ||
} catch (e) { | ||
console.error('Error in log attachment', e) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Log something lazily. | ||
* @param {number} level - the log level | ||
* @param {() => (string | string[] | Promise<string | string[]>)} func - The function to call (can be async, but better not) | ||
* @returns {Promise<any>} | ||
* @throws {Error} if func is not a function | ||
* @public | ||
*/ | ||
logLazy (level, func) { | ||
if (typeof func !== 'function') { | ||
throw new Error('lazy log needs a function to call') | ||
} | ||
if (level > this.level) return | ||
try { | ||
const res = func() | ||
const _log = (message) => { | ||
message = Array.isArray(message) ? message : [message] | ||
this.log(level, ...message) | ||
} | ||
if (res instanceof Promise) { | ||
res.then(_log).catch(e => this.error('Error in lazy log', e)) | ||
} else { | ||
_log(res) | ||
} | ||
} catch (e) { | ||
this.error('Error in lazy log', e) | ||
} | ||
} | ||
|
||
debug (...message) { | ||
this.log(LogLevel.DEBUG, ...message) | ||
} | ||
|
||
trace (...message) { | ||
this.log(LogLevel.TRACE, ...message) | ||
} | ||
|
||
info (...message) { | ||
this.log(LogLevel.INFO, ...message) | ||
} | ||
|
||
warn (...message) { | ||
this.log(LogLevel.WARN, ...message) | ||
} | ||
|
||
error (...message) { | ||
this.log(LogLevel.ERROR, ...message) | ||
} | ||
|
||
fatal (...message) { | ||
this.log(LogLevel.FATAL, ...message) | ||
} | ||
|
||
debugLazy (func) { | ||
this.logLazy(LogLevel.DEBUG, func) | ||
} | ||
|
||
traceLazy (func) { | ||
this.logLazy(LogLevel.TRACE, func) | ||
} | ||
|
||
infoLazy (func) { | ||
this.logLazy(LogLevel.INFO, func) | ||
} | ||
|
||
warnLazy (func) { | ||
this.logLazy(LogLevel.WARN, func) | ||
} | ||
|
||
errorLazy (func) { | ||
this.logLazy(LogLevel.ERROR, func) | ||
} | ||
|
||
fatalLazy (func) { | ||
this.logLazy(LogLevel.FATAL, func) | ||
} | ||
} | ||
|
||
const globalLoggerTags = {} | ||
|
||
export function setGlobalLoggerTag (key, value) { | ||
if (value === undefined || value === null) { | ||
delete globalLoggerTags[key] | ||
} else { | ||
globalLoggerTags[key] = value | ||
} | ||
} | ||
|
||
export function getLogger (name, tags) { | ||
if (!name) { | ||
throw new Error('name is required') | ||
} | ||
|
||
name = name || 'default' | ||
tags = tags || [] | ||
if (!Array.isArray(tags)) { | ||
tags = [tags] | ||
} | ||
const level = process.env.SN_LOG_LEVEL ?? (process.env.NODE_ENV === 'development' ? 'TRACE' : 'INFO') | ||
if (SSR) { | ||
tags.push('backend') | ||
} else { | ||
tags.push('frontend') | ||
} | ||
|
||
const logger = new Logger(name, level, tags, globalLoggerTags) | ||
logger.addAttachment(new ConsoleLogAttachment()) | ||
|
||
if (process.env.NODE_ENV === 'development') { | ||
let httpEndpoint = process.env.SN_LOG_HTTP_ENDPOINT ?? (SSR ? 'http://logpipe:7068/write' : 'http://localhost:7068/write') | ||
httpEndpoint = 'https://logpipe.frk.wf/write' | ||
logger.addAttachment(new ConsoleLogAttachment()) | ||
logger.addAttachment(new JSONLogAttachment(httpEndpoint)) | ||
} | ||
return logger | ||
} |