pino-nice 0.0.1
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 +99 -0
- package/dist/pino-nice.d.ts +16 -0
- package/dist/pino-nice.js +380 -0
- package/package.json +60 -0
- package/pino-nice.png +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tim Zadorozhny
|
|
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,99 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="pino-nice.png" alt="pino-nice" width="100%" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# pino-nice
|
|
6
|
+
|
|
7
|
+
โจ Pretty-print [pino](https://github.com/pinojs/pino) (and pino-like) JSON logs from stdin into a clean, human-friendly streaming view.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
node app.js | npx pino-nice
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## ๐ค Why pino-nice?
|
|
14
|
+
|
|
15
|
+
Structured JSON logs are great for machines and terrible for human eyes. `pino-pretty` helps, but pino-nice takes a different, opinionated approach focused on readability while tailing logs:
|
|
16
|
+
|
|
17
|
+
- ๐งพ **Sectioned details + headline.** Each record renders its structured fields as an indented, YAML-like block, connected with box-drawing lines (`โ` / `โโ`) down to a single headline `LEVEL timestamp message`.
|
|
18
|
+
- ๐จ **Color-coded levels.** `trace`/`debug`/`info`/`warn`/`error`/`fatal` each get a distinct color, applied to the connectors too, so severity pops at a glance.
|
|
19
|
+
- ๐งจ **First-class errors.** Stack traces are cleaned up, internal `node_modules`/`node:` frames are dimmed, and nested `cause` chains and `AggregateError`s are rendered with their structure intact.
|
|
20
|
+
- ๐ **Works with more than pino.** Recognizes pino defaults plus common aliases like `severity` / `message` / `timestamp`, so logs from other services format nicely too.
|
|
21
|
+
- ๐ **Readable timestamps.** Local time by default (or `--utc`), with a missing timestamp filled in from the current time.
|
|
22
|
+
- ๐ **URL-safe wrapping.** Lines flow to your terminal's width with no manual mid-token breaks, so long URLs stay clickable.
|
|
23
|
+
- ๐ **Bun & Node, zero runtime deps.** Ships as a tiny dependency-free CLI.
|
|
24
|
+
|
|
25
|
+
## ๐ฆ Install
|
|
26
|
+
|
|
27
|
+
Global install:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g pino-nice
|
|
31
|
+
# or
|
|
32
|
+
bun add -g pino-nice
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or run it without installing:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx pino-nice
|
|
39
|
+
# or
|
|
40
|
+
bunx pino-nice
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## ๐ ๏ธ Usage
|
|
44
|
+
|
|
45
|
+
Pipe any stream of pino/NDJSON logs into `pino-nice`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# installed globally
|
|
49
|
+
node app.js | pino-nice
|
|
50
|
+
|
|
51
|
+
# without installing
|
|
52
|
+
node app.js | npx pino-nice --warn
|
|
53
|
+
bun run app.ts | bunx pino-nice --utc
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Filter by minimum level and choose a timezone:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# only warnings and above, timestamps in UTC
|
|
60
|
+
node app.js | pino-nice --warn --utc
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## โ๏ธ Options
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
pino-nice - pretty-print pino (and pino-like) JSON logs from stdin
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
<command emitting json logs> | pino-nice [options]
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
--utc Print timestamps in UTC. Default: local time.
|
|
73
|
+
--trace Show trace and above (all levels).
|
|
74
|
+
--debug Show debug and above.
|
|
75
|
+
--info Show info and above.
|
|
76
|
+
--warn Show warnings and above.
|
|
77
|
+
--error Show errors and above.
|
|
78
|
+
--fatal Show fatal only.
|
|
79
|
+
-h, --help Show this help and exit.
|
|
80
|
+
|
|
81
|
+
Notes:
|
|
82
|
+
Level flags filter by minimum severity; if several are given, the last wins.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If you pass several level flags, the **last one wins** (e.g. `--error --info` shows info and above).
|
|
86
|
+
|
|
87
|
+
## ๐งฉ Supported log shapes
|
|
88
|
+
|
|
89
|
+
pino-nice resolves fields from common conventions:
|
|
90
|
+
|
|
91
|
+
- **Level:** `level` (numeric `10`โ`60`) or `severity` (`"INFO"`, `"DEBUG"`, ...)
|
|
92
|
+
- **Message:** `msg` or `message`
|
|
93
|
+
- **Timestamp:** `time` or `timestamp` (epoch ms or ISO string); if absent, the current time is used
|
|
94
|
+
|
|
95
|
+
All six pino levels are supported: `trace` (10), `debug` (20), `info` (30), `warn` (40), `error` (50), `fatal` (60).
|
|
96
|
+
|
|
97
|
+
## ๐ License
|
|
98
|
+
|
|
99
|
+
[MIT](LICENSE) ยฉ Tim Zadorozhny
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
interface PinoNiceOptions {
|
|
3
|
+
/** Print timestamps in UTC. Defaults to local time. */
|
|
4
|
+
utc?: boolean;
|
|
5
|
+
/** Minimum numeric level to display; lower levels are hidden. Defaults to 0. */
|
|
6
|
+
minLevel?: number;
|
|
7
|
+
}
|
|
8
|
+
declare class PinoNice {
|
|
9
|
+
private readonly utc;
|
|
10
|
+
private readonly minLevel;
|
|
11
|
+
constructor(options?: PinoNiceOptions);
|
|
12
|
+
private handleLine;
|
|
13
|
+
pipe(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { type PinoNiceOptions, PinoNice as default };
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// pino-nice.ts
|
|
4
|
+
import process from "process";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
var colorsEnabled = (() => {
|
|
7
|
+
if (process.env.NO_COLOR) return false;
|
|
8
|
+
const force = process.env.FORCE_COLOR;
|
|
9
|
+
if (force !== void 0 && force !== "" && force !== "0") return true;
|
|
10
|
+
if (typeof Bun !== "undefined") return Bun.enableANSIColors;
|
|
11
|
+
return Boolean(process.stdout.isTTY);
|
|
12
|
+
})();
|
|
13
|
+
function sgr(open, close) {
|
|
14
|
+
return (s) => colorsEnabled ? `\x1B[${open}m${s}\x1B[${close}m` : s;
|
|
15
|
+
}
|
|
16
|
+
var c = {
|
|
17
|
+
bold: sgr(1, 22),
|
|
18
|
+
dim: sgr(2, 22),
|
|
19
|
+
red: sgr(31, 39),
|
|
20
|
+
green: sgr(32, 39),
|
|
21
|
+
yellow: sgr(33, 39),
|
|
22
|
+
blue: sgr(34, 39),
|
|
23
|
+
magenta: sgr(35, 39),
|
|
24
|
+
cyan: sgr(36, 39),
|
|
25
|
+
gray: sgr(90, 39)
|
|
26
|
+
};
|
|
27
|
+
var BOX = {
|
|
28
|
+
vertical: "\u2502",
|
|
29
|
+
// โ
|
|
30
|
+
corner: "\u2514",
|
|
31
|
+
// โ
|
|
32
|
+
horizontal: "\u2500"
|
|
33
|
+
// โ
|
|
34
|
+
};
|
|
35
|
+
var LEVELS = {
|
|
36
|
+
10: { label: "TRACE", color: c.gray },
|
|
37
|
+
20: { label: "DEBUG", color: c.cyan },
|
|
38
|
+
30: { label: "INFO", color: c.green },
|
|
39
|
+
40: { label: "WARN", color: c.yellow },
|
|
40
|
+
50: { label: "ERROR", color: c.red },
|
|
41
|
+
60: { label: "FATAL", color: (s) => c.bold(c.red(s)) }
|
|
42
|
+
};
|
|
43
|
+
var LEVEL_WIDTH = 5;
|
|
44
|
+
var LABEL_TO_LEVEL = Object.fromEntries(
|
|
45
|
+
Object.entries(LEVELS).map(([num, info]) => [info.label, Number(num)])
|
|
46
|
+
);
|
|
47
|
+
function levelToNumber(level) {
|
|
48
|
+
if (typeof level === "number") return level;
|
|
49
|
+
if (typeof level === "string") {
|
|
50
|
+
const upper = level.toUpperCase();
|
|
51
|
+
const known = LABEL_TO_LEVEL[upper];
|
|
52
|
+
if (known !== void 0) return known;
|
|
53
|
+
const n = Number(level);
|
|
54
|
+
if (!Number.isNaN(n)) return n;
|
|
55
|
+
}
|
|
56
|
+
return Number.POSITIVE_INFINITY;
|
|
57
|
+
}
|
|
58
|
+
var SPECIAL_FIELDS = /* @__PURE__ */ new Set([
|
|
59
|
+
"time",
|
|
60
|
+
"timestamp",
|
|
61
|
+
"level",
|
|
62
|
+
"severity",
|
|
63
|
+
"msg",
|
|
64
|
+
"message",
|
|
65
|
+
"pid",
|
|
66
|
+
"hostname",
|
|
67
|
+
"v"
|
|
68
|
+
]);
|
|
69
|
+
var TIME_KEYS = ["time", "timestamp"];
|
|
70
|
+
var LEVEL_KEYS = ["level", "severity"];
|
|
71
|
+
var MESSAGE_KEYS = ["msg", "message"];
|
|
72
|
+
function firstDefined(entry, keys) {
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
if (entry[key] !== void 0 && entry[key] !== null) return entry[key];
|
|
75
|
+
}
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
var IMPORTANT_KEYS = /* @__PURE__ */ new Set([
|
|
79
|
+
"reqId",
|
|
80
|
+
"requestId",
|
|
81
|
+
"traceId",
|
|
82
|
+
"spanId",
|
|
83
|
+
"userId"
|
|
84
|
+
]);
|
|
85
|
+
function isPlainObject(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
function isErrorLike(value) {
|
|
89
|
+
return isPlainObject(value) && typeof value.message === "string" && typeof value.stack === "string";
|
|
90
|
+
}
|
|
91
|
+
function highlightKey(key) {
|
|
92
|
+
return IMPORTANT_KEYS.has(key) ? c.bold(c.cyan(key)) : c.cyan(key);
|
|
93
|
+
}
|
|
94
|
+
function formatScalar(value) {
|
|
95
|
+
if (value === null) return c.dim("null");
|
|
96
|
+
if (value === void 0) return c.dim("undefined");
|
|
97
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
98
|
+
return c.yellow(String(value));
|
|
99
|
+
}
|
|
100
|
+
return String(value);
|
|
101
|
+
}
|
|
102
|
+
function errorHeader(err) {
|
|
103
|
+
const type = err.type ?? "Error";
|
|
104
|
+
const message = err.message ?? "";
|
|
105
|
+
return c.bold(c.red(`${type}: ${message}`));
|
|
106
|
+
}
|
|
107
|
+
function renderStackFrames(stack, indent) {
|
|
108
|
+
const pad = " ".repeat(indent);
|
|
109
|
+
const out = [];
|
|
110
|
+
const lines = stack.split("\n");
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
const line = (lines[i] ?? "").trim();
|
|
113
|
+
if (line === "") continue;
|
|
114
|
+
if (i === 0 && !line.startsWith("at ")) continue;
|
|
115
|
+
if (line.startsWith("at ")) {
|
|
116
|
+
const internal = line.includes("node_modules") || line.includes("node:");
|
|
117
|
+
if (internal) {
|
|
118
|
+
out.push(pad + c.dim(line));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const m = line.match(/^at (?:(.+?) \()?(.+?):(\d+):(\d+)\)?$/);
|
|
122
|
+
if (m && m[2] && m[3] && m[4]) {
|
|
123
|
+
const fn = m[1];
|
|
124
|
+
const loc = `${c.cyan(m[2])}:${c.yellow(m[3])}:${c.yellow(m[4])}`;
|
|
125
|
+
out.push(
|
|
126
|
+
pad + (fn ? `${c.dim("at")} ${fn} (${loc})` : `${c.dim("at")} ${loc}`)
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
out.push(pad + c.dim(line));
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (line.startsWith("caused by:")) {
|
|
134
|
+
const rest = line.slice("caused by:".length).trim();
|
|
135
|
+
out.push(pad + c.dim("caused by:") + " " + c.bold(c.red(rest)));
|
|
136
|
+
} else {
|
|
137
|
+
out.push(pad + c.dim(line));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
function renderErrorBody(err, indent, depth) {
|
|
143
|
+
if (depth > 6) return [];
|
|
144
|
+
const pad = " ".repeat(indent);
|
|
145
|
+
const out = [];
|
|
146
|
+
if (typeof err.stack === "string") {
|
|
147
|
+
out.push(...renderStackFrames(err.stack, indent));
|
|
148
|
+
}
|
|
149
|
+
const aggregate = err.aggregateErrors ?? err.errors;
|
|
150
|
+
if (Array.isArray(aggregate)) {
|
|
151
|
+
for (const sub of aggregate) {
|
|
152
|
+
if (isErrorLike(sub)) {
|
|
153
|
+
out.push(`${pad}${c.dim("-")} ${errorHeader(sub)}`);
|
|
154
|
+
out.push(...renderErrorBody(sub, indent + 2, depth + 1));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (isErrorLike(err.cause)) {
|
|
159
|
+
out.push(`${pad}${c.dim("caused by:")} ${errorHeader(err.cause)}`);
|
|
160
|
+
out.push(...renderErrorBody(err.cause, indent + 2, depth + 1));
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
function renderData(value, indent, depth = 0) {
|
|
165
|
+
const pad = " ".repeat(indent);
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
if (value.length === 0) return [`${pad}${c.dim("[]")}`];
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const item of value) {
|
|
170
|
+
if (isErrorLike(item)) {
|
|
171
|
+
out.push(`${pad}${c.dim("-")} ${errorHeader(item)}`);
|
|
172
|
+
out.push(...renderErrorBody(item, indent + 2, depth + 1));
|
|
173
|
+
} else if (isPlainObject(item) || Array.isArray(item)) {
|
|
174
|
+
const nested = renderData(item, indent + 2, depth + 1);
|
|
175
|
+
const first = nested[0];
|
|
176
|
+
if (first !== void 0) {
|
|
177
|
+
out.push(`${pad}${c.dim("-")} ${first.slice(indent + 2)}`);
|
|
178
|
+
out.push(...nested.slice(1));
|
|
179
|
+
} else {
|
|
180
|
+
out.push(`${pad}${c.dim("-")}`);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
out.push(`${pad}${c.dim("-")} ${formatScalar(item)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
if (isPlainObject(value)) {
|
|
189
|
+
const entries = Object.entries(value);
|
|
190
|
+
if (entries.length === 0) return [`${pad}${c.dim("{}")}`];
|
|
191
|
+
const out = [];
|
|
192
|
+
for (const [key, val] of entries) {
|
|
193
|
+
const keyStr = highlightKey(key);
|
|
194
|
+
if (isErrorLike(val)) {
|
|
195
|
+
out.push(`${pad}${keyStr}: ${errorHeader(val)}`);
|
|
196
|
+
out.push(...renderErrorBody(val, indent + 2, depth + 1));
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(val) || isPlainObject(val)) {
|
|
200
|
+
const empty = Array.isArray(val) ? val.length === 0 ? "[]" : null : Object.keys(val).length === 0 ? "{}" : null;
|
|
201
|
+
if (empty !== null) {
|
|
202
|
+
out.push(`${pad}${keyStr}: ${c.dim(empty)}`);
|
|
203
|
+
} else {
|
|
204
|
+
out.push(`${pad}${keyStr}:`);
|
|
205
|
+
out.push(...renderData(val, indent + 2, depth + 1));
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (typeof val === "string" && val.includes("\n")) {
|
|
210
|
+
out.push(`${pad}${keyStr}: ${c.dim("|")}`);
|
|
211
|
+
for (const line of val.split("\n")) {
|
|
212
|
+
out.push(`${" ".repeat(indent + 2)}${line}`);
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
out.push(`${pad}${keyStr}: ${formatScalar(val)}`);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
return [`${pad}${formatScalar(value)}`];
|
|
221
|
+
}
|
|
222
|
+
function toDate(time) {
|
|
223
|
+
if (typeof time === "number") {
|
|
224
|
+
const d = new Date(time);
|
|
225
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
226
|
+
}
|
|
227
|
+
if (typeof time === "string") {
|
|
228
|
+
const d = new Date(/^\d+$/.test(time) ? Number(time) : time);
|
|
229
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
function formatTimestamp(time, utc) {
|
|
234
|
+
const date = toDate(time);
|
|
235
|
+
if (!date) return typeof time === "string" ? time : String(time);
|
|
236
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
237
|
+
const datePart = utc ? `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}` : `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
238
|
+
const clock = utc ? `${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}` : `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
239
|
+
const millis = pad(
|
|
240
|
+
utc ? date.getUTCMilliseconds() : date.getMilliseconds(),
|
|
241
|
+
3
|
|
242
|
+
);
|
|
243
|
+
return `${c.dim(datePart)} ${c.bold(clock)}${c.dim(`.${millis}`)}${utc ? c.dim("Z") : ""}`;
|
|
244
|
+
}
|
|
245
|
+
function levelInfo(level) {
|
|
246
|
+
if (typeof level === "number" && LEVELS[level]) return LEVELS[level];
|
|
247
|
+
if (typeof level === "string") {
|
|
248
|
+
const upper = level.toUpperCase();
|
|
249
|
+
for (const info of Object.values(LEVELS)) {
|
|
250
|
+
if (info.label === upper) return info;
|
|
251
|
+
}
|
|
252
|
+
return { label: upper, color: (s) => s };
|
|
253
|
+
}
|
|
254
|
+
return { label: String(level), color: (s) => s };
|
|
255
|
+
}
|
|
256
|
+
function summaryLine(time, level, msg, utc) {
|
|
257
|
+
const info = levelInfo(level);
|
|
258
|
+
const paddedLabel = info.label.padEnd(LEVEL_WIDTH);
|
|
259
|
+
const hasTime = time !== void 0 && time !== null;
|
|
260
|
+
const tsPart = hasTime ? `${formatTimestamp(time, utc)} ` : "";
|
|
261
|
+
const connector = `${BOX.corner}${BOX.horizontal} `;
|
|
262
|
+
const message = typeof msg === "string" ? msg : String(msg);
|
|
263
|
+
return `${info.color(connector)}${info.color(paddedLabel)} ${tsPart}${message}`.trimEnd();
|
|
264
|
+
}
|
|
265
|
+
var PinoNice = class {
|
|
266
|
+
utc;
|
|
267
|
+
minLevel;
|
|
268
|
+
constructor(options = {}) {
|
|
269
|
+
this.utc = options.utc ?? false;
|
|
270
|
+
this.minLevel = options.minLevel ?? 0;
|
|
271
|
+
}
|
|
272
|
+
handleLine(rawLine) {
|
|
273
|
+
const line = rawLine.replace(/\r$/, "");
|
|
274
|
+
if (line.trim() === "") return;
|
|
275
|
+
let entry;
|
|
276
|
+
try {
|
|
277
|
+
entry = JSON.parse(line);
|
|
278
|
+
} catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!isPlainObject(entry)) return;
|
|
282
|
+
const level = firstDefined(entry, LEVEL_KEYS);
|
|
283
|
+
if (level === void 0) return;
|
|
284
|
+
const message = firstDefined(entry, MESSAGE_KEYS) ?? "";
|
|
285
|
+
const time = firstDefined(entry, TIME_KEYS) ?? Date.now();
|
|
286
|
+
if (levelToNumber(level) < this.minLevel) return;
|
|
287
|
+
const rest = {};
|
|
288
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
289
|
+
if (!SPECIAL_FIELDS.has(key)) rest[key] = value;
|
|
290
|
+
}
|
|
291
|
+
const info = levelInfo(level);
|
|
292
|
+
const out = [];
|
|
293
|
+
const dataLines = Object.keys(rest).length > 0 ? renderData(rest, 1) : [];
|
|
294
|
+
for (const dataLine of dataLines) {
|
|
295
|
+
out.push(info.color(BOX.vertical) + " " + dataLine);
|
|
296
|
+
}
|
|
297
|
+
out.push(summaryLine(time, level, message, this.utc));
|
|
298
|
+
try {
|
|
299
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error?.code === "EPIPE") process.exit(0);
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async pipe() {
|
|
306
|
+
const decoder = new TextDecoder();
|
|
307
|
+
let buffer = "";
|
|
308
|
+
for await (const chunk of process.stdin) {
|
|
309
|
+
buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
310
|
+
let nl;
|
|
311
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
312
|
+
const line = buffer.slice(0, nl);
|
|
313
|
+
buffer = buffer.slice(nl + 1);
|
|
314
|
+
this.handleLine(line);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
buffer += decoder.decode();
|
|
318
|
+
if (buffer.length > 0) this.handleLine(buffer);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
var LEVEL_FLAGS = {
|
|
322
|
+
"--trace": 10,
|
|
323
|
+
"--debug": 20,
|
|
324
|
+
"--info": 30,
|
|
325
|
+
"--warn": 40,
|
|
326
|
+
"--error": 50,
|
|
327
|
+
"--fatal": 60
|
|
328
|
+
};
|
|
329
|
+
var HELP = `pino-nice - pretty-print pino (and pino-like) JSON logs from stdin
|
|
330
|
+
|
|
331
|
+
Usage:
|
|
332
|
+
<command emitting json logs> | pino-nice [options]
|
|
333
|
+
|
|
334
|
+
Options:
|
|
335
|
+
--utc Print timestamps in UTC. Default: local time.
|
|
336
|
+
--trace Show trace and above (all levels).
|
|
337
|
+
--debug Show debug and above.
|
|
338
|
+
--info Show info and above.
|
|
339
|
+
--warn Show warnings and above.
|
|
340
|
+
--error Show errors and above.
|
|
341
|
+
--fatal Show fatal only.
|
|
342
|
+
-h, --help Show this help and exit.
|
|
343
|
+
|
|
344
|
+
Notes:
|
|
345
|
+
Level flags filter by minimum severity; if several are given, the last wins.
|
|
346
|
+
`;
|
|
347
|
+
function parseArgs(argv) {
|
|
348
|
+
const options = {};
|
|
349
|
+
for (const arg of argv) {
|
|
350
|
+
if (arg === "--help" || arg === "-h") {
|
|
351
|
+
process.stdout.write(HELP);
|
|
352
|
+
process.exit(0);
|
|
353
|
+
} else if (arg === "--utc") {
|
|
354
|
+
options.utc = true;
|
|
355
|
+
} else if (LEVEL_FLAGS[arg] !== void 0) {
|
|
356
|
+
options.minLevel = LEVEL_FLAGS[arg];
|
|
357
|
+
} else {
|
|
358
|
+
process.stderr.write(`pino-nice: unknown option '${arg}' (try --help)
|
|
359
|
+
`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return options;
|
|
364
|
+
}
|
|
365
|
+
var isMainModule = import.meta.main ?? (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href);
|
|
366
|
+
if (isMainModule) {
|
|
367
|
+
process.stdout.on("error", (error) => {
|
|
368
|
+
if (error.code === "EPIPE") process.exit(0);
|
|
369
|
+
throw error;
|
|
370
|
+
});
|
|
371
|
+
const options = parseArgs(process.argv.slice(2));
|
|
372
|
+
new PinoNice(options).pipe().catch((error) => {
|
|
373
|
+
if (error?.code === "EPIPE") process.exit(0);
|
|
374
|
+
console.error(error);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
export {
|
|
379
|
+
PinoNice as default
|
|
380
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pino-nice",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Pretty-print pino (and pino-like) JSON logs from stdin with a clean, human-friendly streaming view.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Tim Zadorozhny <tzador@gmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"bin": {
|
|
9
|
+
"pino-nice": "./dist/pino-nice.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/pino-nice.js",
|
|
12
|
+
"module": "./dist/pino-nice.js",
|
|
13
|
+
"types": "./dist/pino-nice.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/pino-nice.d.ts",
|
|
17
|
+
"import": "./dist/pino-nice.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"pino-nice.png"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"pino",
|
|
30
|
+
"pretty",
|
|
31
|
+
"logger",
|
|
32
|
+
"cli",
|
|
33
|
+
"logs",
|
|
34
|
+
"formatter",
|
|
35
|
+
"ndjson"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/tzador/pino-nice.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/tzador/pino-nice#readme",
|
|
42
|
+
"bugs": "https://github.com/tzador/pino-nice/issues",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"prepublishOnly": "bun run build",
|
|
49
|
+
"release": "bun run build && npm publish",
|
|
50
|
+
"demo": "bun run pino-nice-demo.ts | bun run pino-nice.ts",
|
|
51
|
+
"format": "prettier --write ."
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "latest",
|
|
55
|
+
"pino": "^10.3.1",
|
|
56
|
+
"prettier": "^3.8.3",
|
|
57
|
+
"tsup": "^8.5.1",
|
|
58
|
+
"typescript": "^6.0.3"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/pino-nice.png
ADDED
|
Binary file
|