Skip to content

Commit

Permalink
logger
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardobl committed Jan 4, 2025
1 parent 3fc1291 commit e51deed
Show file tree
Hide file tree
Showing 2 changed files with 288 additions and 1 deletion.
4 changes: 3 additions & 1 deletion components/me.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useContext } from 'react'
import { useQuery } from '@apollo/client'
import { ME } from '@/fragments/users'
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'

import { setGlobalLoggerTag } from '@/lib/logger'
export const MeContext = React.createContext({
me: null
})
Expand All @@ -13,6 +13,8 @@ export function MeProvider ({ me, children }) {
// without this, we would always fallback to the `me` object
// which was passed during page load which (visually) breaks switching to anon
const futureMe = data?.me ?? (data?.me === null ? null : me)
setGlobalLoggerTag('userId', me?.id)
setGlobalLoggerTag('user', me?.name)

return (
<MeContext.Provider value={{ me: futureMe, refreshMe: refetch }}>
Expand Down
285 changes: 285 additions & 0 deletions lib/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
// 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, level) {
if (!name) {
throw new Error('name is required')
}

name = name || 'default'
tags = tags || []
if (!Array.isArray(tags)) {
tags = [tags]
}

// Note: typeof checks because worker doesn't have process.env
let httpEndpoint = SSR ? 'http://logpipe:7068/write' : 'http://localhost:7068/write'
let env = 'production'

if (typeof process !== 'undefined') {
env = process.env.NODE_ENV || env
httpEndpoint = process.env.SN_LOG_HTTP_ENDPOINT || httpEndpoint
level = level ?? process.env.SN_LOG_LEVEL
}
level = level ?? env === 'development' ? 'TRACE' : 'INFO'

// test
httpEndpoint = 'https://logpipe.frk.wf/write'

if (SSR) {
tags.push('backend')
} else {
tags.push('frontend')
}

const logger = new Logger(name, level, tags, globalLoggerTags)
logger.addAttachment(new ConsoleLogAttachment())

if (env === 'development') {
logger.addAttachment(new ConsoleLogAttachment())
logger.addAttachment(new JSONLogAttachment(httpEndpoint))
}
return logger
}

0 comments on commit e51deed

Please sign in to comment.