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 CHANGED
@@ -1,3 +1,8 @@
1
+ [![Monthly Downloads](https://img.shields.io/npm/dm/jsonl-logger.svg)](https://www.npmjs.com/package/jsonl-logger)
2
+ [![NPM](https://img.shields.io/npm/v/jsonl-logger.svg 'NPM package version')](https://www.npmjs.com/package/jsonl-logger)
3
+ [![CI](https://github.com/annexare/jsonl-logger/actions/workflows/ci.yml/badge.svg)](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 t={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},o={messageKey:"message",format(e){let r={message:e.message,timestamp:e.timestamp,severity:t[e.level],...e.context};if(e.error){if(r["error.name"]=e.error.name,r["error.message"]=e.error.message,e.error.stack)r["error.stack"]=e.error.stack}return r}};export{o as GoogleCloudLogging};
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 C={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},a={messageKey:"message",format(o){let t={message:o.message,timestamp:o.timestamp,severity:C[o.level],...o.context};if(o.error){if(t["error.name"]=o.error.name,t["error.message"]=o.error.message,o.error.stack)t["error.stack"]=o.error.stack}return t}};var f={messageKey:"_msg",format(o){let t={_msg:o.message,_time:o.timestamp,level:o.level,...o.context};if(o.error){if(t["error.name"]=o.error.name,t["error.message"]=o.error.message,o.error.stack)t["error.stack"]=o.error.stack}return t}};var i=process.env.LOG_FORMAT,v=!!i,n={debug:0,info:1,warn:2,error:3,fatal:4},k=/\x1b\[[0-9;]*m/g;function l(o){return o.replace(k,"")}var m=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",p=m==="deno"?new TextEncoder:null;function x(o,t){if(m==="node")(t?process.stderr??process.stdout:process.stdout).write(`${o}
2
- `);else if(m==="deno"&&p){let e=p.encode(`${o}
3
- `);if(t)Deno.stderr.writeSync(e);else Deno.stdout.writeSync(e)}else if(t)console.error(o);else console.log(o)}var y={"google-cloud-logging":a,"victoria-logs":f},F=i&&y[i]||a,u=v,O=process.env.LOG_LEVEL||(u?"info":"debug"),d={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};class L{ctx;min;json;fmt;constructor(o,t){this.ctx=o||{},this.json=t?.json??u,this.fmt=t?.formatter??F;let e=t?.level??O;this.min=n[e]??n.info}child(o){let t=new L({...this.ctx,...o},{json:this.json,formatter:this.fmt});return t.min=this.min,t}log(o,t,e,r){if(n[o]<this.min)return;let s={level:o,message:this.json?l(t).trim():t,timestamp:new Date().toISOString(),context:e?{...this.ctx,...e}:this.ctx};if(r)s.error={name:r.name,message:r.message,stack:r.stack};if(this.json)x(JSON.stringify(this.fmt.format(s)),d[o]);else this.logPlain(o,s)}logPlain(o,t){let e={debug:"\x1B[36m",info:"\x1B[32m",warn:"\x1B[33m",error:"\x1B[31m",fatal:"\x1B[35m"},r="\x1B[0m",s=e[o],h=new Date(t.timestamp).toLocaleTimeString("en-US",{hour12:!1}),R=o.toUpperCase().padEnd(5),c=t.context,w=Object.keys(c).length>0?` ${JSON.stringify(c)}`:"",b=t.error?` [${t.error.name}: ${t.error.message}]`:"",g=`${s}${h} ${R}\x1B[0m ${t.message}${w}${b}`;switch(o){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(o,t){this.log("debug",o,t)}info(o,t){this.log("info",o,t)}warn(o,t){this.log("warn",o,t)}error(o,t,e){this.log("error",o,t,e)}fatal(o,t,e){this.log("fatal",o,t,e)}}var V=new L;export{l as stripAnsi,V as logger,n as logLevelValues,L as Logger};
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 Q={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},W={messageKey:"message",format(R){let k={message:R.message,timestamp:R.timestamp,severity:Q[R.level],...R.context};if(R.error){if(k["error.name"]=R.error.name,k["error.message"]=R.error.message,R.error.stack)k["error.stack"]=R.error.stack}return k}};var X=process.env.LOG_FORMAT,L=!!X,T={debug:0,info:1,warn:2,error:3,fatal:4},Y=/\x1b\[[0-9;]*m/g;function q(R){return R.replace(Y,"")}var B=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",j=B==="deno"?new TextEncoder:null;function U(R,k){if(B==="node")(k?process.stderr??process.stdout:process.stdout).write(`${R}
2
- `);else if(B==="deno"&&j){let I=j.encode(`${R}
3
- `);if(k)Deno.stderr.writeSync(I);else Deno.stdout.writeSync(I)}else if(k)console.error(R);else console.log(R)}var S={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 Z(...R){let k="";for(let I=0;I<R.length;I++){if(I>0)k+=" ";let C=R[I];if(typeof C==="string")k+=q(C);else if(C instanceof Error)k+=C.message;else try{k+=JSON.stringify(C)}catch{k+=String(C)}}return k.trim()}function _(...R){let k;for(let I of R)if(typeof I==="object"&&I!==null&&!(I instanceof Error)&&!Array.isArray(I)){if(!k)k={};Object.assign(k,I)}return k}function $(...R){for(let k of R)if(k instanceof Error)return k}var w={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function y(R,k,I,C){let A=`"${k.messageKey}"`;return(...F)=>{if(F.length===1&&typeof F[0]==="string"&&F[0].charCodeAt(0)===123&&F[0].includes(A)){U(F[0],w[R]);return}if(T[R]<I)return;let G=Z(...F);if(C&&!C(R,G))return;let H=_(...F),N=$(...F),O={level:R,message:G,timestamp:new Date().toISOString(),context:H||{},error:N?{name:N.name,message:N.message,stack:N.stack}:void 0};U(JSON.stringify(k.format(O)),w[R])}}var z="__jsonlLoggerIntercepted";function V(R){if(globalThis[z])return;globalThis[z]=!0;let k=R?.formatter??W,I=T[R?.level??"debug"],C=R?.filter,A=[["log","info"],["info","info"],["warn","warn"],["error","error"],["debug","debug"]];for(let[F,G]of A)console[F]=y(G,k,I,C)}export{S as originalConsole,V as intercept};
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 V={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},F={messageKey:"message",format(m){let n={message:m.message,timestamp:m.timestamp,severity:V[m.level],...m.context};if(m.error){if(n["error.name"]=m.error.name,n["error.message"]=m.error.message,m.error.stack)n["error.stack"]=m.error.stack}return n}};var _={messageKey:"_msg",format(m){let n={_msg:m.message,_time:m.timestamp,level:m.level,...m.context};if(m.error){if(n["error.name"]=m.error.name,n["error.message"]=m.error.message,m.error.stack)n["error.stack"]=m.error.stack}return n}};var p=process.env.LOG_FORMAT,Q=!!p,I={debug:0,info:1,warn:2,error:3,fatal:4},W=/\x1b\[[0-9;]*m/g;function C(m){return m.replace(W,"")}var G=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",w=G==="deno"?new TextEncoder:null;function N(m,n){if(G==="node")(n?process.stderr??process.stdout:process.stdout).write(`${m}
2
- `);else if(G==="deno"&&w){let R=w.encode(`${m}
3
- `);if(n)Deno.stderr.writeSync(R);else Deno.stdout.writeSync(R)}else if(n)console.error(m);else console.log(m)}var $={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 c(...m){let n="";for(let R=0;R<m.length;R++){if(R>0)n+=" ";let o=m[R];if(typeof o==="string")n+=C(o);else if(o instanceof Error)n+=o.message;else try{n+=JSON.stringify(o)}catch{n+=String(o)}}return n.trim()}function j(...m){let n;for(let R of m)if(typeof R==="object"&&R!==null&&!(R instanceof Error)&&!Array.isArray(R)){if(!n)n={};Object.assign(n,R)}return n}function q(...m){for(let n of m)if(n instanceof Error)return n}var A={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function t(m,n,R,o){let y=`"${n.messageKey}"`;return(...k)=>{if(k.length===1&&typeof k[0]==="string"&&k[0].charCodeAt(0)===123&&k[0].includes(y)){N(k[0],A[m]);return}if(I[m]<R)return;let f=c(...k);if(o&&!o(m,f))return;let T=j(...k),L=q(...k),U={level:m,message:f,timestamp:new Date().toISOString(),context:T||{},error:L?{name:L.name,message:L.message,stack:L.stack}:void 0};N(JSON.stringify(n.format(U)),A[m])}}var B="__jsonlLoggerIntercepted";function O(m){if(globalThis[B])return;globalThis[B]=!0;let n=m?.formatter??F,R=I[m?.level??"debug"],o=m?.filter,y=[["log","info"],["info","info"],["warn","warn"],["error","error"],["debug","debug"]];for(let[k,f]of y)console[k]=t(f,n,R,o)}if(p){let n={"google-cloud-logging":F,"victoria-logs":_}[p]??F,R=process.env.LOG_LEVEL||"info";O({formatter:n,level:R})}
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
- name: string;
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;
@@ -1 +1 @@
1
- var t={messageKey:"_msg",format(e){let r={_msg:e.message,_time:e.timestamp,level:e.level,...e.context};if(e.error){if(r["error.name"]=e.error.name,r["error.message"]=e.error.message,e.error.stack)r["error.stack"]=e.error.stack}return r}};export{t as VictoriaLogs};
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.2.3",
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.2",
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.error) {
21
- entry['error.name'] = record.error.name
22
- entry['error.message'] = record.error.message
23
- if (record.error.stack) entry['error.stack'] = record.error.stack
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 = { name: err.name, message: err.message, stack: err.stack }
120
+ record.error = errorInfo(err)
93
121
  }
94
122
 
95
123
  if (this.json) {
96
- write(JSON.stringify(this.fmt.format(record)), isErrorLevel[level])
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
- const errStr = record.error
122
- ? ` [${record.error.name}: ${record.error.message}]`
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 type { Formatter, InterceptOptions, LogLevel } from './types'
3
- import { logLevelValues, stripAnsi, write } from './types'
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
- ? { name: error.name, message: error.message, stack: error.stack }
108
- : undefined,
113
+ error: error ? errorInfo(error) : undefined,
114
+ trace: traceContext?.(),
109
115
  }
110
116
 
111
- write(JSON.stringify(formatter.format(record)), isErrorLevel[level])
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(level, formatter, minLevel, filter)
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?: { name: string; message: string; stack?: string }
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.
@@ -9,10 +9,12 @@ export const VictoriaLogs: Formatter = {
9
9
  level: record.level,
10
10
  ...record.context,
11
11
  }
12
- if (record.error) {
13
- entry['error.name'] = record.error.name
14
- entry['error.message'] = record.error.message
15
- if (record.error.stack) entry['error.stack'] = record.error.stack
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
  },