├── LICENSE
├── README.md
├── index.mjs
├── out.js
├── package.json
├── public
├── command-palette
│ ├── index.html
│ ├── index.js
│ └── style.css
├── context-menu
│ ├── index.html
│ ├── index.js
│ └── style.css
├── filter.js
├── index.html
├── index.js
├── lib.js
├── log-adder.js
├── style.css
├── tags.js
└── worker
│ ├── highlight.js
│ └── index.js
├── tsconfig.json
└── types.d.ts
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 EmNudge
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Logpipe ![logpipe logo][logo]
2 |
3 | *Get clarity on development logs*
4 |
5 |
6 |
7 | Checkout [the docs][docs] or see it in action in [this online demo][demo]!
8 |
9 | ## Installation
10 |
11 | There is no build step in this codebase, so you can install it directly from git if you want.
12 |
13 | ```sh
14 | npm i -g https://github.com/EmNudge/logpipe.git
15 | # or
16 | npm i -g @emnudge/logpipe
17 | ```
18 |
19 | ## Usage
20 |
21 | ```sh
22 | $ some-other-program | logpipe
23 |
24 | > server running on http://localhost:7381
25 | ```
26 |
27 | Then go to the URL and inspect away!
28 |
29 | `logpipe` parameters:
30 | - `--port `
31 | - Choose a specific port (instead of random). Useful for command runners like `nodemon`.
32 | - `--title `
33 | - Title for the page. Useful if you have multiple `logpipe`s open at once.
34 |
35 |
36 | ### Redirecting Stderr
37 |
38 | Many programs will output their logs to `stderr` instead of `stdout`. If `logpipe` is not capturing anything and you still see output in your terminal, this is probably what's happening.
39 |
40 | You can use bash redirection to fix this.
41 |
42 | ```sh
43 | my-program 2>&1 | logpipe # note the "2>&1"
44 | ```
45 |
46 | Check out [the docs](https://logpipe.dev/guide/shell-redirection.html#shell-redirection) for more info on shell redirection.
47 |
48 | ## Motivation
49 |
50 | When dealing with various codebases in development, you'll come across perhaps hundreds of logs per minute. Some of these are supposed to be useful.
51 | As it's just sending out unstructured text, it can be hard to find what you want. Furthermore, it often lacks syntax highlighting. It is hard to know where one log ends and another starts.
52 |
53 | In contrast, something like your browser's dev console allows filtering, highlighting, and inspection of "unstructured" logs. The goal is to supercharge this to be used for any system that outputs any kind of logs.
54 |
55 | ## Behavior
56 |
57 | This is a tool meant primarily for development. Therefore, the intention is to value assistance over correctness.
58 |
59 | This allows us the following interesting features:
60 | - **Logpipe** will syntax highlight logs that previously had no syntax highlighting.
61 | - It will automatically apply tags to logs for you to search over.
62 | - It will attempt to group logs that seem related (based on indentation or language grammar)
63 |
64 | It also allows us to live-filter logs while retaining the log state - something already present in most log inspection tools.
65 |
66 | ## Advanced Usage, Alternative Tools, Contribution Guide, etc
67 |
68 | A lot of info has been moved to [the docs][docs]. Here are some quick links:
69 |
70 | - [Alternatives](https://logpipe.emnudge.dev/guide/alternatives.html)
71 | - [Shell Redirection](https://logpipe.emnudge.dev/guide/shell-redirection.html)
72 | - [Filtering](https://logpipe.emnudge.dev/guide/filtering.html)
73 | - [Contribution Guide](https://logpipe.emnudge.dev/guide/contribution-guide.html)
74 |
75 |
76 | [logo]: https://github.com/EmNudge/logpipe/assets/24513691/8526ba7d-e8a1-460a-8fad-60c488b5b15e
77 | [demo]: https://logpipe.pages.dev
78 | [docs]: https://logpipe.dev
79 |
80 |
--------------------------------------------------------------------------------
/index.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import http from "http";
4 | import fs from "fs/promises";
5 | import { join } from "path";
6 | import { fileURLToPath } from "url";
7 |
8 | const argv = /** @type {string[]} */ (process.argv.slice(2));
9 | /** @param {string} name */
10 | const findArg = (name) => {
11 | const index = argv.indexOf(name);
12 | if (index === -1) return;
13 | if (!argv[index + 1] || argv[index + 1].startsWith("-")) return "true";
14 | return argv[index + 1];
15 | };
16 |
17 | if (findArg("--version") || findArg("-v")) {
18 | const packageJsonpath = join(
19 | fileURLToPath(import.meta.url),
20 | "..",
21 | "package.json"
22 | );
23 | const packageJson = await fs.readFile(packageJsonpath, "utf-8");
24 | const { version } = JSON.parse(packageJson);
25 | console.log(version);
26 | process.exit();
27 | }
28 |
29 | // user asked for CLI help
30 | if (findArg("--help") || findArg("-h")) {
31 | console.log(
32 | [
33 | "Usage: logpipe [--port PORT] [--title TITLE]",
34 | "",
35 | "Options:",
36 | " --port The port to run the web server on",
37 | " --title The title of the web page",
38 | " --help This menu",
39 | " --version The version of logpipe",
40 | "",
41 | "Examples:",
42 | " your-program | logpipe",
43 | ' your-program | logpipe --port 8080 --title "My CLI Input"',
44 | ' your-program | logpipe -p 8080 -t "My CLI Input"',
45 | "",
46 | ].join("\n")
47 | );
48 | process.exit();
49 | }
50 |
51 | const port = Number(findArg("--port") ?? findArg("-p")) || 0;
52 | const DEFAULT_TITLE = "CLI Input";
53 | const title = findArg("--title") ?? findArg("-t") ?? "CLI Input";
54 |
55 | /** @typedef {import('./types.d.ts').CliInput} CliInput */
56 |
57 | /** @type {CliInput[]} */
58 | const lines = [];
59 |
60 | /** @type {Set<(lines: CliInput[], newLines: CliInput[]) => void>} */
61 | const notifiers = new Set();
62 |
63 | process.stdin.on("data", (data) => {
64 | /** @type {string} */
65 | const input = data.toString();
66 | const date = Date.now();
67 | const id = String(lines.length);
68 |
69 | /** @type {CliInput[]} */
70 | const newLines = [];
71 | let prevWhitespace = 0;
72 | for (const line of input.trim().split("\n")) {
73 | const curWhitespace = line.match(/^\s+/)?.[0].length;
74 | if ((curWhitespace > prevWhitespace || line === "}") && newLines.length) {
75 | const { input } = newLines.pop();
76 | newLines.push({ input: [input, line].join("\n"), date, id });
77 | } else {
78 | newLines.push({ input: line, date, id });
79 | }
80 | }
81 |
82 | lines.push(...newLines);
83 |
84 | for (const func of notifiers) {
85 | func(lines, newLines);
86 | }
87 | });
88 |
89 | process.stdin.on("close", () => {
90 | console.log("\x1b[31mInput has ended.\x1b[0m");
91 | console.log("Use Ctrl+C to close the web server.");
92 | });
93 |
94 | /** @param {string} path */
95 | const getMimeTypeForFile = (path) => {
96 | const ext = path.split(".").slice(-1)[0];
97 |
98 | /** @type {string} */
99 | const mimeType = {
100 | js: "text/javascript",
101 | css: "text/css",
102 | html: "text/html",
103 | png: "image/png",
104 | jpg: "image/jpg",
105 | jpeg: "image/jpeg",
106 | ico: "image/x-icon",
107 | svg: "image/svg+xml",
108 | json: "application/json",
109 | }[ext];
110 | return mimeType ?? "text/plain";
111 | };
112 |
113 | const PUBLIC_DIR = join(fileURLToPath(import.meta.url), "..", "public");
114 |
115 | const publicFiles = new Set();
116 | fs.readdir(PUBLIC_DIR).then((files) =>
117 | Promise.all(
118 | files.map(async (file) => {
119 | const stat = await fs.stat(join(PUBLIC_DIR, file));
120 | if (!stat.isDirectory()) {
121 | publicFiles.add(file);
122 | return;
123 | }
124 | const curFolder = join(PUBLIC_DIR, file);
125 | for (const subFile of await fs.readdir(curFolder)) {
126 | publicFiles.add(join(file, subFile));
127 | }
128 | })
129 | )
130 | );
131 |
132 | const server = http
133 | .createServer(async (req, res) => {
134 | if (req.method !== "GET") return;
135 |
136 | if (req.url === "/_/cli-input") {
137 | res.writeHead(200, {
138 | "Content-Type": "text/event-stream",
139 | Connection: "keep-alive",
140 | "Cache-Control": "no-cache",
141 | });
142 |
143 | res.write(`data: ${JSON.stringify(lines)}\n\n`);
144 | notifiers.add((_lines, newLines) => {
145 | res.write(`data: ${JSON.stringify(newLines)}\n\n`);
146 | });
147 | return;
148 | }
149 |
150 | // We could directly write to FS, but we shouldn't trust the user to specify download location information correctly.
151 | // This means we'd need a lot of error handling. It's easier to let the browser handle file downloads.
152 | if (req.url === "/_/logs" && req.method === "GET") {
153 | res.writeHead(200, { "Content-Type": "application/json" });
154 | res.write(JSON.stringify(lines));
155 | res.end();
156 | return;
157 | }
158 |
159 | if (req.url == "/" || req.url == "/index.html") {
160 | res.writeHead(200, {
161 | "Content-Type": "text/html",
162 | "X-Custom-Title": title,
163 | });
164 | /** @type {string} */
165 | const site = (await fs.readFile(join(PUBLIC_DIR, "index.html"), "utf8"))
166 | .replace(/.+?<\/title>/g, `${title}`)
167 | .replace(/
.+?<\/h1>/g, `
${title}
`);
168 | res.write(site);
169 | res.end();
170 | return;
171 | } else if (publicFiles.has(req.url.slice(1))) {
172 | res.writeHead(200, { "Content-Type": getMimeTypeForFile(req.url) });
173 | res.write(await fs.readFile(join(PUBLIC_DIR, req.url.slice(1))));
174 | res.end();
175 | return;
176 | }
177 |
178 | res.writeHead(404, { "Content-Type": "text/html" });
179 | res.write("Resource not found");
180 | res.end();
181 | })
182 | .listen(port);
183 |
184 | if (!server.address() && port) {
185 | console.error("\nServer could not bind to port", port);
186 | console.log(
187 | "Specify a different port or allow the server to choose a random port."
188 | );
189 | process.exit(1);
190 | }
191 |
192 | const address = `http://localhost:${server.address().port}`;
193 | let log = `Logs are displayed on \x1b[32;1;4m${address}\x1b[0m`
194 | if (title !== DEFAULT_TITLE) {
195 | log += ` with title \x1b[34;3m"${title}"\x1b[0m`;
196 | }
197 | console.log(`\n${log}\n`);
198 |
--------------------------------------------------------------------------------
/out.js:
--------------------------------------------------------------------------------
1 | const { iterations, delay } = (() => {
2 | const args = process.argv.slice(2).join(" ");
3 |
4 | const iterations = (() => {
5 | const match =
6 | args.match(/--iterations\s+(\d+)/) ?? args.match(/-i\s*(\d+)/);
7 | return match ? Number(match[1]) : Infinity;
8 | })();
9 | const delay = (() => {
10 | const match = args.match(/--delay\s+(\d+)/) ?? args.match(/-d\s*(\d+)/);
11 | return match ? Number(match[1]) : 2000;
12 | })();
13 |
14 | return { iterations, delay };
15 | })();
16 |
17 | let count = 0;
18 | const intId = setInterval(logRandom, delay);
19 | function logRandom() {
20 | if (++count > iterations) {
21 | clearInterval(intId);
22 | return;
23 | }
24 |
25 | const branches = [
26 | "\x1B]8;;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles\x1B\\`dev` profile [unoptimized + debuginfo]\x1B]8;;\x1B\\",
27 | "\x1B[31m [ANSI] Hello There! \x1B[0m",
28 | `[INFO] ${new Date().toISOString()} "log random" ${count}`,
29 | "Info 2024-04-08 19:10:00,779 /opt/tmp/file_seq.data",
30 | "[Error(danger)] An error occurred [42] seconds ago due to a false value when expecting a true value",
31 | "failed to throw an error. This is surprisingly not a good thing.",
32 | "[INFO] /opt/file_thing.sock:19 took 232.21ms | all systems go",
33 | "on http://localhost:8080 (127.0.0.1) someone cool made a GET request (source: file_thing.js:32)",
34 | '["this is not a tag"] action took 32h2m1.13s at file_info.go:(12,31)',
35 | `stringified ${JSON.stringify(
36 | { a: 1, b: 2, c: Math.random() * 50 },
37 | null,
38 | 2
39 | )}`,
40 | "Debug received POST, GET, PATCH, PUT, and DELETE from 192.168.1.1 (somehow)",
41 | ];
42 | const log = branches[(branches.length * Math.random()) | 0];
43 | console.log(log);
44 | }
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@emnudge/logpipe",
3 | "version": "0.0.9",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/EmNudge/logpipe.git"
7 | },
8 | "scripts": {
9 | "dev": "nodemon --exec 'node out.js | node index.mjs -p 7280' -e ts,html,js,mjs,css"
10 | },
11 | "bin": {
12 | "logpipe": "./index.mjs"
13 | },
14 | "author": "EmNudge",
15 | "license": "MIT"
16 | }
--------------------------------------------------------------------------------
/public/command-palette/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
76 |
77 |
87 |
88 |
99 |
100 |
167 |
168 |
169 |