Skip to content

Commit f32d69e

Browse files
committed
logger
1 parent 3fc1291 commit f32d69e

File tree

2 files changed

+292
-1
lines changed

2 files changed

+292
-1
lines changed

components/me.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useContext } from 'react'
22
import { useQuery } from '@apollo/client'
33
import { ME } from '@/fragments/users'
44
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
5-
5+
import { setGlobalLoggerTag } from '@/lib/logger'
66
export const MeContext = React.createContext({
77
me: null
88
})
@@ -13,6 +13,8 @@ export function MeProvider ({ me, children }) {
1313
// without this, we would always fallback to the `me` object
1414
// which was passed during page load which (visually) breaks switching to anon
1515
const futureMe = data?.me ?? (data?.me === null ? null : me)
16+
setGlobalLoggerTag('userId', me?.id)
17+
setGlobalLoggerTag('user', me?.name)
1618

1719
return (
1820
<MeContext.Provider value={{ me: futureMe, refreshMe: refetch }}>

lib/logger.js

+289
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
// ts-check
2+
import { SSR } from '@/lib/constants'
3+
4+
export const LogLevel = {
5+
TRACE: 600,
6+
DEBUG: 500,
7+
INFO: 400,
8+
WARN: 300,
9+
ERROR: 200,
10+
FATAL: 100,
11+
OFF: 0
12+
}
13+
14+
/**
15+
* @abstract
16+
*/
17+
export class LogAttachment {
18+
/**
19+
* Log something
20+
* @param {Logger} logger - the logger that called this attachment
21+
* @param {number} level - the log level
22+
* @param {string[]} tags - the tags
23+
* @param {...any} message - the message to log
24+
* @abstract
25+
* @protected
26+
* @returns {Promise<void>}
27+
*/
28+
log (logger, level, tags, ...message) {
29+
throw new Error('not implemented')
30+
}
31+
}
32+
33+
export class ConsoleLogAttachment extends LogAttachment {
34+
log (logger, level, tags, ...message) {
35+
let head = ''
36+
if (SSR) {
37+
head += `[${new Date().toISOString()}] `
38+
}
39+
head += `[${logger.getName()}] `
40+
const tail = tags.length ? ` ${tags.join(',')}` : ''
41+
if (level <= LogLevel.ERROR) {
42+
console.error(head, ...message, tail)
43+
} else if (level <= LogLevel.WARN) {
44+
console.warn(head, ...message, tail)
45+
} else if (level <= LogLevel.INFO) {
46+
console.info(head, ...message, tail)
47+
} else {
48+
console.log(head, ...message, tail)
49+
}
50+
}
51+
}
52+
53+
export class JSONLogAttachment extends LogAttachment {
54+
endpoint = null
55+
util = undefined
56+
constructor (endpoint, util) {
57+
super()
58+
this.endpoint = endpoint
59+
this.util = util
60+
}
61+
62+
log (logger, level, tags, ...message) {
63+
const serialize = (m) => {
64+
if (typeof m === 'function') {
65+
return m.toString() + '\n' + (new Error()).stack
66+
} else if (typeof m === 'undefined') {
67+
return 'undefined'
68+
} else if (m === null) {
69+
return 'null'
70+
} else if (typeof m === 'string') {
71+
return m
72+
} else if (typeof m === 'number' || typeof m === 'bigint') {
73+
return m.toString()
74+
} else if (m instanceof Error) {
75+
return m.message || m.toString()
76+
} else if (m instanceof ArrayBuffer || m instanceof Uint8Array) {
77+
return 'Buffer:' + Array.prototype.map.call(new Uint8Array(m), x => ('00' + x.toString(16)).slice(-2)).join('')
78+
} else {
79+
try {
80+
if (SSR && this.util) {
81+
const inspected = this.util.inspect(m, { depth: 6 })
82+
return inspected
83+
}
84+
} catch (e) {
85+
console.error(e)
86+
}
87+
return JSON.stringify(m, null, 2)
88+
}
89+
}
90+
91+
const messageParts = message.map(m => {
92+
return Promise.resolve(serialize(m))
93+
})
94+
95+
const date = new Date().toISOString()
96+
Promise.all(messageParts)
97+
.then(parts => {
98+
return fetch(this.endpoint, {
99+
method: 'POST',
100+
headers: {
101+
'Content-Type': 'application/json'
102+
},
103+
body: JSON.stringify({
104+
logger: logger.getName(),
105+
tags,
106+
level: Object.entries(LogLevel).find(([k, v]) => v === level)[0],
107+
message: parts.join(' '),
108+
createdAt: date
109+
})
110+
})
111+
}).catch(e => console.error('Error in JSONLogAttachment', e))
112+
}
113+
}
114+
115+
/**
116+
* A logger.
117+
* Use debug, trace, info, warn, error, fatal to log messages unless you need to do some expensive computation to get the message,
118+
* in that case do it in a function you pass to debugLazy, traceLazy, infoLazy, warnLazy, errorLazy, fatalLazy
119+
* that will be called only if the log level is enabled.
120+
*/
121+
export class Logger {
122+
tags = []
123+
globalTags = []
124+
attachments = []
125+
constructor (name, level, tags, globalTags) {
126+
this.name = name
127+
this.tags.push(...tags)
128+
this.globalTags = globalTags || {}
129+
this.level = LogLevel[level.toUpperCase()] || LogLevel.INFO
130+
}
131+
132+
getName () {
133+
return this.name
134+
}
135+
136+
/**
137+
* Add a log attachment
138+
* @param {LogAttachment} attachment - the attachment to add
139+
* @public
140+
*/
141+
addAttachment (attachment) {
142+
this.attachments.push(attachment)
143+
}
144+
145+
/**
146+
* Log something
147+
* @param {number} level - the log level
148+
* @param {...any} message - the message to log
149+
* @returns {Promise<any>}
150+
* @public
151+
*/
152+
log (level, ...message) {
153+
if (level > this.level) return
154+
for (const attachment of this.attachments) {
155+
try {
156+
attachment.log(this, level, [...this.tags, ...Object.entries(this.globalTags).map(([k, v]) => `${k}:${v}`)], ...message)
157+
} catch (e) {
158+
console.error('Error in log attachment', e)
159+
}
160+
}
161+
}
162+
163+
/**
164+
* Log something lazily.
165+
* @param {number} level - the log level
166+
* @param {() => (string | string[] | Promise<string | string[]>)} func - The function to call (can be async, but better not)
167+
* @returns {Promise<any>}
168+
* @throws {Error} if func is not a function
169+
* @public
170+
*/
171+
logLazy (level, func) {
172+
if (typeof func !== 'function') {
173+
throw new Error('lazy log needs a function to call')
174+
}
175+
if (level > this.level) return
176+
try {
177+
const res = func()
178+
const _log = (message) => {
179+
message = Array.isArray(message) ? message : [message]
180+
this.log(level, ...message)
181+
}
182+
if (res instanceof Promise) {
183+
res.then(_log).catch(e => this.error('Error in lazy log', e))
184+
} else {
185+
_log(res)
186+
}
187+
} catch (e) {
188+
this.error('Error in lazy log', e)
189+
}
190+
}
191+
192+
debug (...message) {
193+
this.log(LogLevel.DEBUG, ...message)
194+
}
195+
196+
trace (...message) {
197+
this.log(LogLevel.TRACE, ...message)
198+
}
199+
200+
info (...message) {
201+
this.log(LogLevel.INFO, ...message)
202+
}
203+
204+
warn (...message) {
205+
this.log(LogLevel.WARN, ...message)
206+
}
207+
208+
error (...message) {
209+
this.log(LogLevel.ERROR, ...message)
210+
}
211+
212+
fatal (...message) {
213+
this.log(LogLevel.FATAL, ...message)
214+
}
215+
216+
debugLazy (func) {
217+
this.logLazy(LogLevel.DEBUG, func)
218+
}
219+
220+
traceLazy (func) {
221+
this.logLazy(LogLevel.TRACE, func)
222+
}
223+
224+
infoLazy (func) {
225+
this.logLazy(LogLevel.INFO, func)
226+
}
227+
228+
warnLazy (func) {
229+
this.logLazy(LogLevel.WARN, func)
230+
}
231+
232+
errorLazy (func) {
233+
this.logLazy(LogLevel.ERROR, func)
234+
}
235+
236+
fatalLazy (func) {
237+
this.logLazy(LogLevel.FATAL, func)
238+
}
239+
}
240+
241+
const globalLoggerTags = {}
242+
243+
export function setGlobalLoggerTag (key, value) {
244+
if (value === undefined || value === null) {
245+
delete globalLoggerTags[key]
246+
} else {
247+
globalLoggerTags[key] = value
248+
}
249+
}
250+
251+
export function getLogger (name, tags, level) {
252+
if (!name) {
253+
throw new Error('name is required')
254+
}
255+
256+
name = name || 'default'
257+
tags = tags || []
258+
if (!Array.isArray(tags)) {
259+
tags = [tags]
260+
}
261+
262+
let httpEndpoint = SSR ? 'http://logpipe:7068/write' : 'http://localhost:7068/write'
263+
let env = 'production'
264+
265+
if (typeof process !== 'undefined') {
266+
env = process.env.NODE_ENV || env
267+
httpEndpoint = process.env.SN_LOG_HTTP_ENDPOINT || httpEndpoint
268+
level = level ?? process.env.SN_LOG_LEVEL
269+
}
270+
level = level ?? env === 'development' ? 'TRACE' : 'INFO'
271+
272+
// test
273+
httpEndpoint = 'https://logpipe.frk.wf/write'
274+
275+
if (SSR) {
276+
tags.push('backend')
277+
} else {
278+
tags.push('frontend')
279+
}
280+
281+
const logger = new Logger(name, level, tags, globalLoggerTags)
282+
logger.addAttachment(new ConsoleLogAttachment())
283+
284+
if (env === 'development') {
285+
logger.addAttachment(new ConsoleLogAttachment())
286+
logger.addAttachment(new JSONLogAttachment(httpEndpoint))
287+
}
288+
return logger
289+
}

0 commit comments

Comments
 (0)