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.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/cli.js +224 -0
  4. package/dist/server.js +676 -0
  5. package/dist/static/404/index.html +1 -0
  6. package/dist/static/404.html +1 -0
  7. package/dist/static/__next.__PAGE__.txt +9 -0
  8. package/dist/static/__next._full.txt +17 -0
  9. package/dist/static/__next._head.txt +5 -0
  10. package/dist/static/__next._index.txt +5 -0
  11. package/dist/static/__next._tree.txt +2 -0
  12. package/dist/static/_next/static/chunks/01l47c09f~n66.js +1 -0
  13. package/dist/static/_next/static/chunks/01mhk8rsi88af.js +31 -0
  14. package/dist/static/_next/static/chunks/02aa95iuhqxu7.css +1 -0
  15. package/dist/static/_next/static/chunks/03~yq9q893hmn.js +1 -0
  16. package/dist/static/_next/static/chunks/06cclqnbyt80n.js +1 -0
  17. package/dist/static/_next/static/chunks/0o-7hx1honk9g.js +1 -0
  18. package/dist/static/_next/static/chunks/149l7j7ef19ra.js +5 -0
  19. package/dist/static/_next/static/chunks/turbopack-0ikepo8r-2zp~.js +1 -0
  20. package/dist/static/_next/static/d1qv-JAQphjG0HTu0Ryte/_buildManifest.js +11 -0
  21. package/dist/static/_next/static/d1qv-JAQphjG0HTu0Ryte/_clientMiddlewareManifest.js +1 -0
  22. package/dist/static/_next/static/d1qv-JAQphjG0HTu0Ryte/_ssgManifest.js +1 -0
  23. package/dist/static/_not-found/__next._full.txt +15 -0
  24. package/dist/static/_not-found/__next._head.txt +5 -0
  25. package/dist/static/_not-found/__next._index.txt +5 -0
  26. package/dist/static/_not-found/__next._not-found.__PAGE__.txt +5 -0
  27. package/dist/static/_not-found/__next._not-found.txt +5 -0
  28. package/dist/static/_not-found/__next._tree.txt +2 -0
  29. package/dist/static/_not-found/index.html +1 -0
  30. package/dist/static/_not-found/index.txt +15 -0
  31. package/dist/static/index.html +1 -0
  32. package/dist/static/index.txt +17 -0
  33. 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 { };