jsonl-logger 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -1
- package/dist/google-cloud-logging.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +7 -3
- package/dist/intercept.js +7 -3
- package/dist/preload.js +7 -3
- package/dist/types.d.ts +16 -5
- package/dist/victoria-logs.js +1 -1
- package/package.json +2 -2
- package/src/google-cloud-logging.ts +7 -4
- package/src/index.ts +50 -5
- package/src/intercept.ts +22 -7
- package/src/types.ts +28 -1
- package/src/victoria-logs.ts +6 -4
package/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/jsonl-logger)
|
|
2
|
+
[](https://www.npmjs.com/package/jsonl-logger)
|
|
3
|
+
[](https://github.com/annexare/jsonl-logger/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
# JSONL Logger
|
|
2
7
|
|
|
3
8
|
Lightweight JSON Lines logger with pluggable formatters (**Google Cloud Logging**, **VictoriaLogs**). Modern **ESM**-only, zero dependencies, Bun-first, works on Node.js and Deno.
|
|
@@ -78,6 +83,58 @@ console.log('plain text') // → structured JSON
|
|
|
78
83
|
originalConsole.log('bypass interception')
|
|
79
84
|
```
|
|
80
85
|
|
|
86
|
+
## OpenTelemetry
|
|
87
|
+
|
|
88
|
+
The logger supports automatic trace context injection. Supply a `traceContext` getter that returns the active span's trace/span IDs — the formatter maps them to platform-specific fields automatically.
|
|
89
|
+
|
|
90
|
+
### With `@opentelemetry/api`
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { trace } from '@opentelemetry/api'
|
|
94
|
+
import { Logger } from 'jsonl-logger'
|
|
95
|
+
|
|
96
|
+
const logger = new Logger({}, {
|
|
97
|
+
traceContext: () => {
|
|
98
|
+
const span = trace.getActiveSpan()
|
|
99
|
+
if (!span) return undefined
|
|
100
|
+
const { traceId, spanId, traceFlags } = span.spanContext()
|
|
101
|
+
return { traceId, spanId, traceFlags }
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
logger.info('request handled', { path: '/api' })
|
|
106
|
+
// GCL output includes "logging.googleapis.com/trace", "logging.googleapis.com/spanId", etc.
|
|
107
|
+
// VictoriaLogs output includes "trace_id", "span_id", etc.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Custom trace context
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const logger = new Logger({}, {
|
|
114
|
+
traceContext: () => ({
|
|
115
|
+
traceId: myTracer.currentTraceId(),
|
|
116
|
+
spanId: myTracer.currentSpanId(),
|
|
117
|
+
}),
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The `traceContext` option is also available on `intercept()`:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { intercept } from 'jsonl-logger/intercept'
|
|
125
|
+
|
|
126
|
+
intercept({
|
|
127
|
+
traceContext: () => {
|
|
128
|
+
const span = trace.getActiveSpan()
|
|
129
|
+
if (!span) return undefined
|
|
130
|
+
const { traceId, spanId, traceFlags } = span.spanContext()
|
|
131
|
+
return { traceId, spanId, traceFlags }
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Child loggers inherit the `traceContext` getter from their parent.
|
|
137
|
+
|
|
81
138
|
## Next.js Integration
|
|
82
139
|
|
|
83
140
|
The preload module reads `LOG_FORMAT` and only activates when it's set. Safe to include unconditionally — it's a no-op without `LOG_FORMAT`.
|
|
@@ -122,6 +179,49 @@ requestLogger.info('Processing request')
|
|
|
122
179
|
// All entries include requestId and service
|
|
123
180
|
```
|
|
124
181
|
|
|
182
|
+
## Error Handling
|
|
183
|
+
|
|
184
|
+
Errors passed to `error()` / `fatal()` capture the full stack trace and `error.cause` chain:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const inner = new Error('ECONNREFUSED')
|
|
188
|
+
const outer = new Error('fetch failed', { cause: inner })
|
|
189
|
+
logger.error('API call failed', { endpoint: '/users' }, outer)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Dev mode** (no `LOG_FORMAT`) — colored plain text with full stack:
|
|
193
|
+
```
|
|
194
|
+
18:42:05 ERROR API call failed {"endpoint":"/users"}
|
|
195
|
+
Error: fetch failed
|
|
196
|
+
at handler (/app/api/route.ts:42:5)
|
|
197
|
+
Caused by: Error: ECONNREFUSED
|
|
198
|
+
at connect (/app/db.ts:10:3)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Production** (`LOG_FORMAT` set) — structured JSON with `error.*` and `error.cause.*` fields:
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"message": "API call failed",
|
|
205
|
+
"severity": "ERROR",
|
|
206
|
+
"endpoint": "/users",
|
|
207
|
+
"error.name": "Error",
|
|
208
|
+
"error.message": "fetch failed",
|
|
209
|
+
"error.stack": "Error: fetch failed\n at handler ...",
|
|
210
|
+
"error.cause.name": "Error",
|
|
211
|
+
"error.cause.message": "ECONNREFUSED",
|
|
212
|
+
"error.cause.stack": "Error: ECONNREFUSED\n at connect ..."
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The `errorInfo()` helper is exported for use in custom formatters:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { errorInfo } from 'jsonl-logger'
|
|
220
|
+
|
|
221
|
+
const info = errorInfo(caughtError)
|
|
222
|
+
// { name, message, stack, cause?: { name, message, stack, cause?: ... } }
|
|
223
|
+
```
|
|
224
|
+
|
|
125
225
|
## Environment Variables
|
|
126
226
|
|
|
127
227
|
| Variable | Default | Description |
|
|
@@ -140,7 +240,7 @@ The logger auto-detects the runtime and uses the fastest available I/O:
|
|
|
140
240
|
|
|
141
241
|
| Subpath | Export |
|
|
142
242
|
|---------|--------|
|
|
143
|
-
| `jsonl-logger` | `Logger`, `logger`, types |
|
|
243
|
+
| `jsonl-logger` | `Logger`, `logger`, `errorInfo()`, types (`ErrorInfo`, `LogRecord`, `TraceContext`, etc.) |
|
|
144
244
|
| `jsonl-logger/google-cloud-logging` | `GoogleCloudLogging` formatter |
|
|
145
245
|
| `jsonl-logger/victoria-logs` | `VictoriaLogs` formatter |
|
|
146
246
|
| `jsonl-logger/intercept` | `intercept()`, `originalConsole` |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var g={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},a={messageKey:"message",format(e){let t={message:e.message,timestamp:e.timestamp,severity:g[e.level],...e.context};if(e.trace){if(t["logging.googleapis.com/trace"]=e.trace.traceId,t["logging.googleapis.com/spanId"]=e.trace.spanId,e.trace.traceFlags!==void 0)t["logging.googleapis.com/trace_sampled"]=(e.trace.traceFlags&1)===1}return t}};export{a as GoogleCloudLogging};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { LogContext, LoggerOptions } from './types';
|
|
2
|
-
export type { Formatter, FormatterName, InterceptOptions, LogContext, LoggerOptions, LogLevel, LogRecord, } from './types';
|
|
1
|
+
import type { ErrorInfo, LogContext, LoggerOptions } from './types';
|
|
2
|
+
export type { ErrorInfo, Formatter, FormatterName, InterceptOptions, LogContext, LoggerOptions, LogLevel, LogRecord, TraceContext, } from './types';
|
|
3
3
|
export { logLevelValues, stripAnsi } from './types';
|
|
4
|
+
export declare function errorInfo(err: Error): ErrorInfo;
|
|
4
5
|
export declare class Logger {
|
|
5
6
|
private ctx;
|
|
6
7
|
private min;
|
|
7
8
|
private json;
|
|
8
9
|
private fmt;
|
|
10
|
+
private tc?;
|
|
9
11
|
constructor(context?: LogContext, options?: LoggerOptions);
|
|
10
12
|
child(context: LogContext): Logger;
|
|
11
13
|
private log;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
`);else if(
|
|
3
|
-
`);if(
|
|
1
|
+
var k={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},f={messageKey:"message",format(t){let o={message:t.message,timestamp:t.timestamp,severity:k[t.level],...t.context};if(t.trace){if(o["logging.googleapis.com/trace"]=t.trace.traceId,o["logging.googleapis.com/spanId"]=t.trace.spanId,t.trace.traceFlags!==void 0)o["logging.googleapis.com/trace_sampled"]=(t.trace.traceFlags&1)===1}return o}};var d={messageKey:"_msg",format(t){let o={_msg:t.message,_time:t.timestamp,level:t.level,...t.context};if(t.trace){if(o.trace_id=t.trace.traceId,o.span_id=t.trace.spanId,t.trace.traceFlags!==void 0)o.trace_flags=t.trace.traceFlags}return o}};var i=process.env.LOG_FORMAT,C=!!i,a={debug:0,info:1,warn:2,error:3,fatal:4},$=/\x1b\[[0-9;]*m/g;function u(t){return t.replace($,"")}function p(t,o,e="error"){if(t[`${e}.name`]=o.name,t[`${e}.message`]=o.message,o.stack)t[`${e}.stack`]=o.stack;if(o.cause)p(t,o.cause,`${e}.cause`)}var L=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",x=L==="deno"?new TextEncoder:null;function R(t,o){if(L==="node")(o?process.stderr??process.stdout:process.stdout).write(`${t}
|
|
2
|
+
`);else if(L==="deno"&&x){let e=x.encode(`${t}
|
|
3
|
+
`);if(o)Deno.stderr.writeSync(e);else Deno.stdout.writeSync(e)}else if(o)console.error(t);else console.log(t)}var E={"google-cloud-logging":f,"victoria-logs":d},O=i&&E[i]||f,w=C,S=process.env.LOG_LEVEL||(w?"info":"debug"),y={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function b(t,o){return o.add(t),{name:t.name,message:t.message,stack:t.stack,...t.cause instanceof Error&&!o.has(t.cause)?{cause:b(t.cause,o)}:{}}}function j(t){return b(t,new WeakSet)}class h{ctx;min;json;fmt;tc;constructor(t,o){this.ctx=t||{},this.json=o?.json??w,this.fmt=o?.formatter??O,this.tc=o?.traceContext;let e=o?.level??S;this.min=a[e]??a.info}child(t){let o=new h({...this.ctx,...t},{json:this.json,formatter:this.fmt,traceContext:this.tc});return o.min=this.min,o}log(t,o,e,c){if(a[t]<this.min)return;let s={level:t,message:this.json?u(o).trim():o,timestamp:new Date().toISOString(),context:e?{...this.ctx,...e}:this.ctx};if(this.tc)s.trace=this.tc();if(c)s.error=j(c);if(this.json){let g=this.fmt.format(s);if(s.error)p(g,s.error);R(JSON.stringify(g),y[t])}else this.logPlain(t,s)}logPlain(t,o){let e={debug:"\x1B[36m",info:"\x1B[32m",warn:"\x1B[33m",error:"\x1B[31m",fatal:"\x1B[35m"},c="\x1B[0m",s=e[t],g=new Date(o.timestamp).toLocaleTimeString("en-US",{hour12:!1}),I=t.toUpperCase().padEnd(5),v=o.context,F=Object.keys(v).length>0?` ${JSON.stringify(v)}`:"",m="";if(o.error){let n=o.error,l=!0;while(n){if(n.stack)m+=l?`
|
|
4
|
+
${n.stack}`:`
|
|
5
|
+
Caused by: ${n.stack}`;else m+=l?`
|
|
6
|
+
${n.name}: ${n.message}`:`
|
|
7
|
+
Caused by: ${n.name}: ${n.message}`;n=n.cause,l=!1}}let r=`${s}${g} ${I}\x1B[0m ${o.message}${F}${m}`;switch(t){case"debug":console.debug(r);break;case"warn":console.warn(r);break;case"error":case"fatal":console.error(r);break;default:console.log(r)}}debug(t,o){this.log("debug",t,o)}info(t,o){this.log("info",t,o)}warn(t,o){this.log("warn",t,o)}error(t,o,e){this.log("error",t,o,e)}fatal(t,o,e){this.log("fatal",t,o,e)}}var J=new h;export{u as stripAnsi,J as logger,a as logLevelValues,j as errorInfo,h as Logger};
|
package/dist/intercept.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
`);else if(
|
|
3
|
-
`);if(
|
|
1
|
+
var j={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},u={messageKey:"message",format(t){let o={message:t.message,timestamp:t.timestamp,severity:j[t.level],...t.context};if(t.trace){if(o["logging.googleapis.com/trace"]=t.trace.traceId,o["logging.googleapis.com/spanId"]=t.trace.spanId,t.trace.traceFlags!==void 0)o["logging.googleapis.com/trace_sampled"]=(t.trace.traceFlags&1)===1}return o}};var w={messageKey:"_msg",format(t){let o={_msg:t.message,_time:t.timestamp,level:t.level,...t.context};if(t.trace){if(o.trace_id=t.trace.traceId,o.span_id=t.trace.spanId,t.trace.traceFlags!==void 0)o.trace_flags=t.trace.traceFlags}return o}};var x=process.env.LOG_FORMAT,$=!!x,m={debug:0,info:1,warn:2,error:3,fatal:4},_=/\x1b\[[0-9;]*m/g;function h(t){return t.replace(_,"")}function p(t,o,n="error"){if(t[`${n}.name`]=o.name,t[`${n}.message`]=o.message,o.stack)t[`${n}.stack`]=o.stack;if(o.cause)p(t,o.cause,`${n}.cause`)}var b=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",k=b==="deno"?new TextEncoder:null;function l(t,o){if(b==="node")(o?process.stderr??process.stdout:process.stdout).write(`${t}
|
|
2
|
+
`);else if(b==="deno"&&k){let n=k.encode(`${t}
|
|
3
|
+
`);if(o)Deno.stderr.writeSync(n);else Deno.stdout.writeSync(n)}else if(o)console.error(t);else console.log(t)}var E={"google-cloud-logging":u,"victoria-logs":w},N=x&&E[x]||u,v=$,T=process.env.LOG_LEVEL||(v?"info":"debug"),y={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function d(t,o){return o.add(t),{name:t.name,message:t.message,stack:t.stack,...t.cause instanceof Error&&!o.has(t.cause)?{cause:d(t.cause,o)}:{}}}function F(t){return d(t,new WeakSet)}class I{ctx;min;json;fmt;tc;constructor(t,o){this.ctx=t||{},this.json=o?.json??v,this.fmt=o?.formatter??N,this.tc=o?.traceContext;let n=o?.level??T;this.min=m[n]??m.info}child(t){let o=new I({...this.ctx,...t},{json:this.json,formatter:this.fmt,traceContext:this.tc});return o.min=this.min,o}log(t,o,n,e){if(m[t]<this.min)return;let c={level:t,message:this.json?h(o).trim():o,timestamp:new Date().toISOString(),context:n?{...this.ctx,...n}:this.ctx};if(this.tc)c.trace=this.tc();if(e)c.error=F(e);if(this.json){let a=this.fmt.format(c);if(c.error)p(a,c.error);l(JSON.stringify(a),y[t])}else this.logPlain(t,c)}logPlain(t,o){let n={debug:"\x1B[36m",info:"\x1B[32m",warn:"\x1B[33m",error:"\x1B[31m",fatal:"\x1B[35m"},e="\x1B[0m",c=n[t],a=new Date(o.timestamp).toLocaleTimeString("en-US",{hour12:!1}),i=t.toUpperCase().padEnd(5),L=o.context,R=Object.keys(L).length>0?` ${JSON.stringify(L)}`:"",f="";if(o.error){let s=o.error,C=!0;while(s){if(s.stack)f+=C?`
|
|
4
|
+
${s.stack}`:`
|
|
5
|
+
Caused by: ${s.stack}`;else f+=C?`
|
|
6
|
+
${s.name}: ${s.message}`:`
|
|
7
|
+
Caused by: ${s.name}: ${s.message}`;s=s.cause,C=!1}}let g=`${c}${a} ${i}\x1B[0m ${o.message}${R}${f}`;switch(t){case"debug":console.debug(g);break;case"warn":console.warn(g);break;case"error":case"fatal":console.error(g);break;default:console.log(g)}}debug(t,o){this.log("debug",t,o)}info(t,o){this.log("info",t,o)}warn(t,o){this.log("warn",t,o)}error(t,o,n){this.log("error",t,o,n)}fatal(t,o,n){this.log("fatal",t,o,n)}}var H=new I;var D={log:console.log.bind(console),info:console.info.bind(console),warn:console.warn.bind(console),error:console.error.bind(console),debug:console.debug.bind(console)};function G(...t){let o="";for(let n=0;n<t.length;n++){if(n>0)o+=" ";let e=t[n];if(typeof e==="string")o+=h(e);else if(e instanceof Error)o+=e.message;else try{o+=JSON.stringify(e)}catch{o+=String(e)}}return o.trim()}function A(...t){let o;for(let n of t)if(typeof n==="object"&&n!==null&&!(n instanceof Error)&&!Array.isArray(n)){if(!o)o={};Object.assign(o,n)}return o}function J(...t){for(let o of t)if(o instanceof Error)return o}var O={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function U(t,o,n,e,c){let a=`"${o.messageKey}"`;return(...i)=>{if(i.length===1&&typeof i[0]==="string"&&i[0].charCodeAt(0)===123&&i[0].includes(a)){l(i[0],O[t]);return}if(m[t]<n)return;let L=G(...i);if(e&&!e(t,L))return;let R=A(...i),f=J(...i),g={level:t,message:L,timestamp:new Date().toISOString(),context:R||{},error:f?F(f):void 0,trace:c?.()},s=o.format(g);if(g.error)p(s,g.error);l(JSON.stringify(s),O[t])}}var S="__jsonlLoggerIntercepted";function K(t){if(globalThis[S])return;globalThis[S]=!0;let o=t?.formatter??u,n=m[t?.level??"debug"],e=t?.filter,c=t?.traceContext,a=[["log","info"],["info","info"],["warn","warn"],["error","error"],["debug","debug"]];for(let[i,L]of a)console[i]=U(L,o,n,e,c)}export{D as originalConsole,K as intercept};
|
package/dist/preload.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
`);else if(
|
|
3
|
-
`);if(
|
|
1
|
+
var N={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},g={messageKey:"message",format(t){let o={message:t.message,timestamp:t.timestamp,severity:N[t.level],...t.context};if(t.trace){if(o["logging.googleapis.com/trace"]=t.trace.traceId,o["logging.googleapis.com/spanId"]=t.trace.spanId,t.trace.traceFlags!==void 0)o["logging.googleapis.com/trace_sampled"]=(t.trace.traceFlags&1)===1}return o}};var x={messageKey:"_msg",format(t){let o={_msg:t.message,_time:t.timestamp,level:t.level,...t.context};if(t.trace){if(o.trace_id=t.trace.traceId,o.span_id=t.trace.spanId,t.trace.traceFlags!==void 0)o.trace_flags=t.trace.traceFlags}return o}};var l=process.env.LOG_FORMAT,d=!!l,f={debug:0,info:1,warn:2,error:3,fatal:4},S=/\x1b\[[0-9;]*m/g;function r(t){return t.replace(S,"")}function u(t,o,n="error"){if(t[`${n}.name`]=o.name,t[`${n}.message`]=o.message,o.stack)t[`${n}.stack`]=o.stack;if(o.cause)u(t,o.cause,`${n}.cause`)}var C=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",I=C==="deno"?new TextEncoder:null;function h(t,o){if(C==="node")(o?process.stderr??process.stdout:process.stdout).write(`${t}
|
|
2
|
+
`);else if(C==="deno"&&I){let n=I.encode(`${t}
|
|
3
|
+
`);if(o)Deno.stderr.writeSync(n);else Deno.stdout.writeSync(n)}else if(o)console.error(t);else console.log(t)}var _={"google-cloud-logging":g,"victoria-logs":x},j=l&&_[l]||g,v=d,y=process.env.LOG_LEVEL||(v?"info":"debug"),G={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function k(t,o){return o.add(t),{name:t.name,message:t.message,stack:t.stack,...t.cause instanceof Error&&!o.has(t.cause)?{cause:k(t.cause,o)}:{}}}function b(t){return k(t,new WeakSet)}class w{ctx;min;json;fmt;tc;constructor(t,o){this.ctx=t||{},this.json=o?.json??v,this.fmt=o?.formatter??j,this.tc=o?.traceContext;let n=o?.level??y;this.min=f[n]??f.info}child(t){let o=new w({...this.ctx,...t},{json:this.json,formatter:this.fmt,traceContext:this.tc});return o.min=this.min,o}log(t,o,n,e){if(f[t]<this.min)return;let i={level:t,message:this.json?r(o).trim():o,timestamp:new Date().toISOString(),context:n?{...this.ctx,...n}:this.ctx};if(this.tc)i.trace=this.tc();if(e)i.error=b(e);if(this.json){let a=this.fmt.format(i);if(i.error)u(a,i.error);h(JSON.stringify(a),G[t])}else this.logPlain(t,i)}logPlain(t,o){let n={debug:"\x1B[36m",info:"\x1B[32m",warn:"\x1B[33m",error:"\x1B[31m",fatal:"\x1B[35m"},e="\x1B[0m",i=n[t],a=new Date(o.timestamp).toLocaleTimeString("en-US",{hour12:!1}),c=t.toUpperCase().padEnd(5),L=o.context,R=Object.keys(L).length>0?` ${JSON.stringify(L)}`:"",p="";if(o.error){let s=o.error,F=!0;while(s){if(s.stack)p+=F?`
|
|
4
|
+
${s.stack}`:`
|
|
5
|
+
Caused by: ${s.stack}`;else p+=F?`
|
|
6
|
+
${s.name}: ${s.message}`:`
|
|
7
|
+
Caused by: ${s.name}: ${s.message}`;s=s.cause,F=!1}}let m=`${i}${a} ${c}\x1B[0m ${o.message}${R}${p}`;switch(t){case"debug":console.debug(m);break;case"warn":console.warn(m);break;case"error":case"fatal":console.error(m);break;default:console.log(m)}}debug(t,o){this.log("debug",t,o)}info(t,o){this.log("info",t,o)}warn(t,o){this.log("warn",t,o)}error(t,o,n){this.log("error",t,o,n)}fatal(t,o,n){this.log("fatal",t,o,n)}}var P=new w;var K={log:console.log.bind(console),info:console.info.bind(console),warn:console.warn.bind(console),error:console.error.bind(console),debug:console.debug.bind(console)};function T(...t){let o="";for(let n=0;n<t.length;n++){if(n>0)o+=" ";let e=t[n];if(typeof e==="string")o+=r(e);else if(e instanceof Error)o+=e.message;else try{o+=JSON.stringify(e)}catch{o+=String(e)}}return o.trim()}function V(...t){let o;for(let n of t)if(typeof n==="object"&&n!==null&&!(n instanceof Error)&&!Array.isArray(n)){if(!o)o={};Object.assign(o,n)}return o}function A(...t){for(let o of t)if(o instanceof Error)return o}var $={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function J(t,o,n,e,i){let a=`"${o.messageKey}"`;return(...c)=>{if(c.length===1&&typeof c[0]==="string"&&c[0].charCodeAt(0)===123&&c[0].includes(a)){h(c[0],$[t]);return}if(f[t]<n)return;let L=T(...c);if(e&&!e(t,L))return;let R=V(...c),p=A(...c),m={level:t,message:L,timestamp:new Date().toISOString(),context:R||{},error:p?b(p):void 0,trace:i?.()},s=o.format(m);if(m.error)u(s,m.error);h(JSON.stringify(s),$[t])}}var O="__jsonlLoggerIntercepted";function E(t){if(globalThis[O])return;globalThis[O]=!0;let o=t?.formatter??g,n=f[t?.level??"debug"],e=t?.filter,i=t?.traceContext,a=[["log","info"],["info","info"],["warn","warn"],["error","error"],["debug","debug"]];for(let[c,L]of a)console[c]=J(L,o,n,e,i)}if(l){let o={"google-cloud-logging":g,"victoria-logs":x}[l]??g,n=process.env.LOG_LEVEL||"info";E({formatter:o,level:n})}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
2
2
|
export type LogContext = Record<string, unknown>;
|
|
3
|
+
export type ErrorInfo = {
|
|
4
|
+
name: string;
|
|
5
|
+
message: string;
|
|
6
|
+
stack?: string;
|
|
7
|
+
cause?: ErrorInfo;
|
|
8
|
+
};
|
|
9
|
+
export type TraceContext = {
|
|
10
|
+
traceId: string;
|
|
11
|
+
spanId: string;
|
|
12
|
+
traceFlags?: number;
|
|
13
|
+
};
|
|
3
14
|
export type LogRecord = {
|
|
4
15
|
level: LogLevel;
|
|
5
16
|
message: string;
|
|
6
17
|
timestamp: string;
|
|
7
18
|
context: LogContext;
|
|
8
|
-
error?:
|
|
9
|
-
|
|
10
|
-
message: string;
|
|
11
|
-
stack?: string;
|
|
12
|
-
};
|
|
19
|
+
error?: ErrorInfo;
|
|
20
|
+
trace?: TraceContext;
|
|
13
21
|
};
|
|
14
22
|
export type Formatter = {
|
|
15
23
|
format: (record: LogRecord) => Record<string, unknown>;
|
|
@@ -19,6 +27,7 @@ export type LoggerOptions = {
|
|
|
19
27
|
formatter?: Formatter;
|
|
20
28
|
json?: boolean;
|
|
21
29
|
level?: LogLevel;
|
|
30
|
+
traceContext?: () => TraceContext | undefined;
|
|
22
31
|
};
|
|
23
32
|
export type FormatterName = 'google-cloud-logging' | 'victoria-logs';
|
|
24
33
|
export declare const defaultFormat: FormatterName | undefined;
|
|
@@ -27,7 +36,9 @@ export type InterceptOptions = {
|
|
|
27
36
|
formatter?: Formatter;
|
|
28
37
|
filter?: (level: LogLevel, message: string) => boolean;
|
|
29
38
|
level?: LogLevel;
|
|
39
|
+
traceContext?: () => TraceContext | undefined;
|
|
30
40
|
};
|
|
31
41
|
export declare const logLevelValues: Record<LogLevel, number>;
|
|
32
42
|
export declare function stripAnsi(str: string): string;
|
|
43
|
+
export declare function flattenError(entry: Record<string, unknown>, error: ErrorInfo, prefix?: string): void;
|
|
33
44
|
export declare function write(data: string, isError: boolean): void;
|
package/dist/victoria-logs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var a={messageKey:"_msg",format(t){let e={_msg:t.message,_time:t.timestamp,level:t.level,...t.context};if(t.trace){if(e.trace_id=t.trace.traceId,e.span_id=t.trace.spanId,t.trace.traceFlags!==void 0)e.trace_flags=t.trace.traceFlags}return e}};export{a as VictoriaLogs};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsonl-logger",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Lightweight ESM-only JSON Lines logger with pluggable formatters for Google Cloud Logging, VictoriaLogs, and more",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"test": "bun test"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@biomejs/biome": "2.4.
|
|
52
|
+
"@biomejs/biome": "2.4.3",
|
|
53
53
|
"@types/bun": "1.3.9",
|
|
54
54
|
"typescript": "5.9.3"
|
|
55
55
|
},
|
|
@@ -17,10 +17,13 @@ export const GoogleCloudLogging: Formatter = {
|
|
|
17
17
|
severity: severityMap[record.level],
|
|
18
18
|
...record.context,
|
|
19
19
|
}
|
|
20
|
-
if (record.
|
|
21
|
-
entry['
|
|
22
|
-
entry['
|
|
23
|
-
if (record.
|
|
20
|
+
if (record.trace) {
|
|
21
|
+
entry['logging.googleapis.com/trace'] = record.trace.traceId
|
|
22
|
+
entry['logging.googleapis.com/spanId'] = record.trace.spanId
|
|
23
|
+
if (record.trace.traceFlags !== undefined) {
|
|
24
|
+
entry['logging.googleapis.com/trace_sampled'] =
|
|
25
|
+
(record.trace.traceFlags & 1) === 1
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
return entry
|
|
26
29
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { GoogleCloudLogging } from './google-cloud-logging'
|
|
2
2
|
import type {
|
|
3
|
+
ErrorInfo,
|
|
3
4
|
Formatter,
|
|
4
5
|
FormatterName,
|
|
5
6
|
LogContext,
|
|
6
7
|
LoggerOptions,
|
|
7
8
|
LogLevel,
|
|
8
9
|
LogRecord,
|
|
10
|
+
TraceContext,
|
|
9
11
|
} from './types'
|
|
10
12
|
import {
|
|
11
13
|
defaultFormat,
|
|
14
|
+
flattenError,
|
|
12
15
|
isJsonMode,
|
|
13
16
|
logLevelValues,
|
|
14
17
|
stripAnsi,
|
|
@@ -17,6 +20,7 @@ import {
|
|
|
17
20
|
import { VictoriaLogs } from './victoria-logs'
|
|
18
21
|
|
|
19
22
|
export type {
|
|
23
|
+
ErrorInfo,
|
|
20
24
|
Formatter,
|
|
21
25
|
FormatterName,
|
|
22
26
|
InterceptOptions,
|
|
@@ -24,6 +28,7 @@ export type {
|
|
|
24
28
|
LoggerOptions,
|
|
25
29
|
LogLevel,
|
|
26
30
|
LogRecord,
|
|
31
|
+
TraceContext,
|
|
27
32
|
} from './types'
|
|
28
33
|
export { logLevelValues, stripAnsi } from './types'
|
|
29
34
|
|
|
@@ -47,16 +52,34 @@ const isErrorLevel: Record<LogLevel, boolean> = {
|
|
|
47
52
|
fatal: true,
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
function extractErrorInfo(err: Error, visited: WeakSet<Error>): ErrorInfo {
|
|
56
|
+
visited.add(err)
|
|
57
|
+
return {
|
|
58
|
+
name: err.name,
|
|
59
|
+
message: err.message,
|
|
60
|
+
stack: err.stack,
|
|
61
|
+
...(err.cause instanceof Error && !visited.has(err.cause)
|
|
62
|
+
? { cause: extractErrorInfo(err.cause, visited) }
|
|
63
|
+
: {}),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function errorInfo(err: Error): ErrorInfo {
|
|
68
|
+
return extractErrorInfo(err, new WeakSet())
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
export class Logger {
|
|
51
72
|
private ctx: LogContext
|
|
52
73
|
private min: number
|
|
53
74
|
private json: boolean
|
|
54
75
|
private fmt: Formatter
|
|
76
|
+
private tc?: () => TraceContext | undefined
|
|
55
77
|
|
|
56
78
|
constructor(context?: LogContext, options?: LoggerOptions) {
|
|
57
79
|
this.ctx = context || {}
|
|
58
80
|
this.json = options?.json ?? defaultJson
|
|
59
81
|
this.fmt = options?.formatter ?? defaultFormatter
|
|
82
|
+
this.tc = options?.traceContext
|
|
60
83
|
const level: LogLevel = options?.level ?? defaultLevel
|
|
61
84
|
this.min = logLevelValues[level] ?? logLevelValues.info
|
|
62
85
|
}
|
|
@@ -67,6 +90,7 @@ export class Logger {
|
|
|
67
90
|
{
|
|
68
91
|
json: this.json,
|
|
69
92
|
formatter: this.fmt,
|
|
93
|
+
traceContext: this.tc,
|
|
70
94
|
},
|
|
71
95
|
)
|
|
72
96
|
child.min = this.min
|
|
@@ -88,12 +112,18 @@ export class Logger {
|
|
|
88
112
|
context: meta ? { ...this.ctx, ...meta } : this.ctx,
|
|
89
113
|
}
|
|
90
114
|
|
|
115
|
+
if (this.tc) {
|
|
116
|
+
record.trace = this.tc()
|
|
117
|
+
}
|
|
118
|
+
|
|
91
119
|
if (err) {
|
|
92
|
-
record.error =
|
|
120
|
+
record.error = errorInfo(err)
|
|
93
121
|
}
|
|
94
122
|
|
|
95
123
|
if (this.json) {
|
|
96
|
-
|
|
124
|
+
const formatted = this.fmt.format(record)
|
|
125
|
+
if (record.error) flattenError(formatted, record.error)
|
|
126
|
+
write(JSON.stringify(formatted), isErrorLevel[level])
|
|
97
127
|
} else {
|
|
98
128
|
this.logPlain(level, record)
|
|
99
129
|
}
|
|
@@ -118,9 +148,24 @@ export class Logger {
|
|
|
118
148
|
const ctx = record.context
|
|
119
149
|
const metaStr = Object.keys(ctx).length > 0 ? ` ${JSON.stringify(ctx)}` : ''
|
|
120
150
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
:
|
|
151
|
+
let errStr = ''
|
|
152
|
+
if (record.error) {
|
|
153
|
+
let current: ErrorInfo | undefined = record.error
|
|
154
|
+
let isRoot = true
|
|
155
|
+
while (current) {
|
|
156
|
+
if (current.stack) {
|
|
157
|
+
errStr += isRoot
|
|
158
|
+
? `\n${current.stack}`
|
|
159
|
+
: `\nCaused by: ${current.stack}`
|
|
160
|
+
} else {
|
|
161
|
+
errStr += isRoot
|
|
162
|
+
? `\n ${current.name}: ${current.message}`
|
|
163
|
+
: `\nCaused by: ${current.name}: ${current.message}`
|
|
164
|
+
}
|
|
165
|
+
current = current.cause
|
|
166
|
+
isRoot = false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
124
169
|
|
|
125
170
|
const output = `${color}${time} ${levelStr}${reset} ${record.message}${metaStr}${errStr}`
|
|
126
171
|
|
package/src/intercept.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { GoogleCloudLogging } from './google-cloud-logging'
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import { errorInfo } from './index'
|
|
3
|
+
import type {
|
|
4
|
+
Formatter,
|
|
5
|
+
InterceptOptions,
|
|
6
|
+
LogLevel,
|
|
7
|
+
TraceContext,
|
|
8
|
+
} from './types'
|
|
9
|
+
import { flattenError, logLevelValues, stripAnsi, write } from './types'
|
|
4
10
|
|
|
5
11
|
type ConsoleMethods = {
|
|
6
12
|
log: typeof console.log
|
|
@@ -74,6 +80,7 @@ function createOverride(
|
|
|
74
80
|
formatter: Formatter,
|
|
75
81
|
minLevel: number,
|
|
76
82
|
filter?: (level: LogLevel, message: string) => boolean,
|
|
83
|
+
traceContext?: () => TraceContext | undefined,
|
|
77
84
|
): (...args: unknown[]) => void {
|
|
78
85
|
const msgKey = `"${formatter.messageKey}"`
|
|
79
86
|
|
|
@@ -103,12 +110,13 @@ function createOverride(
|
|
|
103
110
|
message,
|
|
104
111
|
timestamp: new Date().toISOString(),
|
|
105
112
|
context: meta || {},
|
|
106
|
-
error: error
|
|
107
|
-
|
|
108
|
-
: undefined,
|
|
113
|
+
error: error ? errorInfo(error) : undefined,
|
|
114
|
+
trace: traceContext?.(),
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
|
|
117
|
+
const formatted = formatter.format(record)
|
|
118
|
+
if (record.error) flattenError(formatted, record.error)
|
|
119
|
+
write(JSON.stringify(formatted), isErrorLevel[level])
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
|
|
@@ -121,6 +129,7 @@ export function intercept(options?: InterceptOptions): void {
|
|
|
121
129
|
const formatter = options?.formatter ?? GoogleCloudLogging
|
|
122
130
|
const minLevel = logLevelValues[options?.level ?? 'debug']
|
|
123
131
|
const filter = options?.filter
|
|
132
|
+
const traceContext = options?.traceContext
|
|
124
133
|
|
|
125
134
|
const methodMap: [keyof ConsoleMethods, LogLevel][] = [
|
|
126
135
|
['log', 'info'],
|
|
@@ -131,6 +140,12 @@ export function intercept(options?: InterceptOptions): void {
|
|
|
131
140
|
]
|
|
132
141
|
|
|
133
142
|
for (const [method, level] of methodMap) {
|
|
134
|
-
console[method] = createOverride(
|
|
143
|
+
console[method] = createOverride(
|
|
144
|
+
level,
|
|
145
|
+
formatter,
|
|
146
|
+
minLevel,
|
|
147
|
+
filter,
|
|
148
|
+
traceContext,
|
|
149
|
+
)
|
|
135
150
|
}
|
|
136
151
|
}
|
package/src/types.ts
CHANGED
|
@@ -2,12 +2,26 @@ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'
|
|
|
2
2
|
|
|
3
3
|
export type LogContext = Record<string, unknown>
|
|
4
4
|
|
|
5
|
+
export type ErrorInfo = {
|
|
6
|
+
name: string
|
|
7
|
+
message: string
|
|
8
|
+
stack?: string
|
|
9
|
+
cause?: ErrorInfo
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TraceContext = {
|
|
13
|
+
traceId: string
|
|
14
|
+
spanId: string
|
|
15
|
+
traceFlags?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
export type LogRecord = {
|
|
6
19
|
level: LogLevel
|
|
7
20
|
message: string
|
|
8
21
|
timestamp: string
|
|
9
22
|
context: LogContext
|
|
10
|
-
error?:
|
|
23
|
+
error?: ErrorInfo
|
|
24
|
+
trace?: TraceContext
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
export type Formatter = {
|
|
@@ -19,6 +33,7 @@ export type LoggerOptions = {
|
|
|
19
33
|
formatter?: Formatter
|
|
20
34
|
json?: boolean
|
|
21
35
|
level?: LogLevel
|
|
36
|
+
traceContext?: () => TraceContext | undefined
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
export type FormatterName = 'google-cloud-logging' | 'victoria-logs'
|
|
@@ -30,6 +45,7 @@ export type InterceptOptions = {
|
|
|
30
45
|
formatter?: Formatter
|
|
31
46
|
filter?: (level: LogLevel, message: string) => boolean
|
|
32
47
|
level?: LogLevel
|
|
48
|
+
traceContext?: () => TraceContext | undefined
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
export const logLevelValues: Record<LogLevel, number> = {
|
|
@@ -47,6 +63,17 @@ export function stripAnsi(str: string): string {
|
|
|
47
63
|
return str.replace(ansiPattern, '')
|
|
48
64
|
}
|
|
49
65
|
|
|
66
|
+
export function flattenError(
|
|
67
|
+
entry: Record<string, unknown>,
|
|
68
|
+
error: ErrorInfo,
|
|
69
|
+
prefix = 'error',
|
|
70
|
+
): void {
|
|
71
|
+
entry[`${prefix}.name`] = error.name
|
|
72
|
+
entry[`${prefix}.message`] = error.message
|
|
73
|
+
if (error.stack) entry[`${prefix}.stack`] = error.stack
|
|
74
|
+
if (error.cause) flattenError(entry, error.cause, `${prefix}.cause`)
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
/**
|
|
51
78
|
* Detect runtime once, resolve streams at call time.
|
|
52
79
|
* Stream lookup is deferred so tests/runtime can replace process.stdout.
|
package/src/victoria-logs.ts
CHANGED
|
@@ -9,10 +9,12 @@ export const VictoriaLogs: Formatter = {
|
|
|
9
9
|
level: record.level,
|
|
10
10
|
...record.context,
|
|
11
11
|
}
|
|
12
|
-
if (record.
|
|
13
|
-
entry
|
|
14
|
-
entry
|
|
15
|
-
if (record.
|
|
12
|
+
if (record.trace) {
|
|
13
|
+
entry.trace_id = record.trace.traceId
|
|
14
|
+
entry.span_id = record.trace.spanId
|
|
15
|
+
if (record.trace.traceFlags !== undefined) {
|
|
16
|
+
entry.trace_flags = record.trace.traceFlags
|
|
17
|
+
}
|
|
16
18
|
}
|
|
17
19
|
return entry
|
|
18
20
|
},
|