@ragib.mrb/logsift 0.0.0-stage → 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 +111 -2
- package/dist/chunk-QR2OWMYF.js +714 -0
- package/dist/chunk-QR2OWMYF.js.map +1 -0
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ragib MRB
|
|
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
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
#
|
|
1
|
+
# logsift
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Search many log files as **one timeline**. Any format, real filters, and one flag to follow a single request across all your services.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @ragib.mrb/logsift logs/ --request req-2
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
11-05 13:23:05.000 INFO api.log request start reqId=req-2 method=POST url=/orders
|
|
11
|
+
11-05 13:23:05.100 INFO gateway.log proxy request_id=req-2 path=/orders status=502
|
|
12
|
+
11-05 13:23:05.500 DEBUG worker.log picked job for request req-2
|
|
13
|
+
11-05 13:23:06.000 ERROR worker.log charge failed for req-2
|
|
14
|
+
Error: card declined
|
|
15
|
+
at charge (/app/billing.js:42:11)
|
|
16
|
+
11-05 13:23:06.500 ERROR api.log payment failed reqId=req-2 status=502
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Three services, three log formats, one story.
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
- `grep` sees text, not time: lines from different files come out file by file, not in the order things happened.
|
|
24
|
+
- Pretty-printers like pino-pretty format one stream; they don't merge files or follow a request.
|
|
25
|
+
- The great log tools (lnav, hl) aren't one `npx` away.
|
|
26
|
+
|
|
27
|
+
logsift reads every file you give it, works out each line's format, and merges everything by time as it streams. A million-line, 115 MB file is searched in about 3 seconds.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g @ragib.mrb/logsift # gives you the `logsift` command; or just: npx @ragib.mrb/logsift …
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Needs Node.js 22 or newer. No dependencies.
|
|
36
|
+
|
|
37
|
+
## Use
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
logsift api.log worker.log # several files, merged by time
|
|
41
|
+
logsift logs/ # every log file in a folder (and .gz files)
|
|
42
|
+
logsift "logs/**/*.log" # globs work on Windows too
|
|
43
|
+
kubectl logs my-pod | logsift -l warn # or pipe anything in
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Filters
|
|
47
|
+
|
|
48
|
+
All filters must match.
|
|
49
|
+
|
|
50
|
+
| Option | What it keeps | Example |
|
|
51
|
+
| ----------------------------- | ------------------------------------------------------------------------ | --------------------------------------------- |
|
|
52
|
+
| `-l, --level` | this level and above | `-l warn` |
|
|
53
|
+
| `-s, --since` / `-u, --until` | a time range | `-s 15m`, `-s 14:03`, `-u "2026-11-05 14:30"` |
|
|
54
|
+
| `-g, --grep` | text (any case), `/regex/flags` or `re:regex`; repeat to require several | `-g timeout`, `-g "re:fail\|denied"` |
|
|
55
|
+
| `-w, --where` | field filters: `=`, `!=`, `>`, `>=`, `<`, `<=`, `~` (contains) | `-w "status>=500" -w user.id=42` |
|
|
56
|
+
| `-r, --request` | every line that mentions one request, trace or correlation ID | `-r req-2` |
|
|
57
|
+
| `-a, --around` + `--window` | what happened just before and after a moment | `-a 14:03 --window 30s` |
|
|
58
|
+
|
|
59
|
+
`--request` matches the whole ID only (so `req-42` doesn't match `req-420`). It checks the text of each line and common ID fields such as `reqId`, `requestId`, `traceId`, `correlationId` and `x-request-id`.
|
|
60
|
+
|
|
61
|
+
> **Git Bash on Windows** rewrites arguments that start with `/` into file paths, which breaks `/regex/`. Use `re:regex` instead.
|
|
62
|
+
|
|
63
|
+
### Output
|
|
64
|
+
|
|
65
|
+
| Option | What you get |
|
|
66
|
+
| -------------- | -------------------------------------------------------------------------------- |
|
|
67
|
+
| (default) | readable, coloured lines with the time, level, file and fields |
|
|
68
|
+
| `--json` | one JSON object per line, for `jq` or another tool |
|
|
69
|
+
| `--stats` | counts by level and file, and the most common messages (numbers and IDs grouped) |
|
|
70
|
+
| `-n, --limit` | stop after n matching lines |
|
|
71
|
+
| `-f, --follow` | keep watching the files and print new matching lines |
|
|
72
|
+
| `--no-color` | plain text (also when `NO_COLOR` is set) |
|
|
73
|
+
|
|
74
|
+
Exit code: `0` when something matched, `1` when nothing did, `2` on errors, like `grep`.
|
|
75
|
+
|
|
76
|
+
## Formats it understands
|
|
77
|
+
|
|
78
|
+
| Format | Examples |
|
|
79
|
+
| ---------- | -------------------------------------------------------------------------------------------- |
|
|
80
|
+
| JSON lines | pino, bunyan, winston, Elastic (ECS), most JSON loggers |
|
|
81
|
+
| logfmt | `time=… level=info msg="gateway up" port=8080` |
|
|
82
|
+
| Plain text | `2026-11-05 14:03:01,123 ERROR …`, `[2026-11-05T14:03:01Z] [warn] …`, nginx and syslog times |
|
|
83
|
+
|
|
84
|
+
- Times can be ISO strings, seconds or milliseconds; levels can be words or pino/bunyan numbers.
|
|
85
|
+
- Stack traces and other lines that continue an entry stay attached to it.
|
|
86
|
+
- Lines with no time keep their place right after the line before them.
|
|
87
|
+
- Plain-text times without a time zone are read as your local time.
|
|
88
|
+
|
|
89
|
+
## Use it in code
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import {
|
|
93
|
+
readEntries,
|
|
94
|
+
mergeByTime,
|
|
95
|
+
buildFilter,
|
|
96
|
+
expandInputs,
|
|
97
|
+
labelSources,
|
|
98
|
+
openStream,
|
|
99
|
+
} from '@ragib.mrb/logsift';
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Develop
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm install
|
|
106
|
+
npm test
|
|
107
|
+
npm run build && node dist/cli.js test/fixtures --request req-2
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
3
|
+
|
|
4
|
+
// src/parse.ts
|
|
5
|
+
var TIME_KEYS = ["time", "timestamp", "@timestamp", "ts", "date", "datetime", "t"];
|
|
6
|
+
var LEVEL_KEYS = ["level", "lvl", "severity", "loglevel", "log.level", "@level"];
|
|
7
|
+
var MSG_KEYS = ["msg", "message", "@message", "event", "text"];
|
|
8
|
+
var LEVEL_WORDS = {
|
|
9
|
+
trace: "trace",
|
|
10
|
+
verbose: "trace",
|
|
11
|
+
silly: "trace",
|
|
12
|
+
debug: "debug",
|
|
13
|
+
dbg: "debug",
|
|
14
|
+
info: "info",
|
|
15
|
+
information: "info",
|
|
16
|
+
notice: "info",
|
|
17
|
+
http: "info",
|
|
18
|
+
warn: "warn",
|
|
19
|
+
warning: "warn",
|
|
20
|
+
error: "error",
|
|
21
|
+
err: "error",
|
|
22
|
+
fatal: "fatal",
|
|
23
|
+
critical: "fatal",
|
|
24
|
+
crit: "fatal",
|
|
25
|
+
panic: "fatal",
|
|
26
|
+
alert: "fatal",
|
|
27
|
+
emerg: "fatal"
|
|
28
|
+
};
|
|
29
|
+
function toLevel(value) {
|
|
30
|
+
if (typeof value === "number") {
|
|
31
|
+
if (value >= 60) return "fatal";
|
|
32
|
+
if (value >= 50) return "error";
|
|
33
|
+
if (value >= 40) return "warn";
|
|
34
|
+
if (value >= 30) return "info";
|
|
35
|
+
if (value >= 20) return "debug";
|
|
36
|
+
return "trace";
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
const word = value.trim().toLowerCase();
|
|
40
|
+
if (/^\d+$/.test(word)) return toLevel(Number(word));
|
|
41
|
+
return LEVEL_WORDS[word];
|
|
42
|
+
}
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
var levelRank = (level) => level ? LEVELS.indexOf(level) : -1;
|
|
46
|
+
var MONTHS = {
|
|
47
|
+
jan: 0,
|
|
48
|
+
feb: 1,
|
|
49
|
+
mar: 2,
|
|
50
|
+
apr: 3,
|
|
51
|
+
may: 4,
|
|
52
|
+
jun: 5,
|
|
53
|
+
jul: 6,
|
|
54
|
+
aug: 7,
|
|
55
|
+
sep: 8,
|
|
56
|
+
oct: 9,
|
|
57
|
+
nov: 10,
|
|
58
|
+
dec: 11
|
|
59
|
+
};
|
|
60
|
+
function toTime(value, now = /* @__PURE__ */ new Date()) {
|
|
61
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
62
|
+
return value < 1e11 ? Math.round(value * 1e3) : value;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value !== "string" || value.length < 6) return void 0;
|
|
65
|
+
const s = value.trim();
|
|
66
|
+
if (/^\d{10}(\.\d+)?$/.test(s) || /^\d{13}$/.test(s)) return toTime(Number(s));
|
|
67
|
+
let m = /^(\d{2})\/(\w{3})\/(\d{4}):(\d{2}):(\d{2}):(\d{2})(?:\s*([+-]\d{4}))?/.exec(s);
|
|
68
|
+
if (m) {
|
|
69
|
+
const [, d, mon, y, hh, mm, ss, tz] = m;
|
|
70
|
+
const month = MONTHS[mon.toLowerCase()];
|
|
71
|
+
if (month === void 0) return void 0;
|
|
72
|
+
const iso2 = `${y}-${String(month + 1).padStart(2, "0")}-${d}T${hh}:${mm}:${ss}${tz ? `${tz.slice(0, 3)}:${tz.slice(3)}` : ""}`;
|
|
73
|
+
const t2 = Date.parse(iso2);
|
|
74
|
+
return Number.isNaN(t2) ? void 0 : t2;
|
|
75
|
+
}
|
|
76
|
+
m = /^(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})/.exec(s);
|
|
77
|
+
if (m) {
|
|
78
|
+
const month = MONTHS[m[1].toLowerCase()];
|
|
79
|
+
if (month === void 0) return void 0;
|
|
80
|
+
return new Date(
|
|
81
|
+
now.getFullYear(),
|
|
82
|
+
month,
|
|
83
|
+
Number(m[2]),
|
|
84
|
+
Number(m[3]),
|
|
85
|
+
Number(m[4]),
|
|
86
|
+
Number(m[5])
|
|
87
|
+
).getTime();
|
|
88
|
+
}
|
|
89
|
+
const iso = s.replace(
|
|
90
|
+
/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})[,.]?(\d+)?/,
|
|
91
|
+
(_a, d, t2, frac) => `${d}T${t2}${frac ? `.${String(frac).slice(0, 3).padEnd(3, "0")}` : ""}`
|
|
92
|
+
);
|
|
93
|
+
if (!/^\d{4}-\d{2}-\d{2}/.test(iso)) return void 0;
|
|
94
|
+
const t = Date.parse(iso);
|
|
95
|
+
return Number.isNaN(t) ? void 0 : t;
|
|
96
|
+
}
|
|
97
|
+
function getPath(obj, key) {
|
|
98
|
+
if (key in obj) return obj[key];
|
|
99
|
+
let cur = obj;
|
|
100
|
+
for (const part of key.split(".")) {
|
|
101
|
+
if (cur === null || typeof cur !== "object") return void 0;
|
|
102
|
+
cur = cur[part];
|
|
103
|
+
}
|
|
104
|
+
return cur;
|
|
105
|
+
}
|
|
106
|
+
function fromFields(fields, format, now) {
|
|
107
|
+
const rest = { ...fields };
|
|
108
|
+
const take = (keys, convert) => {
|
|
109
|
+
for (const key of keys) {
|
|
110
|
+
const value = getPath(rest, key);
|
|
111
|
+
const converted = value === void 0 ? void 0 : convert(value);
|
|
112
|
+
if (converted !== void 0) {
|
|
113
|
+
if (key in rest) delete rest[key];
|
|
114
|
+
return converted;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return void 0;
|
|
118
|
+
};
|
|
119
|
+
const time = take(TIME_KEYS, (v) => toTime(v, now));
|
|
120
|
+
const level = take(LEVEL_KEYS, toLevel);
|
|
121
|
+
const msg = take(MSG_KEYS, (v) => typeof v === "string" ? v : void 0);
|
|
122
|
+
return { time, level, msg: msg ?? "", fields: rest, format };
|
|
123
|
+
}
|
|
124
|
+
function parseLogfmt(line) {
|
|
125
|
+
const fields = {};
|
|
126
|
+
const re = /([\w.@-]+)=("(?:[^"\\]|\\.)*"|\S*)/g;
|
|
127
|
+
let count = 0;
|
|
128
|
+
let covered = 0;
|
|
129
|
+
for (let m = re.exec(line); m; m = re.exec(line)) {
|
|
130
|
+
let value = m[2];
|
|
131
|
+
if (value.startsWith('"')) value = value.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
132
|
+
fields[m[1]] = value;
|
|
133
|
+
count++;
|
|
134
|
+
covered += m[0].length;
|
|
135
|
+
}
|
|
136
|
+
const known = Object.keys(fields).some((k) => [...LEVEL_KEYS, ...MSG_KEYS, ...TIME_KEYS].includes(k));
|
|
137
|
+
if (count < 2 || !known || covered < line.replace(/\s/g, "").length * 0.6) return void 0;
|
|
138
|
+
return fields;
|
|
139
|
+
}
|
|
140
|
+
var TEXT_TIME = /^\[?((?:\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?)|(?:\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2}(?:\s[+-]\d{4})?)|(?:\w{3}\s+\d{1,2}\s\d{2}:\d{2}:\d{2}))\]?\s*/;
|
|
141
|
+
var TEXT_LEVEL = /^\[?(trace|debug|dbg|info|notice|warn|warning|error|err|fatal|critical|crit|panic)\]?[:\s-]+/i;
|
|
142
|
+
var ANY_LEVEL = /\b(TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|FATAL|CRITICAL)\b/;
|
|
143
|
+
function parseText(line, now) {
|
|
144
|
+
let rest = line;
|
|
145
|
+
let time;
|
|
146
|
+
const tm = TEXT_TIME.exec(rest);
|
|
147
|
+
if (tm) {
|
|
148
|
+
time = toTime(tm[1], now);
|
|
149
|
+
rest = rest.slice(tm[0].length);
|
|
150
|
+
}
|
|
151
|
+
let level;
|
|
152
|
+
const lm = TEXT_LEVEL.exec(rest);
|
|
153
|
+
if (lm) {
|
|
154
|
+
level = toLevel(lm[1]);
|
|
155
|
+
rest = rest.slice(lm[0].length);
|
|
156
|
+
} else {
|
|
157
|
+
const any = ANY_LEVEL.exec(rest.slice(0, 60));
|
|
158
|
+
if (any) level = toLevel(any[1]);
|
|
159
|
+
}
|
|
160
|
+
return { time, level, msg: rest.trim(), fields: {}, format: "text", hasTime: time !== void 0 };
|
|
161
|
+
}
|
|
162
|
+
function parseLine(line, now = /* @__PURE__ */ new Date()) {
|
|
163
|
+
if (line.trim() === "") return void 0;
|
|
164
|
+
const trimmed = line.trimStart();
|
|
165
|
+
if (trimmed.startsWith("{")) {
|
|
166
|
+
try {
|
|
167
|
+
const obj = JSON.parse(trimmed);
|
|
168
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
169
|
+
return { ...fromFields(obj, "json", now), continuation: false };
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const lf = parseLogfmt(line);
|
|
175
|
+
if (lf) return { ...fromFields(lf, "logfmt", now), continuation: false };
|
|
176
|
+
const text = parseText(line, now);
|
|
177
|
+
const looksLikeContinuation = !text.hasTime && (/^\s+/.test(line) || /^(at |Caused by|\.\.\. \d+ more|Traceback|\})/.test(trimmed));
|
|
178
|
+
const { hasTime: _hasTime, ...parsed } = text;
|
|
179
|
+
return { ...parsed, continuation: looksLikeContinuation };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/filter.ts
|
|
183
|
+
var REQUEST_ID_KEYS = [
|
|
184
|
+
"reqId",
|
|
185
|
+
"req_id",
|
|
186
|
+
"requestId",
|
|
187
|
+
"request_id",
|
|
188
|
+
"request-id",
|
|
189
|
+
"x-request-id",
|
|
190
|
+
"X-Request-Id",
|
|
191
|
+
"traceId",
|
|
192
|
+
"trace_id",
|
|
193
|
+
"trace.id",
|
|
194
|
+
"traceID",
|
|
195
|
+
"correlationId",
|
|
196
|
+
"correlation_id",
|
|
197
|
+
"x-correlation-id",
|
|
198
|
+
"spanId",
|
|
199
|
+
"span_id",
|
|
200
|
+
"req.id",
|
|
201
|
+
"request.id",
|
|
202
|
+
"rid"
|
|
203
|
+
];
|
|
204
|
+
var UNITS = {
|
|
205
|
+
ms: 1,
|
|
206
|
+
s: 1e3,
|
|
207
|
+
m: 6e4,
|
|
208
|
+
h: 36e5,
|
|
209
|
+
d: 864e5,
|
|
210
|
+
w: 6048e5
|
|
211
|
+
};
|
|
212
|
+
function parseDuration(text) {
|
|
213
|
+
const m = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)$/i.exec(text.trim());
|
|
214
|
+
return m ? Number(m[1]) * UNITS[m[2].toLowerCase()] : void 0;
|
|
215
|
+
}
|
|
216
|
+
function parseMoment(text, now) {
|
|
217
|
+
const ago = parseDuration(text);
|
|
218
|
+
if (ago !== void 0) return now.getTime() - ago;
|
|
219
|
+
const clock = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(text.trim());
|
|
220
|
+
if (clock) {
|
|
221
|
+
const d = new Date(now);
|
|
222
|
+
d.setHours(Number(clock[1]), Number(clock[2]), Number(clock[3] ?? 0), 0);
|
|
223
|
+
return d.getTime();
|
|
224
|
+
}
|
|
225
|
+
const t = Date.parse(text.trim().replace(" ", "T"));
|
|
226
|
+
if (Number.isNaN(t)) throw new Error(`Can't read the time "${text}". Try 15m, 14:03 or 2026-11-05 14:03.`);
|
|
227
|
+
return t;
|
|
228
|
+
}
|
|
229
|
+
var toRegex = (source, flags) => {
|
|
230
|
+
try {
|
|
231
|
+
return new RegExp(source, flags.replace("g", ""));
|
|
232
|
+
} catch {
|
|
233
|
+
throw new Error(`"${source}" is not a valid regular expression.`);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
function textMatcher(pattern) {
|
|
237
|
+
const slashed = /^\/(.+)\/([a-z]*)$/.exec(pattern);
|
|
238
|
+
if (slashed || pattern.startsWith("re:")) {
|
|
239
|
+
const regex = slashed ? toRegex(slashed[1], slashed[2]) : toRegex(pattern.slice(3), "i");
|
|
240
|
+
return (s) => regex.test(s);
|
|
241
|
+
}
|
|
242
|
+
const needle = pattern.toLowerCase();
|
|
243
|
+
return (s) => s.toLowerCase().includes(needle);
|
|
244
|
+
}
|
|
245
|
+
function fieldValue(entry, key) {
|
|
246
|
+
if (key === "msg" || key === "message") return entry.msg;
|
|
247
|
+
if (key === "level") return entry.level;
|
|
248
|
+
if (key === "source" || key === "file") return entry.source;
|
|
249
|
+
if (key in entry.fields) return entry.fields[key];
|
|
250
|
+
let cur = entry.fields;
|
|
251
|
+
for (const part of key.split(".")) {
|
|
252
|
+
if (cur === null || typeof cur !== "object") return void 0;
|
|
253
|
+
cur = cur[part];
|
|
254
|
+
}
|
|
255
|
+
return cur;
|
|
256
|
+
}
|
|
257
|
+
function parseWhere(expr) {
|
|
258
|
+
const m = /^([\w.@-]+)\s*(!=|>=|<=|=|>|<|~)\s*(.*)$/.exec(expr.trim());
|
|
259
|
+
if (!m) throw new Error(`Can't read the filter "${expr}". Use key=value, key>=500, key~text.`);
|
|
260
|
+
const [, key, op, wanted] = m;
|
|
261
|
+
const asNumber = Number(wanted);
|
|
262
|
+
const numeric = wanted !== "" && !Number.isNaN(asNumber);
|
|
263
|
+
const contains = op === "~" ? textMatcher(wanted) : void 0;
|
|
264
|
+
return (entry) => {
|
|
265
|
+
const value = fieldValue(entry, key);
|
|
266
|
+
if (value === void 0 || value === null) return op === "!=";
|
|
267
|
+
const str = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
268
|
+
switch (op) {
|
|
269
|
+
case "=":
|
|
270
|
+
return str === wanted;
|
|
271
|
+
case "!=":
|
|
272
|
+
return str !== wanted;
|
|
273
|
+
case "~":
|
|
274
|
+
return contains(str);
|
|
275
|
+
default: {
|
|
276
|
+
const n = typeof value === "number" ? value : Number(str);
|
|
277
|
+
if (!numeric || Number.isNaN(n)) return false;
|
|
278
|
+
if (op === ">") return n > asNumber;
|
|
279
|
+
if (op === ">=") return n >= asNumber;
|
|
280
|
+
if (op === "<") return n < asNumber;
|
|
281
|
+
return n <= asNumber;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function buildFilter(options, now = /* @__PURE__ */ new Date()) {
|
|
287
|
+
const tests = [];
|
|
288
|
+
if (options.level) {
|
|
289
|
+
const min = toLevel(options.level);
|
|
290
|
+
if (!min)
|
|
291
|
+
throw new Error(`Unknown level "${options.level}". Use trace, debug, info, warn, error or fatal.`);
|
|
292
|
+
const rank = levelRank(min);
|
|
293
|
+
tests.push((e) => levelRank(e.level) >= rank);
|
|
294
|
+
}
|
|
295
|
+
if (options.since) {
|
|
296
|
+
const from = parseMoment(options.since, now);
|
|
297
|
+
tests.push((e) => e.time !== void 0 && e.time >= from);
|
|
298
|
+
}
|
|
299
|
+
if (options.until) {
|
|
300
|
+
const to = parseMoment(options.until, now);
|
|
301
|
+
tests.push((e) => e.time !== void 0 && e.time <= to);
|
|
302
|
+
}
|
|
303
|
+
if (options.around) {
|
|
304
|
+
const at = parseMoment(options.around, now);
|
|
305
|
+
const span = parseDuration(options.window ?? "1m");
|
|
306
|
+
if (span === void 0) throw new Error(`Can't read the window "${options.window}". Try 30s or 2m.`);
|
|
307
|
+
tests.push((e) => e.time !== void 0 && Math.abs(e.time - at) <= span);
|
|
308
|
+
}
|
|
309
|
+
for (const pattern of options.grep ?? []) {
|
|
310
|
+
const match = textMatcher(pattern);
|
|
311
|
+
tests.push((e) => match(e.raw));
|
|
312
|
+
}
|
|
313
|
+
for (const expr of options.where ?? []) tests.push(parseWhere(expr));
|
|
314
|
+
if (options.request) {
|
|
315
|
+
const id = options.request;
|
|
316
|
+
const whole = new RegExp(`(?<![\\w-])${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`);
|
|
317
|
+
tests.push(
|
|
318
|
+
(e) => (
|
|
319
|
+
// Any match has the ID somewhere in the line, so that cheap check skips most lines.
|
|
320
|
+
e.raw.includes(id) && (whole.test(e.raw) || REQUEST_ID_KEYS.some((k) => String(fieldValue(e, k) ?? "") === id))
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return (e) => tests.every((t) => t(e));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/sources.ts
|
|
328
|
+
import { createReadStream } from "fs";
|
|
329
|
+
import { glob, stat, readdir } from "fs/promises";
|
|
330
|
+
import { basename, join, relative, resolve } from "path";
|
|
331
|
+
import { createInterface } from "readline";
|
|
332
|
+
import { createGunzip } from "zlib";
|
|
333
|
+
var LOG_FILE = /\.(log|txt|json|jsonl|ndjson|out|err)(\.\d+)?(\.gz)?$|\.gz$/i;
|
|
334
|
+
var MAX_ENTRY_CHARS = 64 * 1024;
|
|
335
|
+
async function walk(dir, out) {
|
|
336
|
+
for (const item of await readdir(dir, { withFileTypes: true })) {
|
|
337
|
+
const full = join(dir, item.name);
|
|
338
|
+
if (item.isDirectory()) {
|
|
339
|
+
if (item.name !== "node_modules" && !item.name.startsWith(".")) await walk(full, out);
|
|
340
|
+
} else if (LOG_FILE.test(item.name)) {
|
|
341
|
+
out.push(full);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function expandInputs(inputs, cwd = process.cwd()) {
|
|
346
|
+
const files = /* @__PURE__ */ new Set();
|
|
347
|
+
for (const input of inputs) {
|
|
348
|
+
if (/[*?[\]{}]/.test(input)) {
|
|
349
|
+
for await (const match of glob(input, { cwd })) files.add(resolve(cwd, match));
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const full = resolve(cwd, input);
|
|
353
|
+
const info = await stat(full).catch(() => void 0);
|
|
354
|
+
if (!info) throw new Error(`No such file or folder: ${input}`);
|
|
355
|
+
if (info.isDirectory()) {
|
|
356
|
+
const found = [];
|
|
357
|
+
await walk(full, found);
|
|
358
|
+
found.forEach((f) => files.add(f));
|
|
359
|
+
} else {
|
|
360
|
+
files.add(full);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return [...files].sort();
|
|
364
|
+
}
|
|
365
|
+
function labelSources(paths, cwd = process.cwd()) {
|
|
366
|
+
const counts = /* @__PURE__ */ new Map();
|
|
367
|
+
for (const p of paths) counts.set(basename(p), (counts.get(basename(p)) ?? 0) + 1);
|
|
368
|
+
return paths.map((p) => ({
|
|
369
|
+
path: p,
|
|
370
|
+
name: (counts.get(basename(p)) ?? 0) > 1 ? relative(cwd, p).replace(/\\/g, "/") : basename(p)
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
function openStream(source, stdin = process.stdin) {
|
|
374
|
+
if (!source.path) return stdin;
|
|
375
|
+
const raw = createReadStream(source.path);
|
|
376
|
+
return /\.gz$/i.test(source.path) ? raw.pipe(createGunzip()) : raw;
|
|
377
|
+
}
|
|
378
|
+
async function* readEntries(stream, source, options = {}) {
|
|
379
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
380
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
381
|
+
let pending;
|
|
382
|
+
let pendingHasOwnTime = false;
|
|
383
|
+
let lastTime;
|
|
384
|
+
let lineNo = (options.startLine ?? 1) - 1;
|
|
385
|
+
for await (const line of lines) {
|
|
386
|
+
lineNo++;
|
|
387
|
+
const parsed = parseLine(line, now);
|
|
388
|
+
if (!parsed) continue;
|
|
389
|
+
const untimedTextAfterTimed = parsed.format === "text" && parsed.time === void 0 && pending?.format === "text" && pendingHasOwnTime;
|
|
390
|
+
if (pending && (parsed.continuation || untimedTextAfterTimed)) {
|
|
391
|
+
if (pending.raw.length < MAX_ENTRY_CHARS) pending.raw += `
|
|
392
|
+
${line}`;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (pending) yield pending;
|
|
396
|
+
const { continuation: _c, ...rest } = parsed;
|
|
397
|
+
pendingHasOwnTime = rest.time !== void 0;
|
|
398
|
+
if (rest.time !== void 0) lastTime = rest.time;
|
|
399
|
+
pending = { ...rest, time: rest.time ?? lastTime, raw: line, source, line: lineNo };
|
|
400
|
+
}
|
|
401
|
+
if (pending) yield pending;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/follow.ts
|
|
405
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
406
|
+
import { stat as stat2 } from "fs/promises";
|
|
407
|
+
async function followFiles(sources, onEntry, options = {}) {
|
|
408
|
+
const files = sources.filter((s) => s.path && !/\.gz$/i.test(s.path));
|
|
409
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
410
|
+
const lines = /* @__PURE__ */ new Map();
|
|
411
|
+
for (const f of files) offsets.set(f.path, (await stat2(f.path)).size);
|
|
412
|
+
let busy = false;
|
|
413
|
+
const tick = async () => {
|
|
414
|
+
if (busy) return;
|
|
415
|
+
busy = true;
|
|
416
|
+
try {
|
|
417
|
+
for (const f of files) {
|
|
418
|
+
const path = f.path;
|
|
419
|
+
const size = (await stat2(path).catch(() => void 0))?.size;
|
|
420
|
+
if (size === void 0) continue;
|
|
421
|
+
let start = offsets.get(path) ?? 0;
|
|
422
|
+
if (size < start) start = 0;
|
|
423
|
+
if (size === start) continue;
|
|
424
|
+
offsets.set(path, size);
|
|
425
|
+
const stream = createReadStream2(path, { start, end: size - 1 });
|
|
426
|
+
for await (const entry of readEntries(stream, f.name, { startLine: (lines.get(path) ?? 0) + 1 })) {
|
|
427
|
+
lines.set(path, entry.line);
|
|
428
|
+
onEntry(entry);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} finally {
|
|
432
|
+
busy = false;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const timer = setInterval(() => void tick(), options.intervalMs ?? 500);
|
|
436
|
+
return () => clearInterval(timer);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/merge.ts
|
|
440
|
+
async function* mergeByTime(streams) {
|
|
441
|
+
const iterators = streams.map((s) => s[Symbol.asyncIterator]());
|
|
442
|
+
const heads = await Promise.all(
|
|
443
|
+
iterators.map(async (it) => (await it.next()).value)
|
|
444
|
+
);
|
|
445
|
+
try {
|
|
446
|
+
yield* pickOldest(iterators, heads);
|
|
447
|
+
} finally {
|
|
448
|
+
await Promise.all(iterators.map((it) => it.return?.()));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function* pickOldest(iterators, heads) {
|
|
452
|
+
while (true) {
|
|
453
|
+
let best = -1;
|
|
454
|
+
for (let i = 0; i < heads.length; i++) {
|
|
455
|
+
const head = heads[i];
|
|
456
|
+
if (!head) continue;
|
|
457
|
+
if (best === -1) {
|
|
458
|
+
best = i;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const current = heads[best];
|
|
462
|
+
if ((head.time ?? -Infinity) < (current.time ?? -Infinity)) best = i;
|
|
463
|
+
}
|
|
464
|
+
if (best === -1) return;
|
|
465
|
+
yield heads[best];
|
|
466
|
+
const next = await iterators[best].next();
|
|
467
|
+
heads[best] = next.done ? void 0 : next.value;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/output.ts
|
|
472
|
+
import { styleText } from "util";
|
|
473
|
+
var LEVEL_STYLE = {
|
|
474
|
+
trace: "gray",
|
|
475
|
+
debug: "blue",
|
|
476
|
+
info: "green",
|
|
477
|
+
warn: "yellow",
|
|
478
|
+
error: "red",
|
|
479
|
+
fatal: ["bgRed", "white", "bold"]
|
|
480
|
+
};
|
|
481
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
482
|
+
function formatTime(ms) {
|
|
483
|
+
if (ms === void 0) return " ";
|
|
484
|
+
const d = new Date(ms);
|
|
485
|
+
return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(
|
|
486
|
+
d.getSeconds()
|
|
487
|
+
)}.${String(d.getMilliseconds()).padStart(3, "0")}`;
|
|
488
|
+
}
|
|
489
|
+
function formatValue(v) {
|
|
490
|
+
if (typeof v === "string") return /[\s"=]/.test(v) ? JSON.stringify(v) : v;
|
|
491
|
+
return JSON.stringify(v);
|
|
492
|
+
}
|
|
493
|
+
function formatPretty(entry, options) {
|
|
494
|
+
const paint = (style, text) => options.color ? styleText(style, text) : text;
|
|
495
|
+
const level = (entry.level ?? "").toUpperCase().padEnd(5);
|
|
496
|
+
const parts = [
|
|
497
|
+
paint("dim", formatTime(entry.time)),
|
|
498
|
+
entry.level ? paint(LEVEL_STYLE[entry.level], level) : level,
|
|
499
|
+
paint("cyan", entry.source.padEnd(options.sourceWidth))
|
|
500
|
+
];
|
|
501
|
+
if (entry.format === "text") {
|
|
502
|
+
parts.push(entry.msg);
|
|
503
|
+
} else {
|
|
504
|
+
parts.push(entry.msg);
|
|
505
|
+
const extras = Object.entries(entry.fields).filter(([k]) => !["pid", "hostname", "v", "name"].includes(k)).map(([k, v]) => `${paint("dim", `${k}=`)}${formatValue(v)}`);
|
|
506
|
+
if (extras.length) parts.push(extras.join(" "));
|
|
507
|
+
}
|
|
508
|
+
const continuation = entry.raw.includes("\n") ? `
|
|
509
|
+
${entry.raw.slice(entry.raw.indexOf("\n") + 1)}` : "";
|
|
510
|
+
return parts.join(" ") + (continuation ? paint("dim", continuation) : "");
|
|
511
|
+
}
|
|
512
|
+
function formatJson(entry) {
|
|
513
|
+
return JSON.stringify({
|
|
514
|
+
time: entry.time === void 0 ? null : new Date(entry.time).toISOString(),
|
|
515
|
+
level: entry.level ?? null,
|
|
516
|
+
source: entry.source,
|
|
517
|
+
line: entry.line,
|
|
518
|
+
msg: entry.msg,
|
|
519
|
+
...entry.fields,
|
|
520
|
+
...entry.raw.includes("\n") ? { stack: entry.raw.slice(entry.raw.indexOf("\n") + 1) } : {}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function messageShape(msg) {
|
|
524
|
+
return msg.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<id>").replace(/\b[0-9a-f]{16,}\b/gi, "<id>").replace(/\d+(\.\d+)?/g, "#").slice(0, 120);
|
|
525
|
+
}
|
|
526
|
+
var Stats = class {
|
|
527
|
+
total = 0;
|
|
528
|
+
first;
|
|
529
|
+
last;
|
|
530
|
+
byLevel = /* @__PURE__ */ new Map();
|
|
531
|
+
bySource = /* @__PURE__ */ new Map();
|
|
532
|
+
byMessage = /* @__PURE__ */ new Map();
|
|
533
|
+
add(entry) {
|
|
534
|
+
this.total++;
|
|
535
|
+
if (entry.time !== void 0) {
|
|
536
|
+
this.first = Math.min(this.first ?? entry.time, entry.time);
|
|
537
|
+
this.last = Math.max(this.last ?? entry.time, entry.time);
|
|
538
|
+
}
|
|
539
|
+
const level = entry.level ?? "none";
|
|
540
|
+
this.byLevel.set(level, (this.byLevel.get(level) ?? 0) + 1);
|
|
541
|
+
this.bySource.set(entry.source, (this.bySource.get(entry.source) ?? 0) + 1);
|
|
542
|
+
const shape = messageShape(entry.msg || entry.raw);
|
|
543
|
+
const m = this.byMessage.get(shape) ?? { count: 0, level: entry.level };
|
|
544
|
+
m.count++;
|
|
545
|
+
this.byMessage.set(shape, m);
|
|
546
|
+
}
|
|
547
|
+
format(color) {
|
|
548
|
+
const paint = (style, text) => color ? styleText(style, text) : text;
|
|
549
|
+
const sorted = (map) => [...map].sort((a, b) => b[1] - a[1]);
|
|
550
|
+
const lines = [
|
|
551
|
+
paint("bold", `${this.total} matching lines`) + (this.first !== void 0 ? ` ${formatTime(this.first)} \u2192 ${formatTime(this.last)}` : ""),
|
|
552
|
+
"",
|
|
553
|
+
paint("bold", "By level"),
|
|
554
|
+
...sorted(this.byLevel).map(([k, n]) => ` ${String(n).padStart(8)} ${k}`),
|
|
555
|
+
"",
|
|
556
|
+
paint("bold", "By file"),
|
|
557
|
+
...sorted(this.bySource).map(([k, n]) => ` ${String(n).padStart(8)} ${k}`),
|
|
558
|
+
"",
|
|
559
|
+
paint("bold", "Most common messages"),
|
|
560
|
+
...[...this.byMessage].sort((a, b) => b[1].count - a[1].count).slice(0, 10).map(([shape, m]) => ` ${String(m.count).padStart(8)} ${(m.level ?? "").padEnd(5)} ${shape}`)
|
|
561
|
+
];
|
|
562
|
+
return lines.join("\n");
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// src/run.ts
|
|
567
|
+
import { parseArgs } from "util";
|
|
568
|
+
var HELP = `logsift: search many log files as one timeline
|
|
569
|
+
|
|
570
|
+
Usage
|
|
571
|
+
logsift [files, folders or globs...] [options]
|
|
572
|
+
some-command | logsift [options]
|
|
573
|
+
|
|
574
|
+
Filters (all must match)
|
|
575
|
+
-l, --level <level> this level and above: trace, debug, info, warn, error, fatal
|
|
576
|
+
-s, --since <time> 15m, 2h, 1d, 14:03 or 2026-11-05 14:03
|
|
577
|
+
-u, --until <time> same formats as --since
|
|
578
|
+
-g, --grep <text> text (any case), /regex/i or re:regex; repeat to require several
|
|
579
|
+
-w, --where <expr> field filter: status>=500, user.id=42, msg~timeout; repeatable
|
|
580
|
+
-r, --request <id> every line that mentions this request, trace or correlation ID
|
|
581
|
+
-a, --around <time> lines near a moment, e.g. 14:03 (see --window)
|
|
582
|
+
--window <span> size of --around on each side (default 1m)
|
|
583
|
+
|
|
584
|
+
Output
|
|
585
|
+
--json one JSON object per line
|
|
586
|
+
--stats counts by level and file, and the most common messages
|
|
587
|
+
-n, --limit <n> stop after n matching lines
|
|
588
|
+
-f, --follow keep watching the files for new lines
|
|
589
|
+
--no-color plain text (also when NO_COLOR is set)
|
|
590
|
+
-h, --help show this help
|
|
591
|
+
-v, --version show the version
|
|
592
|
+
|
|
593
|
+
Exit code: 0 when something matched, 1 when nothing did, 2 on errors.`;
|
|
594
|
+
var write = (stream, text) => new Promise((resolve2) => {
|
|
595
|
+
if (stream.write(`${text}
|
|
596
|
+
`)) resolve2();
|
|
597
|
+
else stream.once("drain", resolve2);
|
|
598
|
+
});
|
|
599
|
+
async function run(argv, io) {
|
|
600
|
+
let args;
|
|
601
|
+
try {
|
|
602
|
+
args = parseArgs({
|
|
603
|
+
args: argv,
|
|
604
|
+
allowPositionals: true,
|
|
605
|
+
options: {
|
|
606
|
+
level: { type: "string", short: "l" },
|
|
607
|
+
since: { type: "string", short: "s" },
|
|
608
|
+
until: { type: "string", short: "u" },
|
|
609
|
+
grep: { type: "string", short: "g", multiple: true },
|
|
610
|
+
where: { type: "string", short: "w", multiple: true },
|
|
611
|
+
request: { type: "string", short: "r" },
|
|
612
|
+
around: { type: "string", short: "a" },
|
|
613
|
+
window: { type: "string" },
|
|
614
|
+
json: { type: "boolean" },
|
|
615
|
+
stats: { type: "boolean" },
|
|
616
|
+
limit: { type: "string", short: "n" },
|
|
617
|
+
follow: { type: "boolean", short: "f" },
|
|
618
|
+
"no-color": { type: "boolean" },
|
|
619
|
+
help: { type: "boolean", short: "h" },
|
|
620
|
+
version: { type: "boolean", short: "v" }
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
} catch (err) {
|
|
624
|
+
await write(io.stderr, `logsift: ${err.message}
|
|
625
|
+
Run logsift --help to see the options.`);
|
|
626
|
+
return 2;
|
|
627
|
+
}
|
|
628
|
+
const { values: o, positionals } = args;
|
|
629
|
+
if (o.help) {
|
|
630
|
+
await write(io.stdout, HELP);
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
if (o.version) {
|
|
634
|
+
await write(io.stdout, io.version ?? "unknown");
|
|
635
|
+
return 0;
|
|
636
|
+
}
|
|
637
|
+
const now = io.now ?? /* @__PURE__ */ new Date();
|
|
638
|
+
const cwd = io.cwd ?? process.cwd();
|
|
639
|
+
const color = !o["no-color"] && (io.color ?? Boolean(io.stdout.isTTY && !process.env.NO_COLOR));
|
|
640
|
+
try {
|
|
641
|
+
const filter = buildFilter(o, now);
|
|
642
|
+
const limit = o.limit === void 0 ? Infinity : Number(o.limit);
|
|
643
|
+
if (o.limit !== void 0 && (!Number.isInteger(limit) || limit < 1)) {
|
|
644
|
+
throw new Error("--limit needs a whole number above 0.");
|
|
645
|
+
}
|
|
646
|
+
let sources;
|
|
647
|
+
if (positionals.length > 0) {
|
|
648
|
+
const files = await expandInputs(positionals, cwd);
|
|
649
|
+
if (files.length === 0) throw new Error("No log files found for those inputs.");
|
|
650
|
+
sources = labelSources(files, cwd);
|
|
651
|
+
} else if (!io.stdin.isTTY) {
|
|
652
|
+
sources = [{ name: "stdin" }];
|
|
653
|
+
} else {
|
|
654
|
+
await write(io.stdout, HELP);
|
|
655
|
+
return 2;
|
|
656
|
+
}
|
|
657
|
+
const sourceWidth = Math.min(24, Math.max(...sources.map((s) => s.name.length)));
|
|
658
|
+
const stats = o.stats ? new Stats() : void 0;
|
|
659
|
+
let matched = 0;
|
|
660
|
+
const emit = async (entry) => {
|
|
661
|
+
if (!filter(entry)) return;
|
|
662
|
+
matched++;
|
|
663
|
+
if (stats) stats.add(entry);
|
|
664
|
+
else await write(io.stdout, o.json ? formatJson(entry) : formatPretty(entry, { color, sourceWidth }));
|
|
665
|
+
};
|
|
666
|
+
const streams = sources.map((s) => readEntries(openStream(s, io.stdin), s.name, { now }));
|
|
667
|
+
for await (const entry of mergeByTime(streams)) {
|
|
668
|
+
await emit(entry);
|
|
669
|
+
if (matched >= limit) break;
|
|
670
|
+
}
|
|
671
|
+
if (o.follow && matched < limit && sources.some((s) => s.path)) {
|
|
672
|
+
await new Promise((resolve2) => {
|
|
673
|
+
void followFiles(sources, (entry) => void emit(entry)).then((stop) => {
|
|
674
|
+
const end = () => {
|
|
675
|
+
stop();
|
|
676
|
+
resolve2();
|
|
677
|
+
};
|
|
678
|
+
process.once("SIGINT", end);
|
|
679
|
+
io.onFollow?.(end);
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
if (stats) await write(io.stdout, stats.format(color));
|
|
684
|
+
return matched > 0 ? 0 : 1;
|
|
685
|
+
} catch (err) {
|
|
686
|
+
await write(io.stderr, `logsift: ${err.message}`);
|
|
687
|
+
return 2;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export {
|
|
692
|
+
LEVELS,
|
|
693
|
+
toLevel,
|
|
694
|
+
toTime,
|
|
695
|
+
parseLogfmt,
|
|
696
|
+
parseLine,
|
|
697
|
+
REQUEST_ID_KEYS,
|
|
698
|
+
parseDuration,
|
|
699
|
+
parseMoment,
|
|
700
|
+
parseWhere,
|
|
701
|
+
buildFilter,
|
|
702
|
+
expandInputs,
|
|
703
|
+
labelSources,
|
|
704
|
+
openStream,
|
|
705
|
+
readEntries,
|
|
706
|
+
followFiles,
|
|
707
|
+
mergeByTime,
|
|
708
|
+
formatPretty,
|
|
709
|
+
formatJson,
|
|
710
|
+
Stats,
|
|
711
|
+
HELP,
|
|
712
|
+
run
|
|
713
|
+
};
|
|
714
|
+
//# sourceMappingURL=chunk-QR2OWMYF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/parse.ts","../src/filter.ts","../src/sources.ts","../src/follow.ts","../src/merge.ts","../src/output.ts","../src/run.ts"],"sourcesContent":["export const LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const;\nexport type Level = (typeof LEVELS)[number];\n\n/** One log entry, whatever format it came from. */\nexport interface Entry {\n /** Milliseconds since 1970, or undefined when the line has no time. */\n time?: number;\n level?: Level;\n msg: string;\n /** Structured fields (JSON or logfmt), without the time, level and message keys. */\n fields: Record<string, unknown>;\n /** The original text, including continuation lines such as stack traces. */\n raw: string;\n /** Short name of the file it came from. */\n source: string;\n /** 1-based line number of the first line in its file. */\n line: number;\n format: 'json' | 'logfmt' | 'text';\n}\n","import { LEVELS, type Entry, type Level } from './types.js';\n\ntype Parsed = Omit<Entry, 'source' | 'line' | 'raw'>;\n\nconst TIME_KEYS = ['time', 'timestamp', '@timestamp', 'ts', 'date', 'datetime', 't'];\nconst LEVEL_KEYS = ['level', 'lvl', 'severity', 'loglevel', 'log.level', '@level'];\nconst MSG_KEYS = ['msg', 'message', '@message', 'event', 'text'];\n\nconst LEVEL_WORDS: Record<string, Level> = {\n trace: 'trace',\n verbose: 'trace',\n silly: 'trace',\n debug: 'debug',\n dbg: 'debug',\n info: 'info',\n information: 'info',\n notice: 'info',\n http: 'info',\n warn: 'warn',\n warning: 'warn',\n error: 'error',\n err: 'error',\n fatal: 'fatal',\n critical: 'fatal',\n crit: 'fatal',\n panic: 'fatal',\n alert: 'fatal',\n emerg: 'fatal',\n};\n\n/** Level from a word (\"WARN\") or a pino/bunyan number (40). */\nexport function toLevel(value: unknown): Level | undefined {\n if (typeof value === 'number') {\n if (value >= 60) return 'fatal';\n if (value >= 50) return 'error';\n if (value >= 40) return 'warn';\n if (value >= 30) return 'info';\n if (value >= 20) return 'debug';\n return 'trace';\n }\n if (typeof value === 'string') {\n const word = value.trim().toLowerCase();\n if (/^\\d+$/.test(word)) return toLevel(Number(word));\n return LEVEL_WORDS[word];\n }\n return undefined;\n}\n\nexport const levelRank = (level: Level | undefined) => (level ? LEVELS.indexOf(level) : -1);\n\nconst MONTHS: Record<string, number> = {\n jan: 0,\n feb: 1,\n mar: 2,\n apr: 3,\n may: 4,\n jun: 5,\n jul: 6,\n aug: 7,\n sep: 8,\n oct: 9,\n nov: 10,\n dec: 11,\n};\n\n/** Time in ms from a number (seconds or ms) or a date string. */\nexport function toTime(value: unknown, now = new Date()): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value < 1e11 ? Math.round(value * 1000) : value; // seconds vs milliseconds\n }\n if (typeof value !== 'string' || value.length < 6) return undefined;\n const s = value.trim();\n if (/^\\d{10}(\\.\\d+)?$/.test(s) || /^\\d{13}$/.test(s)) return toTime(Number(s));\n\n // 05/Nov/2026:14:03:01 +0000 (nginx, apache)\n let m = /^(\\d{2})\\/(\\w{3})\\/(\\d{4}):(\\d{2}):(\\d{2}):(\\d{2})(?:\\s*([+-]\\d{4}))?/.exec(s);\n if (m) {\n const [, d, mon, y, hh, mm, ss, tz] = m;\n const month = MONTHS[mon!.toLowerCase()];\n if (month === undefined) return undefined;\n const iso = `${y}-${String(month + 1).padStart(2, '0')}-${d}T${hh}:${mm}:${ss}${tz ? `${tz.slice(0, 3)}:${tz.slice(3)}` : ''}`;\n const t = Date.parse(iso);\n return Number.isNaN(t) ? undefined : t;\n }\n // Nov 5 14:03:01 (syslog, no year: assume this year)\n m = /^(\\w{3})\\s+(\\d{1,2})\\s+(\\d{2}):(\\d{2}):(\\d{2})/.exec(s);\n if (m) {\n const month = MONTHS[m[1]!.toLowerCase()];\n if (month === undefined) return undefined;\n return new Date(\n now.getFullYear(),\n month,\n Number(m[2]),\n Number(m[3]),\n Number(m[4]),\n Number(m[5]),\n ).getTime();\n }\n // 2026-11-05 14:03:01,123 → ISO\n const iso = s.replace(\n /^(\\d{4}-\\d{2}-\\d{2})[ T](\\d{2}:\\d{2}:\\d{2})[,.]?(\\d+)?/,\n (_a, d, t, frac) => `${d}T${t}${frac ? `.${String(frac).slice(0, 3).padEnd(3, '0')}` : ''}`,\n );\n if (!/^\\d{4}-\\d{2}-\\d{2}/.test(iso)) return undefined;\n const t = Date.parse(iso);\n return Number.isNaN(t) ? undefined : t;\n}\n\nfunction getPath(obj: Record<string, unknown>, key: string): unknown {\n if (key in obj) return obj[key];\n let cur: unknown = obj;\n for (const part of key.split('.')) {\n if (cur === null || typeof cur !== 'object') return undefined;\n cur = (cur as Record<string, unknown>)[part];\n }\n return cur;\n}\n\n/** Pulls time, level and message out of structured fields. */\nfunction fromFields(fields: Record<string, unknown>, format: 'json' | 'logfmt', now: Date): Parsed {\n const rest = { ...fields };\n const take = (keys: string[], convert: (v: unknown) => unknown) => {\n for (const key of keys) {\n const value = getPath(rest, key);\n const converted = value === undefined ? undefined : convert(value);\n if (converted !== undefined) {\n if (key in rest) delete rest[key];\n return converted;\n }\n }\n return undefined;\n };\n const time = take(TIME_KEYS, (v) => toTime(v, now)) as number | undefined;\n const level = take(LEVEL_KEYS, toLevel) as Level | undefined;\n const msg = take(MSG_KEYS, (v) => (typeof v === 'string' ? v : undefined)) as string | undefined;\n return { time, level, msg: msg ?? '', fields: rest, format };\n}\n\n/** Parses `key=value key2=\"quoted value\"`; returns undefined when it doesn't look like logfmt. */\nexport function parseLogfmt(line: string): Record<string, unknown> | undefined {\n const fields: Record<string, unknown> = {};\n const re = /([\\w.@-]+)=(\"(?:[^\"\\\\]|\\\\.)*\"|\\S*)/g;\n let count = 0;\n let covered = 0;\n for (let m = re.exec(line); m; m = re.exec(line)) {\n let value: string = m[2]!;\n if (value.startsWith('\"')) value = value.slice(1, -1).replace(/\\\\(.)/g, '$1');\n fields[m[1]!] = value;\n count++;\n covered += m[0].length;\n }\n // Mostly key=value pairs, and at least one well-known key.\n const known = Object.keys(fields).some((k) => [...LEVEL_KEYS, ...MSG_KEYS, ...TIME_KEYS].includes(k));\n if (count < 2 || !known || covered < line.replace(/\\s/g, '').length * 0.6) return undefined;\n return fields;\n}\n\nconst TEXT_TIME =\n /^\\[?((?:\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2}(?:[.,]\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?)|(?:\\d{2}\\/\\w{3}\\/\\d{4}:\\d{2}:\\d{2}:\\d{2}(?:\\s[+-]\\d{4})?)|(?:\\w{3}\\s+\\d{1,2}\\s\\d{2}:\\d{2}:\\d{2}))\\]?\\s*/;\nconst TEXT_LEVEL =\n /^\\[?(trace|debug|dbg|info|notice|warn|warning|error|err|fatal|critical|crit|panic)\\]?[:\\s-]+/i;\nconst ANY_LEVEL = /\\b(TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|FATAL|CRITICAL)\\b/;\n\n/** Plain text: optional timestamp, optional level word, then the message. */\nfunction parseText(line: string, now: Date): Parsed & { hasTime: boolean } {\n let rest = line;\n let time: number | undefined;\n const tm = TEXT_TIME.exec(rest);\n if (tm) {\n time = toTime(tm[1], now);\n rest = rest.slice(tm[0].length);\n }\n let level: Level | undefined;\n const lm = TEXT_LEVEL.exec(rest);\n if (lm) {\n level = toLevel(lm[1]);\n rest = rest.slice(lm[0].length);\n } else {\n const any = ANY_LEVEL.exec(rest.slice(0, 60));\n if (any) level = toLevel(any[1]);\n }\n return { time, level, msg: rest.trim(), fields: {}, format: 'text', hasTime: time !== undefined };\n}\n\n/**\n * Parses one line. Returns `continuation: true` for lines that belong to the\n * entry before them, such as stack trace lines.\n */\nexport function parseLine(line: string, now = new Date()): (Parsed & { continuation: boolean }) | undefined {\n if (line.trim() === '') return undefined;\n const trimmed = line.trimStart();\n if (trimmed.startsWith('{')) {\n try {\n const obj = JSON.parse(trimmed) as unknown;\n if (obj && typeof obj === 'object' && !Array.isArray(obj)) {\n return { ...fromFields(obj as Record<string, unknown>, 'json', now), continuation: false };\n }\n } catch {\n // Not JSON after all: fall through to the other formats.\n }\n }\n const lf = parseLogfmt(line);\n if (lf) return { ...fromFields(lf, 'logfmt', now), continuation: false };\n\n const text = parseText(line, now);\n const looksLikeContinuation =\n !text.hasTime && (/^\\s+/.test(line) || /^(at |Caused by|\\.\\.\\. \\d+ more|Traceback|\\})/.test(trimmed));\n const { hasTime: _hasTime, ...parsed } = text;\n return { ...parsed, continuation: looksLikeContinuation };\n}\n","import { levelRank, toLevel } from './parse.js';\nimport type { Entry, Level } from './types.js';\n\nexport interface FilterOptions {\n /** Minimum level, e.g. \"warn\" shows warn, error and fatal. */\n level?: string;\n since?: string;\n until?: string;\n /** Text (case-insensitive) or /regex/flags. Several are all required. */\n grep?: string[];\n /** Field filters like `status>=500`, `user.id=42`, `msg~timeout`. */\n where?: string[];\n /** Show only lines that mention this request, trace or correlation ID. */\n request?: string;\n /** A moment in time; shows lines within `window` of it. */\n around?: string;\n /** Size of the `around` window, e.g. \"2m\". Default 1m. */\n window?: string;\n}\n\n/** Field names that usually hold a request, trace or correlation ID. */\nexport const REQUEST_ID_KEYS = [\n 'reqId',\n 'req_id',\n 'requestId',\n 'request_id',\n 'request-id',\n 'x-request-id',\n 'X-Request-Id',\n 'traceId',\n 'trace_id',\n 'trace.id',\n 'traceID',\n 'correlationId',\n 'correlation_id',\n 'x-correlation-id',\n 'spanId',\n 'span_id',\n 'req.id',\n 'request.id',\n 'rid',\n];\n\nconst UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n};\n\n/** \"90s\", \"15m\", \"2h\", \"1d\" → milliseconds. */\nexport function parseDuration(text: string): number | undefined {\n const m = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d|w)$/i.exec(text.trim());\n return m ? Number(m[1]) * UNITS[m[2]!.toLowerCase()]! : undefined;\n}\n\n/**\n * A point in time: a duration ago (\"15m\"), a time today (\"14:03\" or \"14:03:30\"),\n * or a date (\"2026-11-05\", \"2026-11-05 14:03\").\n */\nexport function parseMoment(text: string, now: Date): number {\n const ago = parseDuration(text);\n if (ago !== undefined) return now.getTime() - ago;\n const clock = /^(\\d{1,2}):(\\d{2})(?::(\\d{2}))?$/.exec(text.trim());\n if (clock) {\n const d = new Date(now);\n d.setHours(Number(clock[1]), Number(clock[2]), Number(clock[3] ?? 0), 0);\n return d.getTime();\n }\n const t = Date.parse(text.trim().replace(' ', 'T'));\n if (Number.isNaN(t)) throw new Error(`Can't read the time \"${text}\". Try 15m, 14:03 or 2026-11-05 14:03.`);\n return t;\n}\n\nconst toRegex = (source: string, flags: string) => {\n try {\n return new RegExp(source, flags.replace('g', ''));\n } catch {\n throw new Error(`\"${source}\" is not a valid regular expression.`);\n }\n};\n\n/**\n * Plain text matches anywhere, ignoring case. `/regex/flags` is a regex.\n * `re:regex` is a case-insensitive regex that shells (like Git Bash) won't rewrite as a path.\n */\nfunction textMatcher(pattern: string): (s: string) => boolean {\n const slashed = /^\\/(.+)\\/([a-z]*)$/.exec(pattern);\n if (slashed || pattern.startsWith('re:')) {\n const regex = slashed ? toRegex(slashed[1]!, slashed[2]!) : toRegex(pattern.slice(3), 'i');\n return (s) => regex.test(s);\n }\n const needle = pattern.toLowerCase();\n return (s) => s.toLowerCase().includes(needle);\n}\n\nfunction fieldValue(entry: Entry, key: string): unknown {\n if (key === 'msg' || key === 'message') return entry.msg;\n if (key === 'level') return entry.level;\n if (key === 'source' || key === 'file') return entry.source;\n if (key in entry.fields) return entry.fields[key];\n let cur: unknown = entry.fields;\n for (const part of key.split('.')) {\n if (cur === null || typeof cur !== 'object') return undefined;\n cur = (cur as Record<string, unknown>)[part];\n }\n return cur;\n}\n\n/** `key op value`, with op one of = != >= <= > < ~ (contains / regex). */\nexport function parseWhere(expr: string): (e: Entry) => boolean {\n const m = /^([\\w.@-]+)\\s*(!=|>=|<=|=|>|<|~)\\s*(.*)$/.exec(expr.trim());\n if (!m) throw new Error(`Can't read the filter \"${expr}\". Use key=value, key>=500, key~text.`);\n const [, key, op, wanted] = m as unknown as [string, string, string, string];\n const asNumber = Number(wanted);\n const numeric = wanted !== '' && !Number.isNaN(asNumber);\n const contains = op === '~' ? textMatcher(wanted) : undefined;\n return (entry) => {\n const value = fieldValue(entry, key);\n if (value === undefined || value === null) return op === '!=';\n const str = typeof value === 'object' ? JSON.stringify(value) : String(value);\n switch (op) {\n case '=':\n return str === wanted;\n case '!=':\n return str !== wanted;\n case '~':\n return contains!(str);\n default: {\n const n = typeof value === 'number' ? value : Number(str);\n if (!numeric || Number.isNaN(n)) return false;\n if (op === '>') return n > asNumber;\n if (op === '>=') return n >= asNumber;\n if (op === '<') return n < asNumber;\n return n <= asNumber;\n }\n }\n };\n}\n\n/** Builds one test from all the filter options. Throws a readable error on bad input. */\nexport function buildFilter(options: FilterOptions, now = new Date()): (e: Entry) => boolean {\n const tests: ((e: Entry) => boolean)[] = [];\n\n if (options.level) {\n const min = toLevel(options.level);\n if (!min)\n throw new Error(`Unknown level \"${options.level}\". Use trace, debug, info, warn, error or fatal.`);\n const rank = levelRank(min as Level);\n tests.push((e) => levelRank(e.level) >= rank);\n }\n if (options.since) {\n const from = parseMoment(options.since, now);\n tests.push((e) => e.time !== undefined && e.time >= from);\n }\n if (options.until) {\n const to = parseMoment(options.until, now);\n tests.push((e) => e.time !== undefined && e.time <= to);\n }\n if (options.around) {\n const at = parseMoment(options.around, now);\n const span = parseDuration(options.window ?? '1m');\n if (span === undefined) throw new Error(`Can't read the window \"${options.window}\". Try 30s or 2m.`);\n tests.push((e) => e.time !== undefined && Math.abs(e.time - at) <= span);\n }\n for (const pattern of options.grep ?? []) {\n const match = textMatcher(pattern);\n tests.push((e) => match(e.raw));\n }\n for (const expr of options.where ?? []) tests.push(parseWhere(expr));\n if (options.request) {\n const id = options.request;\n // The whole ID only: \"req-42\" must not match \"req-420\".\n const whole = new RegExp(`(?<![\\\\w-])${id.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}(?![\\\\w-])`);\n tests.push(\n (e) =>\n // Any match has the ID somewhere in the line, so that cheap check skips most lines.\n e.raw.includes(id) &&\n (whole.test(e.raw) || REQUEST_ID_KEYS.some((k) => String(fieldValue(e, k) ?? '') === id)),\n );\n }\n return (e) => tests.every((t) => t(e));\n}\n","import { createReadStream } from 'node:fs';\nimport { glob, stat, readdir } from 'node:fs/promises';\nimport { basename, join, relative, resolve } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport type { Readable } from 'node:stream';\nimport { createGunzip } from 'node:zlib';\nimport { parseLine } from './parse.js';\nimport type { Entry } from './types.js';\n\nexport interface Source {\n /** Short label shown next to each line. */\n name: string;\n /** Full path, or undefined for stdin. */\n path?: string;\n}\n\nconst LOG_FILE = /\\.(log|txt|json|jsonl|ndjson|out|err)(\\.\\d+)?(\\.gz)?$|\\.gz$/i;\nconst MAX_ENTRY_CHARS = 64 * 1024;\n\nasync function walk(dir: string, out: string[]): Promise<void> {\n for (const item of await readdir(dir, { withFileTypes: true })) {\n const full = join(dir, item.name);\n if (item.isDirectory()) {\n if (item.name !== 'node_modules' && !item.name.startsWith('.')) await walk(full, out);\n } else if (LOG_FILE.test(item.name)) {\n out.push(full);\n }\n }\n}\n\n/**\n * Turns the inputs (files, folders, globs) into a sorted list of files.\n * Globs are expanded here, so they work on Windows shells too.\n */\nexport async function expandInputs(inputs: string[], cwd = process.cwd()): Promise<string[]> {\n const files = new Set<string>();\n for (const input of inputs) {\n if (/[*?[\\]{}]/.test(input)) {\n for await (const match of glob(input, { cwd })) files.add(resolve(cwd, match));\n continue;\n }\n const full = resolve(cwd, input);\n const info = await stat(full).catch(() => undefined);\n if (!info) throw new Error(`No such file or folder: ${input}`);\n if (info.isDirectory()) {\n const found: string[] = [];\n await walk(full, found);\n found.forEach((f) => files.add(f));\n } else {\n files.add(full);\n }\n }\n return [...files].sort();\n}\n\n/** Short, unique labels: the file name, or a relative path when names repeat. */\nexport function labelSources(paths: string[], cwd = process.cwd()): Source[] {\n const counts = new Map<string, number>();\n for (const p of paths) counts.set(basename(p), (counts.get(basename(p)) ?? 0) + 1);\n return paths.map((p) => ({\n path: p,\n name: (counts.get(basename(p)) ?? 0) > 1 ? relative(cwd, p).replace(/\\\\/g, '/') : basename(p),\n }));\n}\n\nexport function openStream(source: Source, stdin: Readable = process.stdin): Readable {\n if (!source.path) return stdin;\n const raw = createReadStream(source.path);\n return /\\.gz$/i.test(source.path) ? raw.pipe(createGunzip()) : raw;\n}\n\n/**\n * Reads a stream into entries. Lines that continue the entry before them\n * (stack traces, wrapped JSON) are folded into it. Entries without a time take\n * the time of the entry before them, so they keep their place in the timeline.\n */\nexport async function* readEntries(\n stream: Readable,\n source: string,\n options: { now?: Date; startLine?: number } = {},\n): AsyncGenerator<Entry> {\n const now = options.now ?? new Date();\n const lines = createInterface({ input: stream, crlfDelay: Infinity });\n let pending: Entry | undefined;\n let pendingHasOwnTime = false;\n let lastTime: number | undefined;\n let lineNo = (options.startLine ?? 1) - 1;\n\n for await (const line of lines) {\n lineNo++;\n const parsed = parseLine(line, now);\n if (!parsed) continue;\n // In a text log whose lines start with a time, a line without one belongs\n // to the entry above it (e.g. \"Error: card declined\" in a stack trace).\n const untimedTextAfterTimed =\n parsed.format === 'text' &&\n parsed.time === undefined &&\n pending?.format === 'text' &&\n pendingHasOwnTime;\n if (pending && (parsed.continuation || untimedTextAfterTimed)) {\n if (pending.raw.length < MAX_ENTRY_CHARS) pending.raw += `\\n${line}`;\n continue;\n }\n if (pending) yield pending;\n const { continuation: _c, ...rest } = parsed;\n pendingHasOwnTime = rest.time !== undefined;\n if (rest.time !== undefined) lastTime = rest.time;\n pending = { ...rest, time: rest.time ?? lastTime, raw: line, source, line: lineNo };\n }\n if (pending) yield pending;\n}\n","import { createReadStream } from 'node:fs';\nimport { stat } from 'node:fs/promises';\nimport { readEntries, type Source } from './sources.js';\nimport type { Entry } from './types.js';\n\n/**\n * Watches files for new lines and passes each new entry to `onEntry`.\n * Polls file sizes, which works the same on Windows, macOS and Linux.\n * Returns a function that stops watching.\n */\nexport async function followFiles(\n sources: Source[],\n onEntry: (entry: Entry) => void,\n options: { intervalMs?: number } = {},\n): Promise<() => void> {\n const files = sources.filter((s) => s.path && !/\\.gz$/i.test(s.path));\n const offsets = new Map<string, number>();\n const lines = new Map<string, number>();\n for (const f of files) offsets.set(f.path!, (await stat(f.path!)).size);\n\n let busy = false;\n const tick = async () => {\n if (busy) return;\n busy = true;\n try {\n for (const f of files) {\n const path = f.path!;\n const size = (await stat(path).catch(() => undefined))?.size;\n if (size === undefined) continue;\n let start = offsets.get(path) ?? 0;\n if (size < start) start = 0; // file was truncated or rotated\n if (size === start) continue;\n offsets.set(path, size);\n const stream = createReadStream(path, { start, end: size - 1 });\n for await (const entry of readEntries(stream, f.name, { startLine: (lines.get(path) ?? 0) + 1 })) {\n lines.set(path, entry.line);\n onEntry(entry);\n }\n }\n } finally {\n busy = false;\n }\n };\n const timer = setInterval(() => void tick(), options.intervalMs ?? 500);\n return () => clearInterval(timer);\n}\n","import type { Entry } from './types.js';\n\n/**\n * Merges several time-ordered streams into one, oldest first. Each stream is\n * read one entry at a time, so memory stays small however big the files are.\n * Entries with no time at all sort first; ties keep the input order.\n */\nexport async function* mergeByTime(streams: AsyncIterable<Entry>[]): AsyncGenerator<Entry> {\n const iterators = streams.map((s) => s[Symbol.asyncIterator]());\n const heads: (Entry | undefined)[] = await Promise.all(\n iterators.map(async (it) => (await it.next()).value as Entry | undefined),\n );\n\n try {\n yield* pickOldest(iterators, heads);\n } finally {\n // Stopping early (e.g. --limit) closes every file.\n await Promise.all(iterators.map((it) => it.return?.()));\n }\n}\n\nasync function* pickOldest(\n iterators: AsyncIterator<Entry>[],\n heads: (Entry | undefined)[],\n): AsyncGenerator<Entry> {\n while (true) {\n let best = -1;\n for (let i = 0; i < heads.length; i++) {\n const head = heads[i];\n if (!head) continue;\n if (best === -1) {\n best = i;\n continue;\n }\n const current = heads[best]!;\n if ((head.time ?? -Infinity) < (current.time ?? -Infinity)) best = i;\n }\n if (best === -1) return;\n yield heads[best]!;\n const next = await iterators[best]!.next();\n heads[best] = next.done ? undefined : next.value;\n }\n}\n","import { styleText } from 'node:util';\nimport type { Entry, Level } from './types.js';\n\ntype Style = Parameters<typeof styleText>[0];\n\nconst LEVEL_STYLE: Record<Level, Style> = {\n trace: 'gray',\n debug: 'blue',\n info: 'green',\n warn: 'yellow',\n error: 'red',\n fatal: ['bgRed', 'white', 'bold'],\n};\n\nconst pad2 = (n: number) => String(n).padStart(2, '0');\n\n/** Local time: `11-05 14:03:01.123`. */\nexport function formatTime(ms: number | undefined): string {\n if (ms === undefined) return ' ';\n const d = new Date(ms);\n return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(\n d.getSeconds(),\n )}.${String(d.getMilliseconds()).padStart(3, '0')}`;\n}\n\nfunction formatValue(v: unknown): string {\n if (typeof v === 'string') return /[\\s\"=]/.test(v) ? JSON.stringify(v) : v;\n return JSON.stringify(v);\n}\n\n/** One readable line (plus any continuation lines) for the terminal. */\nexport function formatPretty(entry: Entry, options: { color: boolean; sourceWidth: number }): string {\n const paint = (style: Style, text: string) => (options.color ? styleText(style, text) : text);\n const level = (entry.level ?? '').toUpperCase().padEnd(5);\n const parts = [\n paint('dim', formatTime(entry.time)),\n entry.level ? paint(LEVEL_STYLE[entry.level], level) : level,\n paint('cyan', entry.source.padEnd(options.sourceWidth)),\n ];\n if (entry.format === 'text') {\n parts.push(entry.msg);\n } else {\n parts.push(entry.msg);\n const extras = Object.entries(entry.fields)\n .filter(([k]) => !['pid', 'hostname', 'v', 'name'].includes(k))\n .map(([k, v]) => `${paint('dim', `${k}=`)}${formatValue(v)}`);\n if (extras.length) parts.push(extras.join(' '));\n }\n const continuation = entry.raw.includes('\\n') ? `\\n${entry.raw.slice(entry.raw.indexOf('\\n') + 1)}` : '';\n return parts.join(' ') + (continuation ? paint('dim', continuation) : '');\n}\n\n/** One JSON object per line, ready for jq or another tool. */\nexport function formatJson(entry: Entry): string {\n return JSON.stringify({\n time: entry.time === undefined ? null : new Date(entry.time).toISOString(),\n level: entry.level ?? null,\n source: entry.source,\n line: entry.line,\n msg: entry.msg,\n ...entry.fields,\n ...(entry.raw.includes('\\n') ? { stack: entry.raw.slice(entry.raw.indexOf('\\n') + 1) } : {}),\n });\n}\n\n/** Turns \"user 42 timed out after 3000ms\" into \"user # timed out after #ms\" to group similar messages. */\nexport function messageShape(msg: string): string {\n return msg\n .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<id>')\n .replace(/\\b[0-9a-f]{16,}\\b/gi, '<id>')\n .replace(/\\d+(\\.\\d+)?/g, '#')\n .slice(0, 120);\n}\n\nexport class Stats {\n total = 0;\n first?: number;\n last?: number;\n readonly byLevel = new Map<string, number>();\n readonly bySource = new Map<string, number>();\n readonly byMessage = new Map<string, { count: number; level?: Level }>();\n\n add(entry: Entry): void {\n this.total++;\n if (entry.time !== undefined) {\n this.first = Math.min(this.first ?? entry.time, entry.time);\n this.last = Math.max(this.last ?? entry.time, entry.time);\n }\n const level = entry.level ?? 'none';\n this.byLevel.set(level, (this.byLevel.get(level) ?? 0) + 1);\n this.bySource.set(entry.source, (this.bySource.get(entry.source) ?? 0) + 1);\n const shape = messageShape(entry.msg || entry.raw);\n const m = this.byMessage.get(shape) ?? { count: 0, level: entry.level };\n m.count++;\n this.byMessage.set(shape, m);\n }\n\n format(color: boolean): string {\n const paint = (style: Style, text: string) => (color ? styleText(style, text) : text);\n const sorted = (map: Map<string, number>) => [...map].sort((a, b) => b[1] - a[1]);\n const lines = [\n paint('bold', `${this.total} matching lines`) +\n (this.first !== undefined ? ` ${formatTime(this.first)} → ${formatTime(this.last)}` : ''),\n '',\n paint('bold', 'By level'),\n ...sorted(this.byLevel).map(([k, n]) => ` ${String(n).padStart(8)} ${k}`),\n '',\n paint('bold', 'By file'),\n ...sorted(this.bySource).map(([k, n]) => ` ${String(n).padStart(8)} ${k}`),\n '',\n paint('bold', 'Most common messages'),\n ...[...this.byMessage]\n .sort((a, b) => b[1].count - a[1].count)\n .slice(0, 10)\n .map(([shape, m]) => ` ${String(m.count).padStart(8)} ${(m.level ?? '').padEnd(5)} ${shape}`),\n ];\n return lines.join('\\n');\n }\n}\n","import { parseArgs } from 'node:util';\nimport type { Readable, Writable } from 'node:stream';\nimport { buildFilter } from './filter.js';\nimport { followFiles } from './follow.js';\nimport { mergeByTime } from './merge.js';\nimport { Stats, formatJson, formatPretty } from './output.js';\nimport { expandInputs, labelSources, openStream, readEntries, type Source } from './sources.js';\n\nexport const HELP = `logsift: search many log files as one timeline\n\nUsage\n logsift [files, folders or globs...] [options]\n some-command | logsift [options]\n\nFilters (all must match)\n -l, --level <level> this level and above: trace, debug, info, warn, error, fatal\n -s, --since <time> 15m, 2h, 1d, 14:03 or 2026-11-05 14:03\n -u, --until <time> same formats as --since\n -g, --grep <text> text (any case), /regex/i or re:regex; repeat to require several\n -w, --where <expr> field filter: status>=500, user.id=42, msg~timeout; repeatable\n -r, --request <id> every line that mentions this request, trace or correlation ID\n -a, --around <time> lines near a moment, e.g. 14:03 (see --window)\n --window <span> size of --around on each side (default 1m)\n\nOutput\n --json one JSON object per line\n --stats counts by level and file, and the most common messages\n -n, --limit <n> stop after n matching lines\n -f, --follow keep watching the files for new lines\n --no-color plain text (also when NO_COLOR is set)\n -h, --help show this help\n -v, --version show the version\n\nExit code: 0 when something matched, 1 when nothing did, 2 on errors.`;\n\nexport interface RunIO {\n stdout: Writable;\n stderr: Writable;\n stdin: Readable & { isTTY?: boolean };\n cwd?: string;\n now?: Date;\n /** Colours on? Default: when stdout is a terminal and NO_COLOR isn't set. */\n color?: boolean;\n /** Called with a stop function when following, so tests can end it. */\n onFollow?: (stop: () => void) => void;\n version?: string;\n}\n\nconst write = (stream: Writable, text: string) =>\n new Promise<void>((resolve) => {\n if (stream.write(`${text}\\n`)) resolve();\n else stream.once('drain', resolve);\n });\n\n/** Runs the command line. Returns the exit code instead of exiting, so it can be tested. */\nexport async function run(argv: string[], io: RunIO): Promise<number> {\n let args;\n try {\n args = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n level: { type: 'string', short: 'l' },\n since: { type: 'string', short: 's' },\n until: { type: 'string', short: 'u' },\n grep: { type: 'string', short: 'g', multiple: true },\n where: { type: 'string', short: 'w', multiple: true },\n request: { type: 'string', short: 'r' },\n around: { type: 'string', short: 'a' },\n window: { type: 'string' },\n json: { type: 'boolean' },\n stats: { type: 'boolean' },\n limit: { type: 'string', short: 'n' },\n follow: { type: 'boolean', short: 'f' },\n 'no-color': { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean', short: 'v' },\n },\n });\n } catch (err) {\n await write(io.stderr, `logsift: ${(err as Error).message}\\nRun logsift --help to see the options.`);\n return 2;\n }\n const { values: o, positionals } = args;\n if (o.help) {\n await write(io.stdout, HELP);\n return 0;\n }\n if (o.version) {\n await write(io.stdout, io.version ?? 'unknown');\n return 0;\n }\n\n const now = io.now ?? new Date();\n const cwd = io.cwd ?? process.cwd();\n const color =\n !o['no-color'] &&\n (io.color ?? Boolean((io.stdout as { isTTY?: boolean }).isTTY && !process.env.NO_COLOR));\n\n try {\n const filter = buildFilter(o, now);\n const limit = o.limit === undefined ? Infinity : Number(o.limit);\n if (o.limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {\n throw new Error('--limit needs a whole number above 0.');\n }\n\n let sources: Source[];\n if (positionals.length > 0) {\n const files = await expandInputs(positionals, cwd);\n if (files.length === 0) throw new Error('No log files found for those inputs.');\n sources = labelSources(files, cwd);\n } else if (!io.stdin.isTTY) {\n sources = [{ name: 'stdin' }];\n } else {\n await write(io.stdout, HELP);\n return 2;\n }\n\n const sourceWidth = Math.min(24, Math.max(...sources.map((s) => s.name.length)));\n const stats = o.stats ? new Stats() : undefined;\n let matched = 0;\n const emit = async (entry: Parameters<typeof filter>[0]) => {\n if (!filter(entry)) return;\n matched++;\n if (stats) stats.add(entry);\n else await write(io.stdout, o.json ? formatJson(entry) : formatPretty(entry, { color, sourceWidth }));\n };\n\n const streams = sources.map((s) => readEntries(openStream(s, io.stdin), s.name, { now }));\n for await (const entry of mergeByTime(streams)) {\n await emit(entry);\n if (matched >= limit) break;\n }\n\n if (o.follow && matched < limit && sources.some((s) => s.path)) {\n await new Promise<void>((resolve) => {\n void followFiles(sources, (entry) => void emit(entry)).then((stop) => {\n const end = () => {\n stop();\n resolve();\n };\n process.once('SIGINT', end);\n io.onFollow?.(end);\n });\n });\n }\n\n if (stats) await write(io.stdout, stats.format(color));\n return matched > 0 ? 0 : 1;\n } catch (err) {\n await write(io.stderr, `logsift: ${(err as Error).message}`);\n return 2;\n }\n}\n"],"mappings":"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","names":["iso","t","createReadStream","stat","stat","createReadStream","resolve"]}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
run
|
|
4
|
+
} from "./chunk-QR2OWMYF.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
9
|
+
process.stdout.on("error", (err) => {
|
|
10
|
+
if (err.code === "EPIPE") process.exit(0);
|
|
11
|
+
throw err;
|
|
12
|
+
});
|
|
13
|
+
var code = await run(process.argv.slice(2), {
|
|
14
|
+
stdout: process.stdout,
|
|
15
|
+
stderr: process.stderr,
|
|
16
|
+
stdin: process.stdin,
|
|
17
|
+
version: pkg.version
|
|
18
|
+
});
|
|
19
|
+
process.exitCode = code;
|
|
20
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from 'node:fs';\nimport { run } from './run.js';\n\nconst pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {\n version: string;\n};\n\n// Stop quietly when the reader goes away, e.g. `logsift app.log | head`.\nprocess.stdout.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EPIPE') process.exit(0);\n throw err;\n});\n\nconst code = await run(process.argv.slice(2), {\n stdout: process.stdout,\n stderr: process.stderr,\n stdin: process.stdin,\n version: pkg.version,\n});\nprocess.exitCode = code;\n"],"mappings":";;;;;;AACA,SAAS,oBAAoB;AAG7B,IAAM,MAAM,KAAK,MAAM,aAAa,IAAI,IAAI,mBAAmB,YAAY,GAAG,GAAG,MAAM,CAAC;AAKxF,QAAQ,OAAO,GAAG,SAAS,CAAC,QAA+B;AACzD,MAAI,IAAI,SAAS,QAAS,SAAQ,KAAK,CAAC;AACxC,QAAM;AACR,CAAC;AAED,IAAM,OAAO,MAAM,IAAI,QAAQ,KAAK,MAAM,CAAC,GAAG;AAAA,EAC5C,QAAQ,QAAQ;AAAA,EAChB,QAAQ,QAAQ;AAAA,EAChB,OAAO,QAAQ;AAAA,EACf,SAAS,IAAI;AACf,CAAC;AACD,QAAQ,WAAW;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Writable, Readable } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
declare const HELP = "logsift: search many log files as one timeline\n\nUsage\n logsift [files, folders or globs...] [options]\n some-command | logsift [options]\n\nFilters (all must match)\n -l, --level <level> this level and above: trace, debug, info, warn, error, fatal\n -s, --since <time> 15m, 2h, 1d, 14:03 or 2026-11-05 14:03\n -u, --until <time> same formats as --since\n -g, --grep <text> text (any case), /regex/i or re:regex; repeat to require several\n -w, --where <expr> field filter: status>=500, user.id=42, msg~timeout; repeatable\n -r, --request <id> every line that mentions this request, trace or correlation ID\n -a, --around <time> lines near a moment, e.g. 14:03 (see --window)\n --window <span> size of --around on each side (default 1m)\n\nOutput\n --json one JSON object per line\n --stats counts by level and file, and the most common messages\n -n, --limit <n> stop after n matching lines\n -f, --follow keep watching the files for new lines\n --no-color plain text (also when NO_COLOR is set)\n -h, --help show this help\n -v, --version show the version\n\nExit code: 0 when something matched, 1 when nothing did, 2 on errors.";
|
|
4
|
+
interface RunIO {
|
|
5
|
+
stdout: Writable;
|
|
6
|
+
stderr: Writable;
|
|
7
|
+
stdin: Readable & {
|
|
8
|
+
isTTY?: boolean;
|
|
9
|
+
};
|
|
10
|
+
cwd?: string;
|
|
11
|
+
now?: Date;
|
|
12
|
+
/** Colours on? Default: when stdout is a terminal and NO_COLOR isn't set. */
|
|
13
|
+
color?: boolean;
|
|
14
|
+
/** Called with a stop function when following, so tests can end it. */
|
|
15
|
+
onFollow?: (stop: () => void) => void;
|
|
16
|
+
version?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Runs the command line. Returns the exit code instead of exiting, so it can be tested. */
|
|
19
|
+
declare function run(argv: string[], io: RunIO): Promise<number>;
|
|
20
|
+
|
|
21
|
+
declare const LEVELS: readonly ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
22
|
+
type Level = (typeof LEVELS)[number];
|
|
23
|
+
/** One log entry, whatever format it came from. */
|
|
24
|
+
interface Entry {
|
|
25
|
+
/** Milliseconds since 1970, or undefined when the line has no time. */
|
|
26
|
+
time?: number;
|
|
27
|
+
level?: Level;
|
|
28
|
+
msg: string;
|
|
29
|
+
/** Structured fields (JSON or logfmt), without the time, level and message keys. */
|
|
30
|
+
fields: Record<string, unknown>;
|
|
31
|
+
/** The original text, including continuation lines such as stack traces. */
|
|
32
|
+
raw: string;
|
|
33
|
+
/** Short name of the file it came from. */
|
|
34
|
+
source: string;
|
|
35
|
+
/** 1-based line number of the first line in its file. */
|
|
36
|
+
line: number;
|
|
37
|
+
format: 'json' | 'logfmt' | 'text';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Parsed = Omit<Entry, 'source' | 'line' | 'raw'>;
|
|
41
|
+
/** Level from a word ("WARN") or a pino/bunyan number (40). */
|
|
42
|
+
declare function toLevel(value: unknown): Level | undefined;
|
|
43
|
+
/** Time in ms from a number (seconds or ms) or a date string. */
|
|
44
|
+
declare function toTime(value: unknown, now?: Date): number | undefined;
|
|
45
|
+
/** Parses `key=value key2="quoted value"`; returns undefined when it doesn't look like logfmt. */
|
|
46
|
+
declare function parseLogfmt(line: string): Record<string, unknown> | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Parses one line. Returns `continuation: true` for lines that belong to the
|
|
49
|
+
* entry before them, such as stack trace lines.
|
|
50
|
+
*/
|
|
51
|
+
declare function parseLine(line: string, now?: Date): (Parsed & {
|
|
52
|
+
continuation: boolean;
|
|
53
|
+
}) | undefined;
|
|
54
|
+
|
|
55
|
+
interface FilterOptions {
|
|
56
|
+
/** Minimum level, e.g. "warn" shows warn, error and fatal. */
|
|
57
|
+
level?: string;
|
|
58
|
+
since?: string;
|
|
59
|
+
until?: string;
|
|
60
|
+
/** Text (case-insensitive) or /regex/flags. Several are all required. */
|
|
61
|
+
grep?: string[];
|
|
62
|
+
/** Field filters like `status>=500`, `user.id=42`, `msg~timeout`. */
|
|
63
|
+
where?: string[];
|
|
64
|
+
/** Show only lines that mention this request, trace or correlation ID. */
|
|
65
|
+
request?: string;
|
|
66
|
+
/** A moment in time; shows lines within `window` of it. */
|
|
67
|
+
around?: string;
|
|
68
|
+
/** Size of the `around` window, e.g. "2m". Default 1m. */
|
|
69
|
+
window?: string;
|
|
70
|
+
}
|
|
71
|
+
/** Field names that usually hold a request, trace or correlation ID. */
|
|
72
|
+
declare const REQUEST_ID_KEYS: string[];
|
|
73
|
+
/** "90s", "15m", "2h", "1d" → milliseconds. */
|
|
74
|
+
declare function parseDuration(text: string): number | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* A point in time: a duration ago ("15m"), a time today ("14:03" or "14:03:30"),
|
|
77
|
+
* or a date ("2026-11-05", "2026-11-05 14:03").
|
|
78
|
+
*/
|
|
79
|
+
declare function parseMoment(text: string, now: Date): number;
|
|
80
|
+
/** `key op value`, with op one of = != >= <= > < ~ (contains / regex). */
|
|
81
|
+
declare function parseWhere(expr: string): (e: Entry) => boolean;
|
|
82
|
+
/** Builds one test from all the filter options. Throws a readable error on bad input. */
|
|
83
|
+
declare function buildFilter(options: FilterOptions, now?: Date): (e: Entry) => boolean;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Merges several time-ordered streams into one, oldest first. Each stream is
|
|
87
|
+
* read one entry at a time, so memory stays small however big the files are.
|
|
88
|
+
* Entries with no time at all sort first; ties keep the input order.
|
|
89
|
+
*/
|
|
90
|
+
declare function mergeByTime(streams: AsyncIterable<Entry>[]): AsyncGenerator<Entry>;
|
|
91
|
+
|
|
92
|
+
interface Source {
|
|
93
|
+
/** Short label shown next to each line. */
|
|
94
|
+
name: string;
|
|
95
|
+
/** Full path, or undefined for stdin. */
|
|
96
|
+
path?: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Turns the inputs (files, folders, globs) into a sorted list of files.
|
|
100
|
+
* Globs are expanded here, so they work on Windows shells too.
|
|
101
|
+
*/
|
|
102
|
+
declare function expandInputs(inputs: string[], cwd?: string): Promise<string[]>;
|
|
103
|
+
/** Short, unique labels: the file name, or a relative path when names repeat. */
|
|
104
|
+
declare function labelSources(paths: string[], cwd?: string): Source[];
|
|
105
|
+
declare function openStream(source: Source, stdin?: Readable): Readable;
|
|
106
|
+
/**
|
|
107
|
+
* Reads a stream into entries. Lines that continue the entry before them
|
|
108
|
+
* (stack traces, wrapped JSON) are folded into it. Entries without a time take
|
|
109
|
+
* the time of the entry before them, so they keep their place in the timeline.
|
|
110
|
+
*/
|
|
111
|
+
declare function readEntries(stream: Readable, source: string, options?: {
|
|
112
|
+
now?: Date;
|
|
113
|
+
startLine?: number;
|
|
114
|
+
}): AsyncGenerator<Entry>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Watches files for new lines and passes each new entry to `onEntry`.
|
|
118
|
+
* Polls file sizes, which works the same on Windows, macOS and Linux.
|
|
119
|
+
* Returns a function that stops watching.
|
|
120
|
+
*/
|
|
121
|
+
declare function followFiles(sources: Source[], onEntry: (entry: Entry) => void, options?: {
|
|
122
|
+
intervalMs?: number;
|
|
123
|
+
}): Promise<() => void>;
|
|
124
|
+
|
|
125
|
+
/** One readable line (plus any continuation lines) for the terminal. */
|
|
126
|
+
declare function formatPretty(entry: Entry, options: {
|
|
127
|
+
color: boolean;
|
|
128
|
+
sourceWidth: number;
|
|
129
|
+
}): string;
|
|
130
|
+
/** One JSON object per line, ready for jq or another tool. */
|
|
131
|
+
declare function formatJson(entry: Entry): string;
|
|
132
|
+
declare class Stats {
|
|
133
|
+
total: number;
|
|
134
|
+
first?: number;
|
|
135
|
+
last?: number;
|
|
136
|
+
readonly byLevel: Map<string, number>;
|
|
137
|
+
readonly bySource: Map<string, number>;
|
|
138
|
+
readonly byMessage: Map<string, {
|
|
139
|
+
count: number;
|
|
140
|
+
level?: Level;
|
|
141
|
+
}>;
|
|
142
|
+
add(entry: Entry): void;
|
|
143
|
+
format(color: boolean): string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { HELP, LEVELS, REQUEST_ID_KEYS, Stats, buildFilter, expandInputs, followFiles, formatJson, formatPretty, labelSources, mergeByTime, openStream, parseDuration, parseLine, parseLogfmt, parseMoment, parseWhere, readEntries, run, toLevel, toTime };
|
|
147
|
+
export type { Entry, FilterOptions, Level, RunIO, Source };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HELP,
|
|
3
|
+
LEVELS,
|
|
4
|
+
REQUEST_ID_KEYS,
|
|
5
|
+
Stats,
|
|
6
|
+
buildFilter,
|
|
7
|
+
expandInputs,
|
|
8
|
+
followFiles,
|
|
9
|
+
formatJson,
|
|
10
|
+
formatPretty,
|
|
11
|
+
labelSources,
|
|
12
|
+
mergeByTime,
|
|
13
|
+
openStream,
|
|
14
|
+
parseDuration,
|
|
15
|
+
parseLine,
|
|
16
|
+
parseLogfmt,
|
|
17
|
+
parseMoment,
|
|
18
|
+
parseWhere,
|
|
19
|
+
readEntries,
|
|
20
|
+
run,
|
|
21
|
+
toLevel,
|
|
22
|
+
toTime
|
|
23
|
+
} from "./chunk-QR2OWMYF.js";
|
|
24
|
+
export {
|
|
25
|
+
HELP,
|
|
26
|
+
LEVELS,
|
|
27
|
+
REQUEST_ID_KEYS,
|
|
28
|
+
Stats,
|
|
29
|
+
buildFilter,
|
|
30
|
+
expandInputs,
|
|
31
|
+
followFiles,
|
|
32
|
+
formatJson,
|
|
33
|
+
formatPretty,
|
|
34
|
+
labelSources,
|
|
35
|
+
mergeByTime,
|
|
36
|
+
openStream,
|
|
37
|
+
parseDuration,
|
|
38
|
+
parseLine,
|
|
39
|
+
parseLogfmt,
|
|
40
|
+
parseMoment,
|
|
41
|
+
parseWhere,
|
|
42
|
+
readEntries,
|
|
43
|
+
run,
|
|
44
|
+
toLevel,
|
|
45
|
+
toTime
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ragib.mrb/logsift",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Search many log files as one timeline: any format, filters, and follow one request across services.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ragib MRB",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ragib-mrb/logsift.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/ragib-mrb/logsift#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ragib-mrb/logsift/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"logsift": "dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"logs",
|
|
35
|
+
"log-viewer",
|
|
36
|
+
"log-search",
|
|
37
|
+
"grep",
|
|
38
|
+
"json-logs",
|
|
39
|
+
"pino",
|
|
40
|
+
"winston",
|
|
41
|
+
"bunyan",
|
|
42
|
+
"cli",
|
|
43
|
+
"request-tracing"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"lint": "eslint .",
|
|
49
|
+
"format": "prettier --write .",
|
|
50
|
+
"format:check": "prettier --check .",
|
|
51
|
+
"test": "vitest run"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@eslint/js": "10.0.1",
|
|
55
|
+
"@types/node": "26.6.6",
|
|
56
|
+
"eslint": "10.13.0",
|
|
57
|
+
"prettier": "3.9.10",
|
|
58
|
+
"tsup": "9.0.0",
|
|
59
|
+
"typescript": "6.0.3",
|
|
60
|
+
"typescript-eslint": "8.72.1",
|
|
61
|
+
"vitest": "5.0.5"
|
|
62
|
+
}
|
|
63
|
+
}
|