otelly 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 +76 -0
- package/dist/cli.js +224 -0
- package/dist/server.js +676 -0
- package/dist/static/404/index.html +1 -0
- package/dist/static/404.html +1 -0
- package/dist/static/__next.__PAGE__.txt +9 -0
- package/dist/static/__next._full.txt +17 -0
- package/dist/static/__next._head.txt +5 -0
- package/dist/static/__next._index.txt +5 -0
- package/dist/static/__next._tree.txt +2 -0
- package/dist/static/_next/static/chunks/01l47c09f~n66.js +1 -0
- package/dist/static/_next/static/chunks/01mhk8rsi88af.js +31 -0
- package/dist/static/_next/static/chunks/02aa95iuhqxu7.css +1 -0
- package/dist/static/_next/static/chunks/03~yq9q893hmn.js +1 -0
- package/dist/static/_next/static/chunks/06cclqnbyt80n.js +1 -0
- package/dist/static/_next/static/chunks/0o-7hx1honk9g.js +1 -0
- package/dist/static/_next/static/chunks/149l7j7ef19ra.js +5 -0
- package/dist/static/_next/static/chunks/turbopack-0ikepo8r-2zp~.js +1 -0
- package/dist/static/_next/static/d1qv-JAQphjG0HTu0Ryte/_buildManifest.js +11 -0
- package/dist/static/_next/static/d1qv-JAQphjG0HTu0Ryte/_clientMiddlewareManifest.js +1 -0
- package/dist/static/_next/static/d1qv-JAQphjG0HTu0Ryte/_ssgManifest.js +1 -0
- package/dist/static/_not-found/__next._full.txt +15 -0
- package/dist/static/_not-found/__next._head.txt +5 -0
- package/dist/static/_not-found/__next._index.txt +5 -0
- package/dist/static/_not-found/__next._not-found.__PAGE__.txt +5 -0
- package/dist/static/_not-found/__next._not-found.txt +5 -0
- package/dist/static/_not-found/__next._tree.txt +2 -0
- package/dist/static/_not-found/index.html +1 -0
- package/dist/static/_not-found/index.txt +15 -0
- package/dist/static/index.html +1 -0
- package/dist/static/index.txt +17 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Darna Digital
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# otelly
|
|
2
|
+
|
|
3
|
+
Local OpenTelemetry inspector. One command, one port, no infrastructure.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx otelly
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Boots an OTLP/HTTP collector + a dashboard on `http://localhost:4319`, stores
|
|
10
|
+
spans in `./otelly.sqlite`, opens your browser. Like `drizzle-kit studio` —
|
|
11
|
+
for traces.
|
|
12
|
+
|
|
13
|
+
## Use it
|
|
14
|
+
|
|
15
|
+
Point any OpenTelemetry SDK at `http://localhost:4319` (the **base URL** —
|
|
16
|
+
the SDK appends `/v1/traces` itself):
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
20
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
21
|
+
|
|
22
|
+
new NodeSDK({
|
|
23
|
+
serviceName: "my-app",
|
|
24
|
+
traceExporter: new OTLPTraceExporter({
|
|
25
|
+
url: "http://localhost:4319/v1/traces",
|
|
26
|
+
}),
|
|
27
|
+
}).start();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or `curl` a hand-crafted OTLP payload:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
curl -X POST http://localhost:4319/v1/traces \
|
|
34
|
+
-H 'content-type: application/json' \
|
|
35
|
+
-d '{"resourceSpans":[{"resource":{"attributes":[
|
|
36
|
+
{"key":"service.name","value":{"stringValue":"demo"}}]},
|
|
37
|
+
"scopeSpans":[{"spans":[{
|
|
38
|
+
"traceId":"00000000000000000000000000000001",
|
|
39
|
+
"spanId":"0000000000000001","name":"hello","kind":1,
|
|
40
|
+
"startTimeUnixNano":"1700000000000000000",
|
|
41
|
+
"endTimeUnixNano":"1700000000005000000",
|
|
42
|
+
"status":{"code":1}}]}]}]}'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Refresh the browser — span shows up.
|
|
46
|
+
|
|
47
|
+
## Options
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
otelly # all defaults
|
|
51
|
+
otelly --port 4500 # different port
|
|
52
|
+
otelly --db ./traces.sqlite # different SQLite file
|
|
53
|
+
otelly --no-open # skip browser auto-open
|
|
54
|
+
otelly -h # full help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
All flags are also env vars (`OTELLY_PORT`, `OTELLY_DB`).
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
- `POST /v1/traces` — OTLP/HTTP JSON ingest
|
|
62
|
+
- `GET /api/spans?limit=200` — recent spans
|
|
63
|
+
- `GET /api/traces/:id` — all spans for one trace
|
|
64
|
+
- `GET /health` — health probe
|
|
65
|
+
- `GET /openapi` — OpenAPI spec
|
|
66
|
+
- `GET /` — dashboard UI
|
|
67
|
+
|
|
68
|
+
## What this isn't
|
|
69
|
+
|
|
70
|
+
The CLI is for **local development inspection** — one user, one machine,
|
|
71
|
+
one project. For team observability or multi-tenant deployments, see
|
|
72
|
+
[self-hosting docs](https://github.com/Darna-Digital/otelly#self-host).
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { setTimeout } from "node:timers/promises";
|
|
7
|
+
|
|
8
|
+
//#region src/args.ts
|
|
9
|
+
const HELP = `otelly — local OpenTelemetry inspector
|
|
10
|
+
|
|
11
|
+
USAGE
|
|
12
|
+
otelly [command] [options]
|
|
13
|
+
|
|
14
|
+
COMMANDS
|
|
15
|
+
dev (default) start the UI + server with hot reload
|
|
16
|
+
start start the UI + server in production mode (requires build)
|
|
17
|
+
|
|
18
|
+
OPTIONS
|
|
19
|
+
--port <number> UI port (default 4319)
|
|
20
|
+
--api-port <number> OTLP + /api/* port (default 4318)
|
|
21
|
+
--db <path> SQLite file path (default ./otelly.sqlite)
|
|
22
|
+
--no-open do not open the UI in a browser
|
|
23
|
+
-h, --help show this help
|
|
24
|
+
|
|
25
|
+
ENV
|
|
26
|
+
OTELLY_DB same as --db
|
|
27
|
+
OTELLY_PORT same as --port
|
|
28
|
+
OTELLY_API_PORT same as --api-port
|
|
29
|
+
|
|
30
|
+
EXAMPLES
|
|
31
|
+
otelly
|
|
32
|
+
otelly --port 4000 --api-port 4001 --db ./traces.sqlite
|
|
33
|
+
`;
|
|
34
|
+
const parseInt10 = (s) => {
|
|
35
|
+
const n = Number(s);
|
|
36
|
+
if (!Number.isInteger(n) || n <= 0) return null;
|
|
37
|
+
return n;
|
|
38
|
+
};
|
|
39
|
+
const parseArgs = (argv) => {
|
|
40
|
+
let command = "dev";
|
|
41
|
+
let port = 4319;
|
|
42
|
+
let apiPort = 4318;
|
|
43
|
+
let dbPath;
|
|
44
|
+
let open = true;
|
|
45
|
+
let showHelp = false;
|
|
46
|
+
const positional = [];
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const arg = argv[i];
|
|
49
|
+
if (arg === void 0) continue;
|
|
50
|
+
if (arg === "-h" || arg === "--help") {
|
|
51
|
+
showHelp = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === "--port") {
|
|
55
|
+
const next = argv[i + 1];
|
|
56
|
+
if (next === void 0) return { error: "--port requires a value" };
|
|
57
|
+
const n = parseInt10(next);
|
|
58
|
+
if (n === null) return { error: `invalid --port: ${next}` };
|
|
59
|
+
port = n;
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg.startsWith("--port=")) {
|
|
64
|
+
const n = parseInt10(arg.slice(7));
|
|
65
|
+
if (n === null) return { error: `invalid --port: ${arg}` };
|
|
66
|
+
port = n;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === "--api-port") {
|
|
70
|
+
const next = argv[i + 1];
|
|
71
|
+
if (next === void 0) return { error: "--api-port requires a value" };
|
|
72
|
+
const n = parseInt10(next);
|
|
73
|
+
if (n === null) return { error: `invalid --api-port: ${next}` };
|
|
74
|
+
apiPort = n;
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg.startsWith("--api-port=")) {
|
|
79
|
+
const n = parseInt10(arg.slice(11));
|
|
80
|
+
if (n === null) return { error: `invalid --api-port: ${arg}` };
|
|
81
|
+
apiPort = n;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg === "--db") {
|
|
85
|
+
const next = argv[i + 1];
|
|
86
|
+
if (next === void 0) return { error: "--db requires a value" };
|
|
87
|
+
dbPath = next;
|
|
88
|
+
i += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg.startsWith("--db=")) {
|
|
92
|
+
dbPath = arg.slice(5);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (arg === "--no-open") {
|
|
96
|
+
open = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (arg === "--open") {
|
|
100
|
+
open = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (arg.startsWith("--")) return { error: `unknown option: ${arg}` };
|
|
104
|
+
positional.push(arg);
|
|
105
|
+
}
|
|
106
|
+
const envPort = process.env.OTELLY_PORT;
|
|
107
|
+
if (envPort !== void 0) {
|
|
108
|
+
const n = parseInt10(envPort);
|
|
109
|
+
if (n !== null) port = n;
|
|
110
|
+
}
|
|
111
|
+
const envApiPort = process.env.OTELLY_API_PORT;
|
|
112
|
+
if (envApiPort !== void 0) {
|
|
113
|
+
const n = parseInt10(envApiPort);
|
|
114
|
+
if (n !== null) apiPort = n;
|
|
115
|
+
}
|
|
116
|
+
if (dbPath === void 0 && process.env.OTELLY_DB) dbPath = process.env.OTELLY_DB;
|
|
117
|
+
if (positional.length > 0) {
|
|
118
|
+
const cmd = positional[0];
|
|
119
|
+
if (cmd === "dev" || cmd === "start") command = cmd;
|
|
120
|
+
else return { error: `unknown command: ${cmd}` };
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
command,
|
|
124
|
+
port,
|
|
125
|
+
apiPort,
|
|
126
|
+
dbPath,
|
|
127
|
+
open,
|
|
128
|
+
showHelp
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const helpText = HELP;
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/cli.ts
|
|
135
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
136
|
+
const resolveDbPath = (override) => {
|
|
137
|
+
if (override !== void 0) return path.resolve(override);
|
|
138
|
+
return path.resolve(process.cwd(), "otelly.sqlite");
|
|
139
|
+
};
|
|
140
|
+
const resolveStaticDir = () => {
|
|
141
|
+
const bundled = path.join(here, "static");
|
|
142
|
+
if (existsSync(bundled)) return bundled;
|
|
143
|
+
const source = path.resolve(here, "../../local/out");
|
|
144
|
+
if (existsSync(source)) return source;
|
|
145
|
+
throw new Error("otelly: static UI not found. Did you run `pnpm build` in apps/cli?");
|
|
146
|
+
};
|
|
147
|
+
const resolveServerPath = () => {
|
|
148
|
+
const bundled = path.join(here, "server.js");
|
|
149
|
+
if (existsSync(bundled)) return bundled;
|
|
150
|
+
throw new Error("otelly: server bundle not found. Did you run `pnpm build` in apps/cli?");
|
|
151
|
+
};
|
|
152
|
+
const waitForReady = async (url, timeoutMs = 3e4) => {
|
|
153
|
+
const deadline = Date.now() + timeoutMs;
|
|
154
|
+
while (Date.now() < deadline) {
|
|
155
|
+
try {
|
|
156
|
+
if ((await fetch(url, { method: "GET" })).status < 500) return true;
|
|
157
|
+
} catch {}
|
|
158
|
+
await setTimeout(200);
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
};
|
|
162
|
+
const openBrowser = (url) => {
|
|
163
|
+
const platform = process.platform;
|
|
164
|
+
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", [
|
|
165
|
+
"/c",
|
|
166
|
+
"start",
|
|
167
|
+
"",
|
|
168
|
+
url
|
|
169
|
+
]] : ["xdg-open", [url]];
|
|
170
|
+
const child = spawn(cmd, [...args], {
|
|
171
|
+
stdio: "ignore",
|
|
172
|
+
detached: true
|
|
173
|
+
});
|
|
174
|
+
child.on("error", () => {
|
|
175
|
+
process.stderr.write(`otelly: could not open browser (${cmd}). Visit ${url} manually.\n`);
|
|
176
|
+
});
|
|
177
|
+
child.unref();
|
|
178
|
+
};
|
|
179
|
+
const main = async () => {
|
|
180
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
181
|
+
if ("error" in parsed) {
|
|
182
|
+
process.stderr.write(`otelly: ${parsed.error}\n\n${helpText}`);
|
|
183
|
+
process.exit(2);
|
|
184
|
+
}
|
|
185
|
+
if (parsed.showHelp) {
|
|
186
|
+
process.stdout.write(helpText);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const dbPath = resolveDbPath(parsed.dbPath);
|
|
190
|
+
const staticDir = resolveStaticDir();
|
|
191
|
+
const serverPath = resolveServerPath();
|
|
192
|
+
const url = `http://localhost:${parsed.port}/`;
|
|
193
|
+
process.stdout.write(`otelly · ${url} · db=${dbPath}\n`);
|
|
194
|
+
const env = {
|
|
195
|
+
...process.env,
|
|
196
|
+
OTELLY_MODE: "embedded",
|
|
197
|
+
OTELLY_DB: dbPath,
|
|
198
|
+
OTELLY_STATIC_DIR: staticDir,
|
|
199
|
+
PORT: String(parsed.port)
|
|
200
|
+
};
|
|
201
|
+
const child = spawn("node", [serverPath], {
|
|
202
|
+
env,
|
|
203
|
+
stdio: "inherit"
|
|
204
|
+
});
|
|
205
|
+
let shuttingDown = false;
|
|
206
|
+
const shutdown = (signal) => {
|
|
207
|
+
if (shuttingDown) return;
|
|
208
|
+
shuttingDown = true;
|
|
209
|
+
if (!child.killed) child.kill(signal);
|
|
210
|
+
};
|
|
211
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
212
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
213
|
+
child.on("exit", (code, signal) => {
|
|
214
|
+
if (signal) process.kill(process.pid, signal);
|
|
215
|
+
else process.exit(code ?? 0);
|
|
216
|
+
});
|
|
217
|
+
if (parsed.open) waitForReady(url).then((ready) => {
|
|
218
|
+
if (ready && !shuttingDown) openBrowser(url);
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
await main();
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
export { };
|