logger.js
2.94 KB
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
import { serializeLine, serializePrettyLine } from './serialize.js'
/**
* @typedef {import('./context.js').RequestContext} RequestContext
*/
/**
* @typedef {Object} Logger
* @property {(msg: string, fields?: Object) => void} debug
* @property {(msg: string, fields?: Object) => void} info
* @property {(msg: string, fields?: Object) => void} warn
* @property {(msg: string, fields?: Object) => void} error fields 可含 `err`,会被结构化序列化
* @property {(ctx: RequestContext) => Logger} child 派生自动带 trace_id + ctx.fields 的 logger
*/
/** level 名 → 数值,用于过滤。 */
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 }
/**
* 默认 sink:写一行到 stdout。不在 import 期执行,仅作为函数默认参数被引用。
* @param {string} line
*/
function defaultSink(line) {
process.stdout.write(line + '\n')
}
/**
* 结构化日志工厂。`service` 名固定写入每条日志。
*
* @param {Object} cfg
* @param {string} cfg.service 服务/包名,如 'drama-service'
* @param {('debug'|'info'|'warn'|'error')} [cfg.level='info']
* @param {('json'|'pretty')} [cfg.format='json']
* @param {(line: string) => void} [cfg.sink] 输出目标,默认写 stdout;测试可注入 buffer
* @returns {Logger}
*/
export function createLogger(cfg) {
const service = cfg && cfg.service
const level = (cfg && cfg.level) || 'info'
const format = (cfg && cfg.format) || 'json'
const sink = (cfg && cfg.sink) || defaultSink
const threshold = LEVELS[level] ?? LEVELS.info
// bound:随 child(ctx) 绑定的 trace_id + ctx.fields;根 logger 无绑定字段。
return makeLogger({ service, threshold, format, sink, bound: null })
}
/**
* @param {{ service: string, threshold: number, format: 'json'|'pretty', sink: (line:string)=>void, bound: { trace_id: string, fields: Object } | null }} state
* @returns {Logger}
*/
function makeLogger(state) {
/**
* @param {('debug'|'info'|'warn'|'error')} level
* @param {string} msg
* @param {Object} [fields]
*/
function emit(level, msg, fields) {
if (LEVELS[level] < state.threshold) return // level 过滤
const record = {
time: new Date().toISOString(),
level,
service: state.service,
trace_id: state.bound ? state.bound.trace_id : undefined,
msg,
...(state.bound ? state.bound.fields : null),
...(fields || null),
}
state.sink(state.format === 'pretty' ? serializePrettyLine(record) : serializeLine(record))
}
return {
debug: (msg, fields) => emit('debug', msg, fields),
info: (msg, fields) => emit('info', msg, fields),
warn: (msg, fields) => emit('warn', msg, fields),
error: (msg, fields) => emit('error', msg, fields),
child(ctx) {
// 绑定 ctx 的 trace_id 与累积字段,派生新 logger;不修改当前 logger。
return makeLogger({
...state,
bound: { trace_id: ctx.traceId, fields: ctx.fields || {} },
})
},
}
}