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 +21 -0
- package/README.md +127 -0
- package/dist/google-cloud.d.ts +2 -0
- package/dist/google-cloud.js +1 -0
- package/dist/index-abhqk8y0.js +4 -0
- package/dist/index-rhgqp0zd.js +2 -0
- package/dist/index-rxqh9v58.js +2 -0
- package/dist/index-x1ss6ry6.js +2 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +1 -0
- package/dist/intercept.d.ts +11 -0
- package/dist/intercept.js +1 -0
- package/dist/preload.d.ts +1 -0
- package/dist/preload.js +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/victoria-logs.d.ts +2 -0
- package/dist/victoria-logs.js +1 -0
- package/package.json +72 -0
- package/src/google-cloud.ts +27 -0
- package/src/index.ts +148 -0
- package/src/intercept.ts +136 -0
- package/src/preload.ts +17 -0
- package/src/types.ts +75 -0
- package/src/victoria-logs.ts +19 -0
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 @@
|
|
|
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
|
+
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};
|
package/dist/index.d.ts
ADDED
|
@@ -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 {};
|
package/dist/preload.js
ADDED
|
@@ -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});
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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()
|
package/src/intercept.ts
ADDED
|
@@ -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
|
+
}
|