jsonl-logger 0.2.2 → 0.3.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,6 +1,13 @@
1
- # jsonl-logger
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)
2
4
 
3
- Lightweight JSON Lines (JSONL) logger with pluggable formatters. Modern ESM-only, zero dependencies, Bun-first, works on Node.js and Deno.
5
+
6
+ # JSONL Logger
7
+
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.
9
+
10
+ Next.js (and other hardcoded plain text logs) become JSON-only logging for systems where it is required.
4
11
 
5
12
  ## Install
6
13
 
@@ -29,20 +36,14 @@ Set `LOG_FORMAT` to enable JSON output with a specific formatter:
29
36
 
30
37
  ```bash
31
38
  LOG_FORMAT=google-cloud-logging bun run server.ts
32
- ```
33
-
34
- ```typescript
35
- // Output: {"message":"...","timestamp":"...","severity":"INFO",...}
39
+ # Output: {"message":"...","timestamp":"...","severity":"INFO",...}
36
40
  ```
37
41
 
38
42
  ### VictoriaLogs
39
43
 
40
44
  ```bash
41
45
  LOG_FORMAT=victoria-logs bun run server.ts
42
- ```
43
-
44
- ```typescript
45
- // Output: {"_msg":"...","_time":"...","level":"info",...}
46
+ # Output: {"_msg":"...","_time":"...","level":"info",...}
46
47
  ```
47
48
 
48
49
  ### Custom Formatter
@@ -82,15 +83,41 @@ console.log('plain text') // → structured JSON
82
83
  originalConsole.log('bypass interception')
83
84
  ```
84
85
 
85
- ## Preload (Next.js Standalone)
86
+ ## Next.js Integration
86
87
 
87
- Auto-intercept from first line using `--preload`:
88
+ 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`.
88
89
 
89
- ```bash
90
- LOG_FORMAT=victoria-logs bun --preload jsonl-logger/preload server.js
90
+ ### Instrumentation
91
+
92
+ Next.js auto-detects `instrumentation.ts` at the project root. Use it to load the preload module on the server:
93
+
94
+ ```ts
95
+ export async function register() {
96
+ if (process.env.NEXT_RUNTIME === 'nodejs' || typeof Bun !== 'undefined') {
97
+ await import('jsonl-logger/preload')
98
+ }
99
+ }
91
100
  ```
92
101
 
93
- 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`.
102
+ ### Dockerfile (Standalone with Bun)
103
+
104
+ Next.js standalone output doesn't include all `node_modules`. Copy `jsonl-logger` explicitly from the build stage:
105
+
106
+ ```dockerfile
107
+ COPY --from=build /app/node_modules/jsonl-logger ./node_modules/jsonl-logger
108
+
109
+ ENV LOG_FORMAT=victoria-logs
110
+
111
+ CMD ["bun", "--preload", "jsonl-logger/preload", "server.js"]
112
+ ```
113
+
114
+ ### Node.js
115
+
116
+ For non-Bun deployments, use `--import` to preload:
117
+
118
+ ```bash
119
+ LOG_FORMAT=google-cloud-logging node --import jsonl-logger/preload server.js
120
+ ```
94
121
 
95
122
  ## Child Loggers
96
123
 
@@ -100,6 +127,49 @@ requestLogger.info('Processing request')
100
127
  // All entries include requestId and service
101
128
  ```
102
129
 
130
+ ## Error Handling
131
+
132
+ Errors passed to `error()` / `fatal()` capture the full stack trace and `error.cause` chain:
133
+
134
+ ```typescript
135
+ const inner = new Error('ECONNREFUSED')
136
+ const outer = new Error('fetch failed', { cause: inner })
137
+ logger.error('API call failed', { endpoint: '/users' }, outer)
138
+ ```
139
+
140
+ **Dev mode** (no `LOG_FORMAT`) — colored plain text with full stack:
141
+ ```
142
+ 18:42:05 ERROR API call failed {"endpoint":"/users"}
143
+ Error: fetch failed
144
+ at handler (/app/api/route.ts:42:5)
145
+ Caused by: Error: ECONNREFUSED
146
+ at connect (/app/db.ts:10:3)
147
+ ```
148
+
149
+ **Production** (`LOG_FORMAT` set) — structured JSON with `error.*` and `error.cause.*` fields:
150
+ ```json
151
+ {
152
+ "message": "API call failed",
153
+ "severity": "ERROR",
154
+ "endpoint": "/users",
155
+ "error.name": "Error",
156
+ "error.message": "fetch failed",
157
+ "error.stack": "Error: fetch failed\n at handler ...",
158
+ "error.cause.name": "Error",
159
+ "error.cause.message": "ECONNREFUSED",
160
+ "error.cause.stack": "Error: ECONNREFUSED\n at connect ..."
161
+ }
162
+ ```
163
+
164
+ The `errorInfo()` helper is exported for use in custom formatters:
165
+
166
+ ```typescript
167
+ import { errorInfo } from 'jsonl-logger'
168
+
169
+ const info = errorInfo(caughtError)
170
+ // { name, message, stack, cause?: { name, message, stack, cause?: ... } }
171
+ ```
172
+
103
173
  ## Environment Variables
104
174
 
105
175
  | Variable | Default | Description |
@@ -118,7 +188,7 @@ The logger auto-detects the runtime and uses the fastest available I/O:
118
188
 
119
189
  | Subpath | Export |
120
190
  |---------|--------|
121
- | `jsonl-logger` | `Logger`, `logger`, types |
191
+ | `jsonl-logger` | `Logger`, `logger`, `errorInfo()`, types (`ErrorInfo`, `LogRecord`, etc.) |
122
192
  | `jsonl-logger/google-cloud-logging` | `GoogleCloudLogging` formatter |
123
193
  | `jsonl-logger/victoria-logs` | `VictoriaLogs` formatter |
124
194
  | `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 t={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},r={messageKey:"message",format(e){return{message:e.message,timestamp:e.timestamp,severity:t[e.level],...e.context}}};export{r as GoogleCloudLogging};
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
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, } 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;
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 F={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},L={messageKey:"message",format(o){return{message:o.message,timestamp:o.timestamp,severity:F[o.level],...o.context}}};var d={messageKey:"_msg",format(o){return{_msg:o.message,_time:o.timestamp,level:o.level,...o.context}}};var i=process.env.LOG_FORMAT,R=!!i,r={debug:0,info:1,warn:2,error:3,fatal:4},I=/\x1b\[[0-9;]*m/g;function u(o){return o.replace(I,"")}function v(o,t,e="error"){if(o[`${e}.name`]=t.name,o[`${e}.message`]=t.message,t.stack)o[`${e}.stack`]=t.stack;if(t.cause)v(o,t.cause,`${e}.cause`)}var f=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",x=f==="deno"?new TextEncoder:null;function w(o,t){if(f==="node")(t?process.stderr??process.stdout:process.stdout).write(`${o}
2
+ `);else if(f==="deno"&&x){let e=x.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":L,"victoria-logs":d},E=i&&y[i]||L,b=R,O=process.env.LOG_LEVEL||(b?"info":"debug"),S={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function C(o,t){return t.add(o),{name:o.name,message:o.message,stack:o.stack,...o.cause instanceof Error&&!t.has(o.cause)?{cause:C(o.cause,t)}:{}}}function j(o){return C(o,new WeakSet)}class p{ctx;min;json;fmt;constructor(o,t){this.ctx=o||{},this.json=t?.json??b,this.fmt=t?.formatter??E;let e=t?.level??O;this.min=r[e]??r.info}child(o){let t=new p({...this.ctx,...o},{json:this.json,formatter:this.fmt});return t.min=this.min,t}log(o,t,e,m){if(r[o]<this.min)return;let s={level:o,message:this.json?u(t).trim():t,timestamp:new Date().toISOString(),context:e?{...this.ctx,...e}:this.ctx};if(m)s.error=j(m);if(this.json){let g=this.fmt.format(s);if(s.error)v(g,s.error);w(JSON.stringify(g),S[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"},m="\x1B[0m",s=e[o],g=new Date(t.timestamp).toLocaleTimeString("en-US",{hour12:!1}),k=o.toUpperCase().padEnd(5),h=t.context,$=Object.keys(h).length>0?` ${JSON.stringify(h)}`:"",c="";if(t.error){let n=t.error,l=!0;while(n){if(n.stack)c+=l?`
4
+ ${n.stack}`:`
5
+ Caused by: ${n.stack}`;else c+=l?`
6
+ ${n.name}: ${n.message}`:`
7
+ Caused by: ${n.name}: ${n.message}`;n=n.cause,l=!1}}let a=`${s}${g} ${k}\x1B[0m ${t.message}${$}${c}`;switch(o){case"debug":console.debug(a);break;case"warn":console.warn(a);break;case"error":case"fatal":console.error(a);break;default:console.log(a)}}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 U=new p;export{u as stripAnsi,U as logger,r as logLevelValues,j as errorInfo,p 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 d={debug:"DEBUG",info:"INFO",warn:"WARNING",error:"ERROR",fatal:"CRITICAL"},a={messageKey:"message",format(o){return{message:o.message,timestamp:o.timestamp,severity:d[o.level],...o.context}}};var l={messageKey:"_msg",format(o){return{_msg:o.message,_time:o.timestamp,level:o.level,...o.context}}};var b=process.env.LOG_FORMAT,I=!!b,m={debug:0,info:1,warn:2,error:3,fatal:4},E=/\x1b\[[0-9;]*m/g;function u(o){return o.replace(E,"")}function h(o,t,n="error"){if(o[`${n}.name`]=t.name,o[`${n}.message`]=t.message,t.stack)o[`${n}.stack`]=t.stack;if(t.cause)h(o,t.cause,`${n}.cause`)}var w=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",F=w==="deno"?new TextEncoder:null;function R(o,t){if(w==="node")(t?process.stderr??process.stdout:process.stdout).write(`${o}
2
+ `);else if(w==="deno"&&F){let n=F.encode(`${o}
3
+ `);if(t)Deno.stderr.writeSync(n);else Deno.stdout.writeSync(n)}else if(t)console.error(o);else console.log(o)}var N={"google-cloud-logging":a,"victoria-logs":l},y=b&&N[b]||a,v=I,G=process.env.LOG_LEVEL||(v?"info":"debug"),_={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function O(o,t){return t.add(o),{name:o.name,message:o.message,stack:o.stack,...o.cause instanceof Error&&!t.has(o.cause)?{cause:O(o.cause,t)}:{}}}function k(o){return O(o,new WeakSet)}class ${ctx;min;json;fmt;constructor(o,t){this.ctx=o||{},this.json=t?.json??v,this.fmt=t?.formatter??y;let n=t?.level??G;this.min=m[n]??m.info}child(o){let t=new $({...this.ctx,...o},{json:this.json,formatter:this.fmt});return t.min=this.min,t}log(o,t,n,s){if(m[o]<this.min)return;let c={level:o,message:this.json?u(t).trim():t,timestamp:new Date().toISOString(),context:n?{...this.ctx,...n}:this.ctx};if(s)c.error=k(s);if(this.json){let e=this.fmt.format(c);if(c.error)h(e,c.error);R(JSON.stringify(e),_[o])}else this.logPlain(o,c)}logPlain(o,t){let n={debug:"\x1B[36m",info:"\x1B[32m",warn:"\x1B[33m",error:"\x1B[31m",fatal:"\x1B[35m"},s="\x1B[0m",c=n[o],e=new Date(t.timestamp).toLocaleTimeString("en-US",{hour12:!1}),f=o.toUpperCase().padEnd(5),x=t.context,p=Object.keys(x).length>0?` ${JSON.stringify(x)}`:"",L="";if(t.error){let i=t.error,C=!0;while(i){if(i.stack)L+=C?`
4
+ ${i.stack}`:`
5
+ Caused by: ${i.stack}`;else L+=C?`
6
+ ${i.name}: ${i.message}`:`
7
+ Caused by: ${i.name}: ${i.message}`;i=i.cause,C=!1}}let g=`${c}${e} ${f}\x1B[0m ${t.message}${p}${L}`;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,n){this.log("error",o,t,n)}fatal(o,t,n){this.log("fatal",o,t,n)}}var P=new $;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 A(...o){let t="";for(let n=0;n<o.length;n++){if(n>0)t+=" ";let s=o[n];if(typeof s==="string")t+=u(s);else if(s instanceof Error)t+=s.message;else try{t+=JSON.stringify(s)}catch{t+=String(s)}}return t.trim()}function J(...o){let t;for(let n of o)if(typeof n==="object"&&n!==null&&!(n instanceof Error)&&!Array.isArray(n)){if(!t)t={};Object.assign(t,n)}return t}function U(...o){for(let t of o)if(t instanceof Error)return t}var S={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function V(o,t,n,s){let c=`"${t.messageKey}"`;return(...e)=>{if(e.length===1&&typeof e[0]==="string"&&e[0].charCodeAt(0)===123&&e[0].includes(c)){R(e[0],S[o]);return}if(m[o]<n)return;let f=A(...e);if(s&&!s(o,f))return;let x=J(...e),p=U(...e),L={level:o,message:f,timestamp:new Date().toISOString(),context:x||{},error:p?k(p):void 0},g=t.format(L);if(L.error)h(g,L.error);R(JSON.stringify(g),S[o])}}var j="__jsonlLoggerIntercepted";function M(o){if(globalThis[j])return;globalThis[j]=!0;let t=o?.formatter??a,n=m[o?.level??"debug"],s=o?.filter,c=[["log","info"],["info","info"],["warn","warn"],["error","error"],["debug","debug"]];for(let[e,f]of c)console[e]=V(f,t,n,s)}export{K as originalConsole,M 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"},c={messageKey:"message",format(o){return{message:o.message,timestamp:o.timestamp,severity:N[o.level],...o.context}}};var x={messageKey:"_msg",format(o){return{_msg:o.message,_time:o.timestamp,level:o.level,...o.context}}};var a=process.env.LOG_FORMAT,v=!!a,L={debug:0,info:1,warn:2,error:3,fatal:4},S=/\x1b\[[0-9;]*m/g;function u(o){return o.replace(S,"")}function p(o,t,n="error"){if(o[`${n}.name`]=t.name,o[`${n}.message`]=t.message,t.stack)o[`${n}.stack`]=t.stack;if(t.cause)p(o,t.cause,`${n}.cause`)}var C=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",k=C==="deno"?new TextEncoder:null;function l(o,t){if(C==="node")(t?process.stderr??process.stdout:process.stdout).write(`${o}
2
+ `);else if(C==="deno"&&k){let n=k.encode(`${o}
3
+ `);if(t)Deno.stderr.writeSync(n);else Deno.stdout.writeSync(n)}else if(t)console.error(o);else console.log(o)}var j={"google-cloud-logging":c,"victoria-logs":x},y=a&&j[a]||c,$=v,G=process.env.LOG_LEVEL||($?"info":"debug"),V={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function d(o,t){return t.add(o),{name:o.name,message:o.message,stack:o.stack,...o.cause instanceof Error&&!t.has(o.cause)?{cause:d(o.cause,t)}:{}}}function F(o){return d(o,new WeakSet)}class w{ctx;min;json;fmt;constructor(o,t){this.ctx=o||{},this.json=t?.json??$,this.fmt=t?.formatter??y;let n=t?.level??G;this.min=L[n]??L.info}child(o){let t=new w({...this.ctx,...o},{json:this.json,formatter:this.fmt});return t.min=this.min,t}log(o,t,n,e){if(L[o]<this.min)return;let i={level:o,message:this.json?u(t).trim():t,timestamp:new Date().toISOString(),context:n?{...this.ctx,...n}:this.ctx};if(e)i.error=F(e);if(this.json){let s=this.fmt.format(i);if(i.error)p(s,i.error);l(JSON.stringify(s),V[o])}else this.logPlain(o,i)}logPlain(o,t){let n={debug:"\x1B[36m",info:"\x1B[32m",warn:"\x1B[33m",error:"\x1B[31m",fatal:"\x1B[35m"},e="\x1B[0m",i=n[o],s=new Date(t.timestamp).toLocaleTimeString("en-US",{hour12:!1}),r=o.toUpperCase().padEnd(5),R=t.context,h=Object.keys(R).length>0?` ${JSON.stringify(R)}`:"",g="";if(t.error){let m=t.error,b=!0;while(m){if(m.stack)g+=b?`
4
+ ${m.stack}`:`
5
+ Caused by: ${m.stack}`;else g+=b?`
6
+ ${m.name}: ${m.message}`:`
7
+ Caused by: ${m.name}: ${m.message}`;m=m.cause,b=!1}}let f=`${i}${s} ${r}\x1B[0m ${t.message}${h}${g}`;switch(o){case"debug":console.debug(f);break;case"warn":console.warn(f);break;case"error":case"fatal":console.error(f);break;default:console.log(f)}}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,n){this.log("error",o,t,n)}fatal(o,t,n){this.log("fatal",o,t,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 _(...o){let t="";for(let n=0;n<o.length;n++){if(n>0)t+=" ";let e=o[n];if(typeof e==="string")t+=u(e);else if(e instanceof Error)t+=e.message;else try{t+=JSON.stringify(e)}catch{t+=String(e)}}return t.trim()}function A(...o){let t;for(let n of o)if(typeof n==="object"&&n!==null&&!(n instanceof Error)&&!Array.isArray(n)){if(!t)t={};Object.assign(t,n)}return t}function J(...o){for(let t of o)if(t instanceof Error)return t}var I={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function U(o,t,n,e){let i=`"${t.messageKey}"`;return(...s)=>{if(s.length===1&&typeof s[0]==="string"&&s[0].charCodeAt(0)===123&&s[0].includes(i)){l(s[0],I[o]);return}if(L[o]<n)return;let r=_(...s);if(e&&!e(o,r))return;let R=A(...s),h=J(...s),g={level:o,message:r,timestamp:new Date().toISOString(),context:R||{},error:h?F(h):void 0},f=t.format(g);if(g.error)p(f,g.error);l(JSON.stringify(f),I[o])}}var O="__jsonlLoggerIntercepted";function E(o){if(globalThis[O])return;globalThis[O]=!0;let t=o?.formatter??c,n=L[o?.level??"debug"],e=o?.filter,i=[["log","info"],["info","info"],["warn","warn"],["error","error"],["debug","debug"]];for(let[s,r]of i)console[s]=U(r,t,n,e)}if(a){let t={"google-cloud-logging":c,"victoria-logs":x}[a]??c,n=process.env.LOG_LEVEL||"info";E({formatter:t,level:n})}
package/dist/types.d.ts CHANGED
@@ -1,15 +1,17 @@
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
+ };
3
9
  export type LogRecord = {
4
10
  level: LogLevel;
5
11
  message: string;
6
12
  timestamp: string;
7
13
  context: LogContext;
8
- error?: {
9
- name: string;
10
- message: string;
11
- stack?: string;
12
- };
14
+ error?: ErrorInfo;
13
15
  };
14
16
  export type Formatter = {
15
17
  format: (record: LogRecord) => Record<string, unknown>;
@@ -30,4 +32,5 @@ export type InterceptOptions = {
30
32
  };
31
33
  export declare const logLevelValues: Record<LogLevel, number>;
32
34
  export declare function stripAnsi(str: string): string;
35
+ export declare function flattenError(entry: Record<string, unknown>, error: ErrorInfo, prefix?: string): void;
33
36
  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 o={messageKey:"_msg",format(t){return{_msg:t.message,_time:t.timestamp,level:t.level,...t.context}}};export{o as VictoriaLogs};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsonl-logger",
3
- "version": "0.2.2",
3
+ "version": "0.3.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",
@@ -17,11 +17,6 @@ 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
24
- }
25
20
  return entry
26
21
  },
27
22
  }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { GoogleCloudLogging } from './google-cloud-logging'
2
2
  import type {
3
+ ErrorInfo,
3
4
  Formatter,
4
5
  FormatterName,
5
6
  LogContext,
@@ -9,6 +10,7 @@ import type {
9
10
  } from './types'
10
11
  import {
11
12
  defaultFormat,
13
+ flattenError,
12
14
  isJsonMode,
13
15
  logLevelValues,
14
16
  stripAnsi,
@@ -17,6 +19,7 @@ import {
17
19
  import { VictoriaLogs } from './victoria-logs'
18
20
 
19
21
  export type {
22
+ ErrorInfo,
20
23
  Formatter,
21
24
  FormatterName,
22
25
  InterceptOptions,
@@ -47,6 +50,22 @@ const isErrorLevel: Record<LogLevel, boolean> = {
47
50
  fatal: true,
48
51
  }
49
52
 
53
+ function extractErrorInfo(err: Error, visited: WeakSet<Error>): ErrorInfo {
54
+ visited.add(err)
55
+ return {
56
+ name: err.name,
57
+ message: err.message,
58
+ stack: err.stack,
59
+ ...(err.cause instanceof Error && !visited.has(err.cause)
60
+ ? { cause: extractErrorInfo(err.cause, visited) }
61
+ : {}),
62
+ }
63
+ }
64
+
65
+ export function errorInfo(err: Error): ErrorInfo {
66
+ return extractErrorInfo(err, new WeakSet())
67
+ }
68
+
50
69
  export class Logger {
51
70
  private ctx: LogContext
52
71
  private min: number
@@ -89,11 +108,13 @@ export class Logger {
89
108
  }
90
109
 
91
110
  if (err) {
92
- record.error = { name: err.name, message: err.message, stack: err.stack }
111
+ record.error = errorInfo(err)
93
112
  }
94
113
 
95
114
  if (this.json) {
96
- write(JSON.stringify(this.fmt.format(record)), isErrorLevel[level])
115
+ const formatted = this.fmt.format(record)
116
+ if (record.error) flattenError(formatted, record.error)
117
+ write(JSON.stringify(formatted), isErrorLevel[level])
97
118
  } else {
98
119
  this.logPlain(level, record)
99
120
  }
@@ -118,9 +139,22 @@ export class Logger {
118
139
  const ctx = record.context
119
140
  const metaStr = Object.keys(ctx).length > 0 ? ` ${JSON.stringify(ctx)}` : ''
120
141
 
121
- const errStr = record.error
122
- ? ` [${record.error.name}: ${record.error.message}]`
123
- : ''
142
+ let errStr = ''
143
+ if (record.error) {
144
+ let current: ErrorInfo | undefined = record.error
145
+ let isRoot = true
146
+ while (current) {
147
+ if (current.stack) {
148
+ errStr += isRoot ? `\n${current.stack}` : `\nCaused by: ${current.stack}`
149
+ } else {
150
+ errStr += isRoot
151
+ ? `\n ${current.name}: ${current.message}`
152
+ : `\nCaused by: ${current.name}: ${current.message}`
153
+ }
154
+ current = current.cause
155
+ isRoot = false
156
+ }
157
+ }
124
158
 
125
159
  const output = `${color}${time} ${levelStr}${reset} ${record.message}${metaStr}${errStr}`
126
160
 
package/src/intercept.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { GoogleCloudLogging } from './google-cloud-logging'
2
+ import { errorInfo } from './index'
2
3
  import type { Formatter, InterceptOptions, LogLevel } from './types'
3
- import { logLevelValues, stripAnsi, write } from './types'
4
+ import { flattenError, logLevelValues, stripAnsi, write } from './types'
4
5
 
5
6
  type ConsoleMethods = {
6
7
  log: typeof console.log
@@ -103,12 +104,12 @@ function createOverride(
103
104
  message,
104
105
  timestamp: new Date().toISOString(),
105
106
  context: meta || {},
106
- error: error
107
- ? { name: error.name, message: error.message, stack: error.stack }
108
- : undefined,
107
+ error: error ? errorInfo(error) : undefined,
109
108
  }
110
109
 
111
- write(JSON.stringify(formatter.format(record)), isErrorLevel[level])
110
+ const formatted = formatter.format(record)
111
+ if (record.error) flattenError(formatted, record.error)
112
+ write(JSON.stringify(formatted), isErrorLevel[level])
112
113
  }
113
114
  }
114
115
 
package/src/types.ts CHANGED
@@ -2,12 +2,19 @@ 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
+
5
12
  export type LogRecord = {
6
13
  level: LogLevel
7
14
  message: string
8
15
  timestamp: string
9
16
  context: LogContext
10
- error?: { name: string; message: string; stack?: string }
17
+ error?: ErrorInfo
11
18
  }
12
19
 
13
20
  export type Formatter = {
@@ -47,6 +54,17 @@ export function stripAnsi(str: string): string {
47
54
  return str.replace(ansiPattern, '')
48
55
  }
49
56
 
57
+ export function flattenError(
58
+ entry: Record<string, unknown>,
59
+ error: ErrorInfo,
60
+ prefix = 'error',
61
+ ): void {
62
+ entry[`${prefix}.name`] = error.name
63
+ entry[`${prefix}.message`] = error.message
64
+ if (error.stack) entry[`${prefix}.stack`] = error.stack
65
+ if (error.cause) flattenError(entry, error.cause, `${prefix}.cause`)
66
+ }
67
+
50
68
  /**
51
69
  * Detect runtime once, resolve streams at call time.
52
70
  * Stream lookup is deferred so tests/runtime can replace process.stdout.
@@ -9,11 +9,6 @@ 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
16
- }
17
12
  return entry
18
13
  },
19
14
  }