jsonl-logger 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Annexare
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # jsonl-logger
2
+
3
+ Lightweight JSON Lines logger with pluggable formatters. Modern ESM-only, zero dependencies, Bun-first, works on Node.js and Deno.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add jsonl-logger
9
+ # or
10
+ npm install jsonl-logger
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { logger } from 'jsonl-logger'
17
+
18
+ logger.info('Server started', { port: 3000 })
19
+ logger.error('Request failed', { path: '/api' }, new Error('timeout'))
20
+ ```
21
+
22
+ ## Formatters
23
+
24
+ Two built-in formatters for popular log backends:
25
+
26
+ ### Google Cloud Logging
27
+
28
+ ```typescript
29
+ import { googleCloud } from 'jsonl-logger/google-cloud'
30
+ import { Logger } from 'jsonl-logger'
31
+
32
+ const logger = new Logger(undefined, { json: true, formatter: googleCloud })
33
+ // Output: {"message":"...","timestamp":"...","severity":"INFO",...}
34
+ ```
35
+
36
+ ### VictoriaLogs (default)
37
+
38
+ ```typescript
39
+ import { victoriaLogs } from 'jsonl-logger/victoria-logs'
40
+ // Output: {"_msg":"...","_time":"...","level":"info",...}
41
+ ```
42
+
43
+ ### Custom Formatter
44
+
45
+ ```typescript
46
+ import type { Formatter } from 'jsonl-logger'
47
+
48
+ const myFormatter: Formatter = {
49
+ messageKey: 'msg',
50
+ format: (record) => ({
51
+ msg: record.message,
52
+ ts: record.timestamp,
53
+ lvl: record.level,
54
+ ...record.context,
55
+ }),
56
+ }
57
+ ```
58
+
59
+ ## Console Interception
60
+
61
+ Monkey-patch `console.*` methods to output structured JSON — captures logs from third-party libraries:
62
+
63
+ ```typescript
64
+ import { intercept, originalConsole } from 'jsonl-logger/intercept'
65
+
66
+ intercept({
67
+ // Optional: custom formatter (default: victoriaLogs)
68
+ formatter: googleCloud,
69
+ // Optional: filter out noisy messages
70
+ filter: (level, message) => !message.includes('deprecation'),
71
+ // Optional: minimum log level
72
+ level: 'warn',
73
+ })
74
+
75
+ // Already-formatted JSON from the Logger class passes through unchanged
76
+ console.log('plain text') // → structured JSON
77
+ originalConsole.log('bypass interception')
78
+ ```
79
+
80
+ ## Preload (Next.js Standalone)
81
+
82
+ Auto-intercept from first line using `--preload`:
83
+
84
+ ```bash
85
+ bun --preload jsonl-logger/preload server.js
86
+ ```
87
+
88
+ Environment variables:
89
+ - `LOG_FORMAT` — `google-cloud` or `victoria-logs` (default)
90
+ - `LOG_LEVEL` — `debug`, `info` (default), `warn`, `error`, `fatal`
91
+
92
+ ## Child Loggers
93
+
94
+ ```typescript
95
+ const requestLogger = logger.child({ requestId: 'abc-123', service: 'api' })
96
+ requestLogger.info('Processing request')
97
+ // All entries include requestId and service
98
+ ```
99
+
100
+ ## Environment Variables
101
+
102
+ | Variable | Default | Description |
103
+ |----------|---------|-------------|
104
+ | `JSON_LOGS` | `false` | Enable JSON output (`true` for production) |
105
+ | `LOG_LEVEL` | `info`/`debug` | Minimum log level (defaults to `info` when JSON, `debug` otherwise) |
106
+ | `LOG_FORMAT` | `victoria-logs` | Formatter for preload module (`google-cloud` or `victoria-logs`) |
107
+
108
+ ## Runtime Detection
109
+
110
+ The logger auto-detects the runtime and uses the fastest available I/O:
111
+ - **Bun** / **Node.js** — `process.stdout.write` / `process.stderr.write` (bypasses console overhead)
112
+ - **Deno** — `Deno.stdout.writeSync` / `Deno.stderr.writeSync`
113
+ - **Browser / unknown** — falls back to `console.log` / `console.error`
114
+
115
+ ## Exports
116
+
117
+ | Subpath | Export |
118
+ |---------|--------|
119
+ | `jsonl-logger` | `Logger`, `logger`, types |
120
+ | `jsonl-logger/google-cloud` | `googleCloud` formatter |
121
+ | `jsonl-logger/victoria-logs` | `victoriaLogs` formatter |
122
+ | `jsonl-logger/intercept` | `intercept()`, `originalConsole` |
123
+ | `jsonl-logger/preload` | Side-effect auto-intercept |
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,2 @@
1
+ import type { Formatter } from './types';
2
+ export declare const googleCloud: Formatter;
@@ -0,0 +1 @@
1
+ import{a}from"./index-rhgqp0zd.js";export{a as googleCloud};
@@ -0,0 +1,4 @@
1
+ var B={debug:0,info:1,warn:2,error:3,fatal:4},A=/\x1b\[[0-9;]*m/g;function C(h){return h.replace(A,"")}var w=typeof process<"u"&&process.stdout&&typeof process.stdout.write==="function"?"node":typeof Deno<"u"&&Deno.stdout?"deno":"browser",z=w==="deno"?new TextEncoder:null;function F(h,j){if(w==="node")(j?process.stderr??process.stdout:process.stdout).write(`${h}
2
+ `);else if(w==="deno"&&z){let q=z.encode(`${h}
3
+ `);if(j)Deno.stderr.writeSync(q);else Deno.stdout.writeSync(q)}else if(j)console.error(h);else console.log(h)}
4
+ export{B as d,C as e,F as f};
@@ -0,0 +1,2 @@
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}};
2
+ export{o as a};
@@ -0,0 +1,2 @@
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}};
2
+ export{t as g};
@@ -0,0 +1,2 @@
1
+ import{d as G,e as Q,f as F}from"./index-abhqk8y0.js";import{g as R}from"./index-rxqh9v58.js";var A={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 W(...j){let b="";for(let k=0;k<j.length;k++){if(k>0)b+=" ";let q=j[k];if(typeof q==="string")b+=Q(q);else if(q instanceof Error)b+=q.message;else try{b+=JSON.stringify(q)}catch{b+=String(q)}}return b.trim()}function X(...j){let b;for(let k of j)if(typeof k==="object"&&k!==null&&!(k instanceof Error)&&!Array.isArray(k)){if(!b)b={};Object.assign(b,k)}return b}function Y(...j){for(let b of j)if(b instanceof Error)return b}var H={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};function Z(j,b,k,q){let D=`"${b.messageKey}"`;return(...z)=>{if(z.length===1&&typeof z[0]==="string"&&z[0].charCodeAt(0)===123&&z[0].includes(D)){F(z[0],H[j]);return}if(G[j]<k)return;let B=W(...z);if(q&&!q(j,B))return;let T=X(...z),C=Y(...z),U={level:j,message:B,timestamp:new Date().toISOString(),context:T||{},error:C?{name:C.name,message:C.message,stack:C.stack}:void 0};F(JSON.stringify(b.format(U)),H[j])}}var I="__jsonlLoggerIntercepted";function J(j){if(globalThis[I])return;globalThis[I]=!0;let b=j?.formatter??R,k=G[j?.level??"debug"],q=j?.filter,D=[["log","info"],["info","info"],["warn","warn"],["error","error"],["debug","debug"]];for(let[z,B]of D)console[z]=Z(B,b,k,q)}
2
+ export{A as b,J as c};
@@ -0,0 +1,19 @@
1
+ import type { LogContext, LoggerOptions } from './types';
2
+ export type { Formatter, InterceptOptions, LogContext, LoggerOptions, LogLevel, LogRecord, } from './types';
3
+ export { logLevelValues, stripAnsi } from './types';
4
+ export declare class Logger {
5
+ private ctx;
6
+ private min;
7
+ private json;
8
+ private fmt;
9
+ constructor(context?: LogContext, options?: LoggerOptions);
10
+ child(context: LogContext): Logger;
11
+ private log;
12
+ private logPlain;
13
+ debug(message: string, meta?: LogContext): void;
14
+ info(message: string, meta?: LogContext): void;
15
+ warn(message: string, meta?: LogContext): void;
16
+ error(message: string, meta?: LogContext, error?: Error): void;
17
+ fatal(message: string, meta?: LogContext, error?: Error): void;
18
+ }
19
+ export declare const logger: Logger;
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{d as n,e as a,f as c}from"./index-abhqk8y0.js";import{g as L}from"./index-rxqh9v58.js";var m=process.env.JSON_LOGS==="true",d=process.env.LOG_LEVEL||(m?"info":"debug"),h={debug:!1,info:!1,warn:!1,error:!0,fatal:!0};class g{ctx;min;json;fmt;constructor(o,t){this.ctx=o||{},this.json=t?.json??m,this.fmt=t?.formatter??L;let e=t?.level??d;this.min=n[e]??n.info}child(o){let t=new g({...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?a(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)c(JSON.stringify(this.fmt.format(s)),h[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],f=new Date(t.timestamp).toLocaleTimeString("en-US",{hour12:!1}),p=o.toUpperCase().padEnd(5),l=t.context,x=Object.keys(l).length>0?` ${JSON.stringify(l)}`:"",v=t.error?` [${t.error.name}: ${t.error.message}]`:"",i=`${s}${f} ${p}\x1B[0m ${t.message}${x}${v}`;switch(o){case"debug":console.debug(i);break;case"warn":console.warn(i);break;case"error":case"fatal":console.error(i);break;default:console.log(i)}}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 w=new g;export{a as stripAnsi,w as logger,n as logLevelValues,g as Logger};
@@ -0,0 +1,11 @@
1
+ import type { InterceptOptions } from './types';
2
+ type ConsoleMethods = {
3
+ log: typeof console.log;
4
+ info: typeof console.info;
5
+ warn: typeof console.warn;
6
+ error: typeof console.error;
7
+ debug: typeof console.debug;
8
+ };
9
+ export declare const originalConsole: ConsoleMethods;
10
+ export declare function intercept(options?: InterceptOptions): void;
11
+ export {};
@@ -0,0 +1 @@
1
+ import{b as a,c as b}from"./index-x1ss6ry6.js";import"./index-abhqk8y0.js";import"./index-rxqh9v58.js";export{a as originalConsole,b as intercept};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ import{a as r}from"./index-rhgqp0zd.js";import{c as t}from"./index-x1ss6ry6.js";import"./index-abhqk8y0.js";import{g as o}from"./index-rxqh9v58.js";var e={"victoria-logs":o,"google-cloud":r},s=process.env.LOG_FORMAT||"victoria-logs",i=e[s]??o,m=process.env.LOG_LEVEL||"info";t({formatter:i,level:m});
@@ -0,0 +1,30 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
2
+ export type LogContext = Record<string, unknown>;
3
+ export type LogRecord = {
4
+ level: LogLevel;
5
+ message: string;
6
+ timestamp: string;
7
+ context: LogContext;
8
+ error?: {
9
+ name: string;
10
+ message: string;
11
+ stack?: string;
12
+ };
13
+ };
14
+ export type Formatter = {
15
+ format: (record: LogRecord) => Record<string, unknown>;
16
+ messageKey: string;
17
+ };
18
+ export type LoggerOptions = {
19
+ formatter?: Formatter;
20
+ json?: boolean;
21
+ level?: LogLevel;
22
+ };
23
+ export type InterceptOptions = {
24
+ formatter?: Formatter;
25
+ filter?: (level: LogLevel, message: string) => boolean;
26
+ level?: LogLevel;
27
+ };
28
+ export declare const logLevelValues: Record<LogLevel, number>;
29
+ export declare function stripAnsi(str: string): string;
30
+ export declare function write(data: string, isError: boolean): void;
@@ -0,0 +1,2 @@
1
+ import type { Formatter } from './types';
2
+ export declare const victoriaLogs: Formatter;
@@ -0,0 +1 @@
1
+ import{g as a}from"./index-rxqh9v58.js";export{a as victoriaLogs};
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "jsonl-logger",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight ESM-only JSON Lines logger with pluggable formatters for Google Cloud Logging, VictoriaLogs, and more",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "./src/preload.ts",
9
+ "./dist/preload.js"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "bun": "./src/index.ts",
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ },
17
+ "./google-cloud": {
18
+ "bun": "./src/google-cloud.ts",
19
+ "import": "./dist/google-cloud.js",
20
+ "types": "./dist/google-cloud.d.ts"
21
+ },
22
+ "./victoria-logs": {
23
+ "bun": "./src/victoria-logs.ts",
24
+ "import": "./dist/victoria-logs.js",
25
+ "types": "./dist/victoria-logs.d.ts"
26
+ },
27
+ "./intercept": {
28
+ "bun": "./src/intercept.ts",
29
+ "import": "./dist/intercept.js",
30
+ "types": "./dist/intercept.d.ts"
31
+ },
32
+ "./preload": {
33
+ "bun": "./src/preload.ts",
34
+ "import": "./dist/preload.js",
35
+ "types": "./dist/preload.d.ts"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src"
41
+ ],
42
+ "scripts": {
43
+ "build": "bun run build:js && bun run build:types",
44
+ "build:js": "bun build src/index.ts src/google-cloud.ts src/victoria-logs.ts src/intercept.ts src/preload.ts --outdir dist --target node --format esm --splitting --minify",
45
+ "build:types": "tsc --emitDeclarationOnly",
46
+ "lint": "biome check --write .",
47
+ "test": "bun test"
48
+ },
49
+ "devDependencies": {
50
+ "@biomejs/biome": "2.4.2",
51
+ "@types/bun": "1.3.9",
52
+ "typescript": "5.9.3"
53
+ },
54
+ "keywords": [
55
+ "logger",
56
+ "jsonl",
57
+ "json-lines",
58
+ "esm",
59
+ "structured-logging",
60
+ "google-cloud-logging",
61
+ "victoria-logs",
62
+ "bun",
63
+ "nextjs"
64
+ ],
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "git+https://github.com/annexare/jsonl-logger.git"
68
+ },
69
+ "engines": {
70
+ "node": ">=18"
71
+ }
72
+ }
@@ -0,0 +1,27 @@
1
+ import type { Formatter, LogLevel, LogRecord } from './types'
2
+
3
+ const severityMap: Record<LogLevel, string> = {
4
+ debug: 'DEBUG',
5
+ info: 'INFO',
6
+ warn: 'WARNING',
7
+ error: 'ERROR',
8
+ fatal: 'CRITICAL',
9
+ }
10
+
11
+ export const googleCloud: Formatter = {
12
+ messageKey: 'message',
13
+ format(record: LogRecord): Record<string, unknown> {
14
+ const entry: Record<string, unknown> = {
15
+ message: record.message,
16
+ timestamp: record.timestamp,
17
+ severity: severityMap[record.level],
18
+ ...record.context,
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
+ return entry
26
+ },
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,148 @@
1
+ import type {
2
+ Formatter,
3
+ LogContext,
4
+ LoggerOptions,
5
+ LogLevel,
6
+ LogRecord,
7
+ } from './types'
8
+ import { logLevelValues, stripAnsi, write } from './types'
9
+ import { victoriaLogs } from './victoria-logs'
10
+
11
+ export type {
12
+ Formatter,
13
+ InterceptOptions,
14
+ LogContext,
15
+ LoggerOptions,
16
+ LogLevel,
17
+ LogRecord,
18
+ } from './types'
19
+ export { logLevelValues, stripAnsi } from './types'
20
+
21
+ const defaultJson = process.env.JSON_LOGS === 'true'
22
+ const defaultLevel: LogLevel =
23
+ (process.env.LOG_LEVEL as LogLevel | undefined) ||
24
+ (defaultJson ? 'info' : 'debug')
25
+
26
+ const isErrorLevel: Record<LogLevel, boolean> = {
27
+ debug: false,
28
+ info: false,
29
+ warn: false,
30
+ error: true,
31
+ fatal: true,
32
+ }
33
+
34
+ export class Logger {
35
+ private ctx: LogContext
36
+ private min: number
37
+ private json: boolean
38
+ private fmt: Formatter
39
+
40
+ constructor(context?: LogContext, options?: LoggerOptions) {
41
+ this.ctx = context || {}
42
+ this.json = options?.json ?? defaultJson
43
+ this.fmt = options?.formatter ?? victoriaLogs
44
+ const level: LogLevel = options?.level ?? defaultLevel
45
+ this.min = logLevelValues[level] ?? logLevelValues.info
46
+ }
47
+
48
+ child(context: LogContext): Logger {
49
+ const child = new Logger(
50
+ { ...this.ctx, ...context },
51
+ {
52
+ json: this.json,
53
+ formatter: this.fmt,
54
+ },
55
+ )
56
+ child.min = this.min
57
+ return child
58
+ }
59
+
60
+ private log(
61
+ level: LogLevel,
62
+ message: string,
63
+ meta?: LogContext,
64
+ err?: Error,
65
+ ): void {
66
+ if (logLevelValues[level] < this.min) return
67
+
68
+ const record: LogRecord = {
69
+ level,
70
+ message: this.json ? stripAnsi(message).trim() : message,
71
+ timestamp: new Date().toISOString(),
72
+ context: meta ? { ...this.ctx, ...meta } : this.ctx,
73
+ }
74
+
75
+ if (err) {
76
+ record.error = { name: err.name, message: err.message, stack: err.stack }
77
+ }
78
+
79
+ if (this.json) {
80
+ write(JSON.stringify(this.fmt.format(record)), isErrorLevel[level])
81
+ } else {
82
+ this.logPlain(level, record)
83
+ }
84
+ }
85
+
86
+ private logPlain(level: LogLevel, record: LogRecord): void {
87
+ const colors: Record<LogLevel, string> = {
88
+ debug: '\x1b[36m',
89
+ info: '\x1b[32m',
90
+ warn: '\x1b[33m',
91
+ error: '\x1b[31m',
92
+ fatal: '\x1b[35m',
93
+ }
94
+ const reset = '\x1b[0m'
95
+ const color = colors[level]
96
+
97
+ const time = new Date(record.timestamp).toLocaleTimeString('en-US', {
98
+ hour12: false,
99
+ })
100
+ const levelStr = level.toUpperCase().padEnd(5)
101
+
102
+ const ctx = record.context
103
+ const metaStr = Object.keys(ctx).length > 0 ? ` ${JSON.stringify(ctx)}` : ''
104
+
105
+ const errStr = record.error
106
+ ? ` [${record.error.name}: ${record.error.message}]`
107
+ : ''
108
+
109
+ const output = `${color}${time} ${levelStr}${reset} ${record.message}${metaStr}${errStr}`
110
+
111
+ switch (level) {
112
+ case 'debug':
113
+ console.debug(output)
114
+ break
115
+ case 'warn':
116
+ console.warn(output)
117
+ break
118
+ case 'error':
119
+ case 'fatal':
120
+ console.error(output)
121
+ break
122
+ default:
123
+ console.log(output)
124
+ }
125
+ }
126
+
127
+ debug(message: string, meta?: LogContext): void {
128
+ this.log('debug', message, meta)
129
+ }
130
+
131
+ info(message: string, meta?: LogContext): void {
132
+ this.log('info', message, meta)
133
+ }
134
+
135
+ warn(message: string, meta?: LogContext): void {
136
+ this.log('warn', message, meta)
137
+ }
138
+
139
+ error(message: string, meta?: LogContext, error?: Error): void {
140
+ this.log('error', message, meta, error)
141
+ }
142
+
143
+ fatal(message: string, meta?: LogContext, error?: Error): void {
144
+ this.log('fatal', message, meta, error)
145
+ }
146
+ }
147
+
148
+ export const logger = new Logger()
@@ -0,0 +1,136 @@
1
+ import type { Formatter, InterceptOptions, LogLevel } from './types'
2
+ import { logLevelValues, stripAnsi, write } from './types'
3
+ import { victoriaLogs } from './victoria-logs'
4
+
5
+ type ConsoleMethods = {
6
+ log: typeof console.log
7
+ info: typeof console.info
8
+ warn: typeof console.warn
9
+ error: typeof console.error
10
+ debug: typeof console.debug
11
+ }
12
+
13
+ // Capture original methods once, before anything else runs
14
+ export const originalConsole: ConsoleMethods = {
15
+ log: console.log.bind(console),
16
+ info: console.info.bind(console),
17
+ warn: console.warn.bind(console),
18
+ error: console.error.bind(console),
19
+ debug: console.debug.bind(console),
20
+ }
21
+
22
+ function formatMessage(...args: unknown[]): string {
23
+ let result = ''
24
+ for (let i = 0; i < args.length; i++) {
25
+ if (i > 0) result += ' '
26
+ const arg = args[i]
27
+ if (typeof arg === 'string') {
28
+ result += stripAnsi(arg)
29
+ } else if (arg instanceof Error) {
30
+ result += arg.message
31
+ } else {
32
+ try {
33
+ result += JSON.stringify(arg)
34
+ } catch {
35
+ result += String(arg)
36
+ }
37
+ }
38
+ }
39
+ return result.trim()
40
+ }
41
+
42
+ function extractMeta(...args: unknown[]): Record<string, unknown> | undefined {
43
+ let merged: Record<string, unknown> | undefined
44
+ for (const arg of args) {
45
+ if (
46
+ typeof arg === 'object' &&
47
+ arg !== null &&
48
+ !(arg instanceof Error) &&
49
+ !Array.isArray(arg)
50
+ ) {
51
+ if (!merged) merged = {}
52
+ Object.assign(merged, arg)
53
+ }
54
+ }
55
+ return merged
56
+ }
57
+
58
+ function extractError(...args: unknown[]): Error | undefined {
59
+ for (const arg of args) {
60
+ if (arg instanceof Error) return arg
61
+ }
62
+ }
63
+
64
+ const isErrorLevel = {
65
+ debug: false,
66
+ info: false,
67
+ warn: false,
68
+ error: true,
69
+ fatal: true,
70
+ }
71
+
72
+ function createOverride(
73
+ level: LogLevel,
74
+ formatter: Formatter,
75
+ minLevel: number,
76
+ filter?: (level: LogLevel, message: string) => boolean,
77
+ ): (...args: unknown[]) => void {
78
+ const msgKey = `"${formatter.messageKey}"`
79
+
80
+ return (...args: unknown[]) => {
81
+ // Passthrough: already-formatted JSON from our Logger
82
+ if (
83
+ args.length === 1 &&
84
+ typeof args[0] === 'string' &&
85
+ args[0].charCodeAt(0) === 123 && // starts with '{'
86
+ args[0].includes(msgKey)
87
+ ) {
88
+ write(args[0], isErrorLevel[level])
89
+ return
90
+ }
91
+
92
+ if (logLevelValues[level] < minLevel) return
93
+
94
+ const message = formatMessage(...args)
95
+
96
+ if (filter && !filter(level, message)) return
97
+
98
+ const meta = extractMeta(...args)
99
+ const error = extractError(...args)
100
+
101
+ const record = {
102
+ level,
103
+ message,
104
+ timestamp: new Date().toISOString(),
105
+ context: meta || {},
106
+ error: error
107
+ ? { name: error.name, message: error.message, stack: error.stack }
108
+ : undefined,
109
+ }
110
+
111
+ write(JSON.stringify(formatter.format(record)), isErrorLevel[level])
112
+ }
113
+ }
114
+
115
+ const guardKey = '__jsonlLoggerIntercepted'
116
+
117
+ export function intercept(options?: InterceptOptions): void {
118
+ if ((globalThis as Record<string, unknown>)[guardKey]) return
119
+ ;(globalThis as Record<string, unknown>)[guardKey] = true
120
+
121
+ const formatter = options?.formatter ?? victoriaLogs
122
+ const minLevel = logLevelValues[options?.level ?? 'debug']
123
+ const filter = options?.filter
124
+
125
+ const methodMap: [keyof ConsoleMethods, LogLevel][] = [
126
+ ['log', 'info'],
127
+ ['info', 'info'],
128
+ ['warn', 'warn'],
129
+ ['error', 'error'],
130
+ ['debug', 'debug'],
131
+ ]
132
+
133
+ for (const [method, level] of methodMap) {
134
+ console[method] = createOverride(level, formatter, minLevel, filter)
135
+ }
136
+ }
package/src/preload.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { googleCloud } from './google-cloud'
2
+ import { intercept } from './intercept'
3
+ import type { Formatter } from './types'
4
+ import { victoriaLogs } from './victoria-logs'
5
+
6
+ const formatters: Record<string, Formatter> = {
7
+ 'victoria-logs': victoriaLogs,
8
+ 'google-cloud': googleCloud,
9
+ }
10
+
11
+ const format = process.env.LOG_FORMAT || 'victoria-logs'
12
+ const formatter = formatters[format] ?? victoriaLogs
13
+ const level =
14
+ (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error' | 'fatal') ||
15
+ 'info'
16
+
17
+ intercept({ formatter, level })
package/src/types.ts ADDED
@@ -0,0 +1,75 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'
2
+
3
+ export type LogContext = Record<string, unknown>
4
+
5
+ export type LogRecord = {
6
+ level: LogLevel
7
+ message: string
8
+ timestamp: string
9
+ context: LogContext
10
+ error?: { name: string; message: string; stack?: string }
11
+ }
12
+
13
+ export type Formatter = {
14
+ format: (record: LogRecord) => Record<string, unknown>
15
+ messageKey: string
16
+ }
17
+
18
+ export type LoggerOptions = {
19
+ formatter?: Formatter
20
+ json?: boolean
21
+ level?: LogLevel
22
+ }
23
+
24
+ export type InterceptOptions = {
25
+ formatter?: Formatter
26
+ filter?: (level: LogLevel, message: string) => boolean
27
+ level?: LogLevel
28
+ }
29
+
30
+ export const logLevelValues: Record<LogLevel, number> = {
31
+ debug: 0,
32
+ info: 1,
33
+ warn: 2,
34
+ error: 3,
35
+ fatal: 4,
36
+ }
37
+
38
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: needed to strip ANSI color codes
39
+ const ansiPattern = /\x1b\[[0-9;]*m/g
40
+
41
+ export function stripAnsi(str: string): string {
42
+ return str.replace(ansiPattern, '')
43
+ }
44
+
45
+ /**
46
+ * Detect runtime once, resolve streams at call time.
47
+ * Stream lookup is deferred so tests/runtime can replace process.stdout.
48
+ */
49
+ const runtime: 'node' | 'deno' | 'browser' =
50
+ typeof process !== 'undefined' &&
51
+ process.stdout &&
52
+ typeof process.stdout.write === 'function'
53
+ ? 'node'
54
+ : // @ts-expect-error Deno global
55
+ typeof Deno !== 'undefined' && Deno.stdout
56
+ ? 'deno'
57
+ : 'browser'
58
+
59
+ const denoEncoder = runtime === 'deno' ? new TextEncoder() : null
60
+
61
+ export function write(data: string, isError: boolean): void {
62
+ if (runtime === 'node') {
63
+ const stream = isError ? (process.stderr ?? process.stdout) : process.stdout
64
+ stream.write(`${data}\n`)
65
+ } else if (runtime === 'deno' && denoEncoder) {
66
+ const bytes = denoEncoder.encode(`${data}\n`)
67
+ // @ts-expect-error Deno global
68
+ if (isError) Deno.stderr.writeSync(bytes)
69
+ // @ts-expect-error Deno global
70
+ else Deno.stdout.writeSync(bytes)
71
+ } else {
72
+ if (isError) console.error(data)
73
+ else console.log(data)
74
+ }
75
+ }
@@ -0,0 +1,19 @@
1
+ import type { Formatter, LogRecord } from './types'
2
+
3
+ export const victoriaLogs: Formatter = {
4
+ messageKey: '_msg',
5
+ format(record: LogRecord): Record<string, unknown> {
6
+ const entry: Record<string, unknown> = {
7
+ _msg: record.message,
8
+ _time: record.timestamp,
9
+ level: record.level,
10
+ ...record.context,
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
+ return entry
18
+ },
19
+ }