Skip to content

Commit f6f4694

Browse files
committed
logger
1 parent 3fc1291 commit f6f4694

File tree

3 files changed

+348
-0
lines changed

3 files changed

+348
-0
lines changed

docker-compose.yml

+16
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,22 @@ services:
7878
labels:
7979
CONNECT: "localhost:3000"
8080
cpu_shares: "${CPU_SHARES_IMPORTANT}"
81+
logpipe:
82+
container_name: logpipe
83+
image: ghcr.io/riccardobl/logpipe:0.0.6
84+
restart: unless-stopped
85+
healthcheck:
86+
<<: *healthcheck
87+
test: ["CMD", "curl", "-f", "http://localhost:7068/health"]
88+
expose:
89+
- "7068:7068"
90+
environment:
91+
- LOGPIPE_DEBUG=true
92+
ports:
93+
- "7068:7068"
94+
tmpfs:
95+
- /tmp
96+
cpu_shares: "${CPU_SHARES_LOW}"
8197
capture:
8298
container_name: capture
8399
build:

lib/logger.js

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

sndev

+5
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ sndev__logs() {
139139
docker__compose logs "$@"
140140
}
141141

142+
sndev__logview() {
143+
docker__compose exec -it logpipe bash /app/scripts/stream.sh
144+
}
145+
142146
sndev__help_logs() {
143147
help="
144148
get logs from sndev env
@@ -593,6 +597,7 @@ COMMANDS
593597
restart restart env
594598
status status of env
595599
logs logs from env
600+
logview stream logs from the app
596601
delete delete env
597602
598603
sn:

0 commit comments

Comments
 (0)