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/dist/server.js ADDED
@@ -0,0 +1,676 @@
1
+ import path from "node:path";
2
+ import { serve } from "@hono/node-server";
3
+ import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, HttpServer, OpenApi } from "@effect/platform";
4
+ import { Context, Data, Effect, Layer, ManagedRuntime, Ref, Schema } from "effect";
5
+ import { serveStatic } from "@hono/node-server/serve-static";
6
+ import { Hono } from "hono";
7
+ import { cors } from "hono/cors";
8
+ import Database from "better-sqlite3";
9
+ import { Pool } from "pg";
10
+ import { drizzle } from "drizzle-orm/node-postgres";
11
+ import { bigint, index, integer, jsonb, pgTable, primaryKey, text } from "drizzle-orm/pg-core";
12
+ import { and, desc, eq, sql } from "drizzle-orm";
13
+
14
+ //#region rolldown:runtime
15
+ var __defProp = Object.defineProperty;
16
+ var __exportAll = (all, symbols) => {
17
+ let target = {};
18
+ for (var name in all) {
19
+ __defProp(target, name, {
20
+ get: all[name],
21
+ enumerable: true
22
+ });
23
+ }
24
+ if (symbols) {
25
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
26
+ }
27
+ return target;
28
+ };
29
+
30
+ //#endregion
31
+ //#region ../../packages/core/src/spans/schema/span.model.ts
32
+ const hex = (length) => Schema.String.pipe(Schema.pattern(/* @__PURE__ */ new RegExp(`^[0-9a-fA-F]{${length}}$`), { message: () => `must be a ${length}-character hex string` }));
33
+ const TraceId = hex(32).pipe(Schema.brand("TraceId"));
34
+ const SpanId = hex(16).pipe(Schema.brand("SpanId"));
35
+ const ServiceName = Schema.String.pipe(Schema.minLength(1), Schema.brand("ServiceName"));
36
+ const UnixNano = Schema.String.pipe(Schema.pattern(/^\d+$/, { message: () => "must be a non-negative integer string" }), Schema.brand("UnixNano"));
37
+ const SpanKind = Schema.Literal(0, 1, 2, 3, 4, 5);
38
+ const SpanStatusCode = Schema.Literal(0, 1, 2);
39
+ const AttributeValue = Schema.Union(Schema.String, Schema.Number, Schema.Boolean, Schema.Null, Schema.Array(Schema.Union(Schema.String, Schema.Number, Schema.Boolean, Schema.Null)));
40
+ const Attributes = Schema.Record({
41
+ key: Schema.String,
42
+ value: AttributeValue
43
+ });
44
+ const Span = Schema.Struct({
45
+ traceId: TraceId,
46
+ spanId: SpanId,
47
+ parentSpanId: Schema.NullOr(SpanId),
48
+ serviceName: ServiceName,
49
+ scopeName: Schema.NullOr(Schema.String),
50
+ name: Schema.String,
51
+ kind: Schema.NullOr(SpanKind),
52
+ startUnixNano: UnixNano,
53
+ endUnixNano: UnixNano,
54
+ durationNs: Schema.Number,
55
+ statusCode: Schema.NullOr(SpanStatusCode),
56
+ statusMessage: Schema.NullOr(Schema.String),
57
+ attributes: Attributes,
58
+ ingestedAt: Schema.Number
59
+ });
60
+ const SpanQuery = Schema.Struct({
61
+ service: Schema.optional(ServiceName),
62
+ traceId: Schema.optional(TraceId),
63
+ limit: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.between(1, 1e3)))
64
+ });
65
+
66
+ //#endregion
67
+ //#region ../../packages/core/src/spans/schema/span.errors.ts
68
+ var SpanStorageError = class extends Data.TaggedError("SpanStorageError") {};
69
+ var SpanIngestError = class extends Data.TaggedError("SpanIngestError") {};
70
+
71
+ //#endregion
72
+ //#region ../../packages/core/src/spans/repository/span.repository.ts
73
+ var SpanRepository = class extends Context.Tag("SpanRepository")() {};
74
+
75
+ //#endregion
76
+ //#region ../../packages/core/src/spans/service/span.service.ts
77
+ var Spans = class extends Effect.Service()("Spans", {
78
+ accessors: true,
79
+ effect: Effect.gen(function* () {
80
+ const repo = yield* SpanRepository;
81
+ const ingest = (spans$1) => repo.insertMany(spans$1).pipe(Effect.withSpan("Spans.ingest", { attributes: { "spans.count": spans$1.length } }));
82
+ const byTraceId = (id) => repo.findByTraceId(id).pipe(Effect.withSpan("Spans.byTraceId", { attributes: { "trace.id": id } }));
83
+ const search = (query) => repo.search(query).pipe(Effect.withSpan("Spans.search"));
84
+ const recent = (limit = 100) => repo.recent(limit).pipe(Effect.withSpan("Spans.recent", { attributes: { "spans.limit": limit } }));
85
+ return {
86
+ ingest,
87
+ byTraceId,
88
+ search,
89
+ recent
90
+ };
91
+ })
92
+ }) {};
93
+
94
+ //#endregion
95
+ //#region ../../packages/core/src/blobs/schema/blob.model.ts
96
+ const BlobKey = Schema.String.pipe(Schema.minLength(1), Schema.maxLength(512), Schema.brand("BlobKey"));
97
+ const BlobRef = Schema.Struct({
98
+ key: BlobKey,
99
+ size: Schema.Number,
100
+ contentType: Schema.NullOr(Schema.String)
101
+ });
102
+
103
+ //#endregion
104
+ //#region ../../packages/core/src/blobs/schema/blob.errors.ts
105
+ var BlobNotFound = class extends Data.TaggedError("BlobNotFound") {};
106
+
107
+ //#endregion
108
+ //#region ../../packages/core/src/blobs/storage/blob.storage.ts
109
+ var BlobStorage = class extends Context.Tag("BlobStorage")() {};
110
+
111
+ //#endregion
112
+ //#region ../../packages/core/src/blobs/storage/blob.storage.memory.ts
113
+ const createMemoryBlobStore = () => Effect.gen(function* () {
114
+ const ref = yield* Ref.make(/* @__PURE__ */ new Map());
115
+ const put = (key, body, contentType) => Ref.update(ref, (m) => {
116
+ const next = new Map(m);
117
+ next.set(key, {
118
+ body,
119
+ contentType: contentType ?? null
120
+ });
121
+ return next;
122
+ }).pipe(Effect.map(() => ({
123
+ key,
124
+ size: body.byteLength,
125
+ contentType: contentType ?? null
126
+ })));
127
+ const get = (key) => Effect.gen(function* () {
128
+ const entry = (yield* Ref.get(ref)).get(key);
129
+ if (!entry) return yield* Effect.fail(new BlobNotFound({ key }));
130
+ return entry.body;
131
+ });
132
+ const head = (key) => Effect.gen(function* () {
133
+ const entry = (yield* Ref.get(ref)).get(key);
134
+ if (!entry) return yield* Effect.fail(new BlobNotFound({ key }));
135
+ return {
136
+ key,
137
+ size: entry.body.byteLength,
138
+ contentType: entry.contentType
139
+ };
140
+ });
141
+ const remove = (key) => Ref.update(ref, (m) => {
142
+ if (!m.has(key)) return m;
143
+ const next = new Map(m);
144
+ next.delete(key);
145
+ return next;
146
+ });
147
+ return {
148
+ put,
149
+ get,
150
+ head,
151
+ delete: remove
152
+ };
153
+ });
154
+
155
+ //#endregion
156
+ //#region ../../packages/core/src/blobs/layer/blob.layer.ts
157
+ const BlobStorageMemory = Layer.effect(BlobStorage, createMemoryBlobStore());
158
+
159
+ //#endregion
160
+ //#region ../../packages/api/src/spans/spans.api.ts
161
+ const TraceIdParam = Schema.Struct({ id: TraceId });
162
+ const RecentParams = Schema.Struct({ limit: Schema.optional(Schema.NumberFromString.pipe(Schema.int(), Schema.between(1, 1e3))) });
163
+ var TraceNotFound = class extends Schema.TaggedError()("TraceNotFound", { id: TraceId }, HttpApiSchema.annotations({ status: 404 })) {};
164
+ var SpansApi = class extends HttpApiGroup.make("spans").add(HttpApiEndpoint.get("recent", "/spans").setUrlParams(RecentParams).addSuccess(Schema.Array(Span))).add(HttpApiEndpoint.get("byTraceId", "/traces/:id").setPath(TraceIdParam).addSuccess(Schema.Array(Span)).addError(TraceNotFound)) {};
165
+
166
+ //#endregion
167
+ //#region ../../packages/api/src/api.ts
168
+ var Api = class extends HttpApi.make("otelly").add(SpansApi).prefix("/api") {};
169
+
170
+ //#endregion
171
+ //#region ../../packages/storage-sqlite/src/db.ts
172
+ const SCHEMA_SQL$1 = `
173
+ CREATE TABLE IF NOT EXISTS spans (
174
+ trace_id TEXT NOT NULL,
175
+ span_id TEXT NOT NULL,
176
+ parent_span_id TEXT,
177
+ service_name TEXT NOT NULL,
178
+ scope_name TEXT,
179
+ name TEXT NOT NULL,
180
+ kind INTEGER,
181
+ start_unix_nano TEXT NOT NULL,
182
+ end_unix_nano TEXT NOT NULL,
183
+ duration_ns INTEGER NOT NULL,
184
+ status_code INTEGER,
185
+ status_message TEXT,
186
+ attributes_json TEXT,
187
+ ingested_at INTEGER NOT NULL,
188
+ PRIMARY KEY (trace_id, span_id)
189
+ );
190
+ CREATE INDEX IF NOT EXISTS spans_ingested_at_idx ON spans (ingested_at DESC);
191
+ CREATE INDEX IF NOT EXISTS spans_service_idx ON spans (service_name, ingested_at DESC);
192
+ CREATE INDEX IF NOT EXISTS spans_trace_idx ON spans (trace_id);
193
+ `;
194
+ const openSqlite = (path$1) => {
195
+ const handle = new Database(path$1);
196
+ handle.pragma("journal_mode = WAL");
197
+ handle.pragma("synchronous = NORMAL");
198
+ handle.exec(SCHEMA_SQL$1);
199
+ return handle;
200
+ };
201
+
202
+ //#endregion
203
+ //#region ../../packages/storage-sqlite/src/effect.ts
204
+ const SQLITE_SPAN_ATTRS = { "db.system": "sqlite" };
205
+ const trySqlite = (op, run) => Effect.try({
206
+ try: run,
207
+ catch: (cause) => new SpanStorageError({
208
+ op,
209
+ cause
210
+ })
211
+ }).pipe(Effect.tapErrorTag("SpanStorageError", (e) => Effect.logError(`sqlite ${e.op} failed`, e.cause)), Effect.orDie, Effect.withSpan(op, { attributes: SQLITE_SPAN_ATTRS }));
212
+
213
+ //#endregion
214
+ //#region ../../packages/storage-sqlite/src/spans/span.repository.sqlite.ts
215
+ const decodeRow = Schema.decodeUnknownSync(Span);
216
+ const rowToSpan$1 = (row) => decodeRow({
217
+ traceId: row.trace_id,
218
+ spanId: row.span_id,
219
+ parentSpanId: row.parent_span_id,
220
+ serviceName: row.service_name,
221
+ scopeName: row.scope_name,
222
+ name: row.name,
223
+ kind: row.kind,
224
+ startUnixNano: row.start_unix_nano,
225
+ endUnixNano: row.end_unix_nano,
226
+ durationNs: row.duration_ns,
227
+ statusCode: row.status_code,
228
+ statusMessage: row.status_message,
229
+ attributes: row.attributes_json ? JSON.parse(row.attributes_json) : {},
230
+ ingestedAt: row.ingested_at
231
+ });
232
+ const spanToRow$1 = (s) => ({
233
+ trace_id: s.traceId,
234
+ span_id: s.spanId,
235
+ parent_span_id: s.parentSpanId,
236
+ service_name: s.serviceName,
237
+ scope_name: s.scopeName,
238
+ name: s.name,
239
+ kind: s.kind,
240
+ start_unix_nano: s.startUnixNano,
241
+ end_unix_nano: s.endUnixNano,
242
+ duration_ns: s.durationNs,
243
+ status_code: s.statusCode,
244
+ status_message: s.statusMessage,
245
+ attributes_json: JSON.stringify(s.attributes),
246
+ ingested_at: s.ingestedAt
247
+ });
248
+ const createSqliteSpanRepo = (handle) => {
249
+ const insertStmt = handle.prepare(`
250
+ INSERT OR REPLACE INTO spans
251
+ (trace_id, span_id, parent_span_id, service_name, scope_name, name, kind,
252
+ start_unix_nano, end_unix_nano, duration_ns, status_code, status_message,
253
+ attributes_json, ingested_at)
254
+ VALUES (@trace_id, @span_id, @parent_span_id, @service_name, @scope_name, @name, @kind,
255
+ @start_unix_nano, @end_unix_nano, @duration_ns, @status_code, @status_message,
256
+ @attributes_json, @ingested_at)
257
+ `);
258
+ const insertTx = handle.transaction((rows) => {
259
+ for (const row of rows) insertStmt.run(row);
260
+ });
261
+ const byTraceStmt = handle.prepare(`
262
+ SELECT * FROM spans WHERE trace_id = ? ORDER BY start_unix_nano ASC
263
+ `);
264
+ const recentStmt = handle.prepare(`
265
+ SELECT * FROM spans ORDER BY ingested_at DESC LIMIT ?
266
+ `);
267
+ return {
268
+ insertMany: (spans$1) => trySqlite("sqlite.spans.insertMany", () => {
269
+ if (spans$1.length === 0) return 0;
270
+ insertTx(spans$1.map(spanToRow$1));
271
+ return spans$1.length;
272
+ }),
273
+ findByTraceId: (id) => trySqlite("sqlite.spans.findByTraceId", () => byTraceStmt.all(id)).pipe(Effect.map((rows) => rows.map(rowToSpan$1))),
274
+ search: (query) => trySqlite("sqlite.spans.search", () => {
275
+ const limit = query.limit ?? 100;
276
+ const where = [];
277
+ const params = [];
278
+ if (query.service !== void 0) {
279
+ where.push("service_name = ?");
280
+ params.push(query.service);
281
+ }
282
+ if (query.traceId !== void 0) {
283
+ where.push("trace_id = ?");
284
+ params.push(query.traceId);
285
+ }
286
+ const sql$1 = `
287
+ SELECT * FROM spans
288
+ ${where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""}
289
+ ORDER BY ingested_at DESC
290
+ LIMIT ?
291
+ `;
292
+ params.push(limit);
293
+ return handle.prepare(sql$1).all(...params);
294
+ }).pipe(Effect.map((rows) => rows.map(rowToSpan$1))),
295
+ recent: (limit) => trySqlite("sqlite.spans.recent", () => recentStmt.all(limit)).pipe(Effect.map((rows) => rows.map(rowToSpan$1)))
296
+ };
297
+ };
298
+
299
+ //#endregion
300
+ //#region ../../packages/storage-pg/src/schema.ts
301
+ var schema_exports = /* @__PURE__ */ __exportAll({ spans: () => spans });
302
+ const spans = pgTable("spans", {
303
+ traceId: text("trace_id").notNull(),
304
+ spanId: text("span_id").notNull(),
305
+ parentSpanId: text("parent_span_id"),
306
+ serviceName: text("service_name").notNull(),
307
+ scopeName: text("scope_name"),
308
+ name: text("name").notNull(),
309
+ kind: integer("kind"),
310
+ startUnixNano: text("start_unix_nano").notNull(),
311
+ endUnixNano: text("end_unix_nano").notNull(),
312
+ durationNs: bigint("duration_ns", { mode: "number" }).notNull(),
313
+ statusCode: integer("status_code"),
314
+ statusMessage: text("status_message"),
315
+ attributes: jsonb("attributes").$type().notNull(),
316
+ ingestedAt: bigint("ingested_at", { mode: "number" }).notNull()
317
+ }, (t) => [
318
+ primaryKey({ columns: [t.traceId, t.spanId] }),
319
+ index("spans_ingested_at_idx").on(sql`${t.ingestedAt} DESC`),
320
+ index("spans_service_idx").on(t.serviceName, sql`${t.ingestedAt} DESC`),
321
+ index("spans_trace_idx").on(t.traceId)
322
+ ]);
323
+
324
+ //#endregion
325
+ //#region ../../packages/storage-pg/src/db.ts
326
+ const SCHEMA_SQL = `
327
+ CREATE TABLE IF NOT EXISTS spans (
328
+ trace_id TEXT NOT NULL,
329
+ span_id TEXT NOT NULL,
330
+ parent_span_id TEXT,
331
+ service_name TEXT NOT NULL,
332
+ scope_name TEXT,
333
+ name TEXT NOT NULL,
334
+ kind INTEGER,
335
+ start_unix_nano TEXT NOT NULL,
336
+ end_unix_nano TEXT NOT NULL,
337
+ duration_ns BIGINT NOT NULL,
338
+ status_code INTEGER,
339
+ status_message TEXT,
340
+ attributes JSONB NOT NULL DEFAULT '{}'::jsonb,
341
+ ingested_at BIGINT NOT NULL,
342
+ PRIMARY KEY (trace_id, span_id)
343
+ );
344
+ CREATE INDEX IF NOT EXISTS spans_ingested_at_idx ON spans (ingested_at DESC);
345
+ CREATE INDEX IF NOT EXISTS spans_service_idx ON spans (service_name, ingested_at DESC);
346
+ CREATE INDEX IF NOT EXISTS spans_trace_idx ON spans (trace_id);
347
+ `;
348
+ const openPostgres = async (databaseUrl) => {
349
+ const pool = new Pool({ connectionString: databaseUrl });
350
+ const client = await pool.connect();
351
+ try {
352
+ await client.query(SCHEMA_SQL);
353
+ } finally {
354
+ client.release();
355
+ }
356
+ return {
357
+ pool,
358
+ db: drizzle(pool, { schema: schema_exports })
359
+ };
360
+ };
361
+ const closePostgres = (handle) => handle.pool.end();
362
+
363
+ //#endregion
364
+ //#region ../../packages/storage-pg/src/effect.ts
365
+ var PostgresError = class extends Data.TaggedError("PostgresError") {};
366
+ const PG_SPAN_ATTRS = { "db.system": "postgresql" };
367
+ const tryDb = (op, run) => Effect.tryPromise({
368
+ try: run,
369
+ catch: (cause) => new PostgresError({
370
+ op,
371
+ cause
372
+ })
373
+ }).pipe(Effect.tapErrorTag("PostgresError", (e) => Effect.logError(`postgres ${e.op} failed`, e.cause)), Effect.orDie, Effect.withSpan(op, { attributes: PG_SPAN_ATTRS }));
374
+
375
+ //#endregion
376
+ //#region ../../packages/storage-pg/src/spans/span.repository.pg.ts
377
+ const decodeSpan = Schema.decodeUnknownSync(Span);
378
+ const rowToSpan = (row) => decodeSpan({
379
+ traceId: row.traceId,
380
+ spanId: row.spanId,
381
+ parentSpanId: row.parentSpanId,
382
+ serviceName: row.serviceName,
383
+ scopeName: row.scopeName,
384
+ name: row.name,
385
+ kind: row.kind,
386
+ startUnixNano: row.startUnixNano,
387
+ endUnixNano: row.endUnixNano,
388
+ durationNs: row.durationNs,
389
+ statusCode: row.statusCode,
390
+ statusMessage: row.statusMessage,
391
+ attributes: row.attributes ?? {},
392
+ ingestedAt: row.ingestedAt
393
+ });
394
+ const spanToRow = (s) => ({
395
+ traceId: s.traceId,
396
+ spanId: s.spanId,
397
+ parentSpanId: s.parentSpanId,
398
+ serviceName: s.serviceName,
399
+ scopeName: s.scopeName,
400
+ name: s.name,
401
+ kind: s.kind,
402
+ startUnixNano: s.startUnixNano,
403
+ endUnixNano: s.endUnixNano,
404
+ durationNs: s.durationNs,
405
+ statusCode: s.statusCode,
406
+ statusMessage: s.statusMessage,
407
+ attributes: s.attributes,
408
+ ingestedAt: s.ingestedAt
409
+ });
410
+ const createPostgresSpanRepo = (db) => ({
411
+ insertMany: (toInsert) => tryDb("pg.spans.insertMany", async () => {
412
+ if (toInsert.length === 0) return 0;
413
+ const rows = toInsert.map(spanToRow);
414
+ await db.insert(spans).values(rows).onConflictDoUpdate({
415
+ target: [spans.traceId, spans.spanId],
416
+ set: {
417
+ parentSpanId: sql`excluded.parent_span_id`,
418
+ serviceName: sql`excluded.service_name`,
419
+ scopeName: sql`excluded.scope_name`,
420
+ name: sql`excluded.name`,
421
+ kind: sql`excluded.kind`,
422
+ startUnixNano: sql`excluded.start_unix_nano`,
423
+ endUnixNano: sql`excluded.end_unix_nano`,
424
+ durationNs: sql`excluded.duration_ns`,
425
+ statusCode: sql`excluded.status_code`,
426
+ statusMessage: sql`excluded.status_message`,
427
+ attributes: sql`excluded.attributes`,
428
+ ingestedAt: sql`excluded.ingested_at`
429
+ }
430
+ });
431
+ return toInsert.length;
432
+ }),
433
+ findByTraceId: (id) => tryDb("pg.spans.findByTraceId", () => db.select().from(spans).where(eq(spans.traceId, id))).pipe(Effect.map((rows) => rows.map(rowToSpan))),
434
+ search: (query) => tryDb("pg.spans.search", () => {
435
+ const limit = query.limit ?? 100;
436
+ const where = [query.service !== void 0 ? eq(spans.serviceName, query.service) : void 0, query.traceId !== void 0 ? eq(spans.traceId, query.traceId) : void 0].filter((c) => c !== void 0);
437
+ const condition = where.length > 0 ? and(...where) : void 0;
438
+ const base = db.select().from(spans);
439
+ return (condition !== void 0 ? base.where(condition) : base).orderBy(desc(spans.ingestedAt)).limit(limit);
440
+ }).pipe(Effect.map((rows) => rows.map(rowToSpan))),
441
+ recent: (limit) => tryDb("pg.spans.recent", () => db.select().from(spans).orderBy(desc(spans.ingestedAt)).limit(limit)).pipe(Effect.map((rows) => rows.map(rowToSpan)))
442
+ });
443
+
444
+ //#endregion
445
+ //#region ../server/src/runtime.ts
446
+ const readMode = () => process.env.OTELLY_MODE === "server" ? "server" : "embedded";
447
+ const requireEnv = (name) => {
448
+ const v = process.env[name];
449
+ if (!v || v.length === 0) throw new Error(`${name} env var is required in server mode`);
450
+ return v;
451
+ };
452
+ const resolveSqlitePath = () => {
453
+ const fromEnv = process.env.OTELLY_DB;
454
+ if (fromEnv && fromEnv.length > 0) return fromEnv;
455
+ return path.resolve(process.cwd(), "otelly.sqlite");
456
+ };
457
+ const buildEmbedded = () => {
458
+ const dbPath = resolveSqlitePath();
459
+ const sqliteHandle = openSqlite(dbPath);
460
+ const spanRepo = createSqliteSpanRepo(sqliteHandle);
461
+ return {
462
+ mode: "embedded",
463
+ displayPrimary: `sqlite=${dbPath}`,
464
+ spanRepo,
465
+ shutdown: async () => {
466
+ sqliteHandle.close();
467
+ }
468
+ };
469
+ };
470
+ const buildServer = async () => {
471
+ const databaseUrl = requireEnv("DATABASE_URL");
472
+ const pgHandle = await openPostgres(databaseUrl);
473
+ const spanRepo = createPostgresSpanRepo(pgHandle.db);
474
+ return {
475
+ mode: "server",
476
+ displayPrimary: `postgres=${databaseUrl.replace(/:[^@]+@/, ":***@")}`,
477
+ spanRepo,
478
+ shutdown: async () => closePostgres(pgHandle)
479
+ };
480
+ };
481
+ const resolved = readMode() === "server" ? await buildServer() : buildEmbedded();
482
+ process.on("exit", () => {
483
+ resolved.shutdown();
484
+ });
485
+ const propagate = (signal) => () => {
486
+ resolved.shutdown().finally(() => process.kill(process.pid, signal));
487
+ };
488
+ process.once("SIGINT", propagate("SIGINT"));
489
+ process.once("SIGTERM", propagate("SIGTERM"));
490
+ const SpanRepoLive = Layer.succeed(SpanRepository, resolved.spanRepo);
491
+ const ServicesLive = Spans.Default.pipe(Layer.provide(SpanRepoLive));
492
+ const config = {
493
+ mode: resolved.mode,
494
+ primary: resolved.displayPrimary
495
+ };
496
+
497
+ //#endregion
498
+ //#region ../server/src/features/spans/spans.handlers.ts
499
+ const SpanHandlersLive = HttpApiBuilder.group(Api, "spans", (handlers) => handlers.handle("recent", ({ urlParams }) => Spans.recent(urlParams.limit ?? 100).pipe(Effect.map((arr) => [...arr]))).handle("byTraceId", ({ path: path$1 }) => Effect.gen(function* () {
500
+ const spans$1 = yield* Spans.byTraceId(path$1.id);
501
+ if (spans$1.length === 0) return yield* Effect.fail(new TraceNotFound({ id: path$1.id }));
502
+ return [...spans$1];
503
+ })));
504
+ const SpanHandlers = SpanHandlersLive.pipe(Layer.provide(ServicesLive));
505
+
506
+ //#endregion
507
+ //#region ../../packages/ingest/src/otlp/attributes.ts
508
+ const toPrimitive = (v) => {
509
+ if (!v) return null;
510
+ if (typeof v.stringValue === "string") return v.stringValue;
511
+ if (typeof v.boolValue === "boolean") return v.boolValue;
512
+ if (typeof v.intValue !== "undefined") {
513
+ const n = typeof v.intValue === "number" ? v.intValue : Number(v.intValue);
514
+ return Number.isFinite(n) ? n : String(v.intValue);
515
+ }
516
+ if (typeof v.doubleValue === "number") return v.doubleValue;
517
+ if (typeof v.bytesValue === "string") return v.bytesValue;
518
+ return null;
519
+ };
520
+ const toValue = (v) => {
521
+ if (v?.arrayValue) return (v.arrayValue.values ?? []).map(toPrimitive);
522
+ return toPrimitive(v);
523
+ };
524
+ const flattenAttributes = (attrs) => {
525
+ const out = {};
526
+ if (!attrs) return out;
527
+ for (const kv of attrs) {
528
+ if (!kv.key) continue;
529
+ out[kv.key] = toValue(kv.value);
530
+ }
531
+ return out;
532
+ };
533
+ const findAttr = (attrs, key) => {
534
+ if (!attrs) return null;
535
+ const v = attrs.find((kv) => kv.key === key)?.value;
536
+ if (!v) return null;
537
+ if (typeof v.stringValue === "string") return v.stringValue;
538
+ if (typeof v.intValue !== "undefined") return String(v.intValue);
539
+ if (typeof v.doubleValue === "number") return String(v.doubleValue);
540
+ if (typeof v.boolValue === "boolean") return String(v.boolValue);
541
+ return null;
542
+ };
543
+
544
+ //#endregion
545
+ //#region ../../packages/ingest/src/otlp/traces.ts
546
+ const HEX_RE = /^[0-9a-f]+$/;
547
+ const normalizeHex = (raw, length) => {
548
+ if (!raw) return null;
549
+ const lower = raw.toLowerCase();
550
+ if (lower.length !== length) return null;
551
+ if (!HEX_RE.test(lower)) return null;
552
+ return lower;
553
+ };
554
+ const toUnixNano = (raw) => {
555
+ if (raw === void 0 || raw === null) return null;
556
+ const s = typeof raw === "number" ? Math.trunc(raw).toString() : raw;
557
+ if (!/^\d+$/.test(s)) return null;
558
+ return s;
559
+ };
560
+ const buildRow = (rs, serviceName, scopeName, span, ingestedAt) => {
561
+ const traceId = normalizeHex(span.traceId, 32);
562
+ const spanId = normalizeHex(span.spanId, 16);
563
+ if (!traceId || !spanId) return null;
564
+ const startNs = toUnixNano(span.startTimeUnixNano);
565
+ const endNs = toUnixNano(span.endTimeUnixNano);
566
+ if (!startNs || !endNs) return null;
567
+ const parentSpanId = normalizeHex(span.parentSpanId, 16);
568
+ const durationNs = Number(BigInt(endNs) - BigInt(startNs));
569
+ const spanAttrs = flattenAttributes(span.attributes);
570
+ const attributes = {
571
+ ...flattenAttributes(rs.resource?.attributes),
572
+ ...spanAttrs
573
+ };
574
+ return {
575
+ traceId,
576
+ spanId,
577
+ parentSpanId,
578
+ serviceName,
579
+ scopeName,
580
+ name: span.name ?? "",
581
+ kind: typeof span.kind === "number" ? span.kind : null,
582
+ startUnixNano: startNs,
583
+ endUnixNano: endNs,
584
+ durationNs,
585
+ statusCode: typeof span.status?.code === "number" ? span.status.code : null,
586
+ statusMessage: span.status?.message ?? null,
587
+ attributes,
588
+ ingestedAt
589
+ };
590
+ };
591
+ const decodeTraceRequest = (body, now = Date.now()) => Effect.gen(function* () {
592
+ const candidates = [];
593
+ let skipped = 0;
594
+ for (const rs of body.resourceSpans ?? []) {
595
+ const serviceName = findAttr(rs.resource?.attributes, "service.name") ?? "unknown";
596
+ for (const ss of rs.scopeSpans ?? []) {
597
+ const scopeName = ss.scope?.name ?? null;
598
+ for (const span of ss.spans ?? []) {
599
+ const row = buildRow(rs, serviceName, scopeName, span, now);
600
+ if (row === null) {
601
+ skipped += 1;
602
+ continue;
603
+ }
604
+ candidates.push(row);
605
+ }
606
+ }
607
+ }
608
+ return {
609
+ spans: yield* Schema.decodeUnknown(Schema.Array(Span))(candidates).pipe(Effect.mapError((cause) => new SpanIngestError({
610
+ reason: "schema decode failed for trace batch",
611
+ cause
612
+ }))),
613
+ skipped
614
+ };
615
+ }).pipe(Effect.withSpan("Ingest.decodeTraceRequest"));
616
+
617
+ //#endregion
618
+ //#region ../server/src/features/spans/spans.ingest.ts
619
+ const mountIngest = (app$1, runtime$1) => {
620
+ app$1.post("/v1/traces", async (c) => {
621
+ let body;
622
+ try {
623
+ body = await c.req.json();
624
+ } catch {
625
+ return c.json({ error: "invalid JSON" }, 400);
626
+ }
627
+ const program = Effect.gen(function* () {
628
+ const { spans: spans$1, skipped } = yield* decodeTraceRequest(body);
629
+ return {
630
+ insertedSpans: yield* Spans.ingest(spans$1),
631
+ skippedSpans: skipped
632
+ };
633
+ });
634
+ const exit = await runtime$1.runPromiseExit(program);
635
+ if (exit._tag === "Failure") return c.json({
636
+ error: "ingest failed",
637
+ cause: String(exit.cause)
638
+ }, 500);
639
+ return c.json(exit.value);
640
+ });
641
+ };
642
+
643
+ //#endregion
644
+ //#region ../server/src/server.ts
645
+ const ApiLive = HttpApiBuilder.api(Api).pipe(Layer.provide(SpanHandlers));
646
+ const ApiLayer = Layer.mergeAll(ApiLive, HttpApiBuilder.Router.Live, HttpServer.layerContext);
647
+ const { handler: apiHandler } = HttpApiBuilder.toWebHandler(ApiLayer);
648
+ const spec = OpenApi.fromApi(Api);
649
+ const runtime = ManagedRuntime.make(ServicesLive);
650
+ const staticDir = process.env.OTELLY_STATIC_DIR;
651
+ const app = new Hono().use("*", cors({
652
+ origin: (o) => o ?? "*",
653
+ credentials: true
654
+ })).get("/health", (c) => c.json({
655
+ ok: true,
656
+ mode: config.mode,
657
+ primary: config.primary
658
+ })).get("/openapi", (c) => c.json(spec)).all("/api/*", (c) => apiHandler(c.req.raw));
659
+ mountIngest(app, runtime);
660
+ if (staticDir !== void 0 && staticDir.length > 0) app.use("/*", serveStatic({
661
+ root: staticDir,
662
+ rewriteRequestPath: (path$1) => path$1 === "/" ? "/index.html" : path$1
663
+ }));
664
+
665
+ //#endregion
666
+ //#region ../server/src/main.ts
667
+ const port = Number(process.env.PORT ?? 4318);
668
+ serve({
669
+ fetch: app.fetch,
670
+ port
671
+ }, (info) => {
672
+ process.stdout.write(`otelly-server · http://localhost:${info.port} · mode=${config.mode} · ${config.primary}\n`);
673
+ });
674
+
675
+ //#endregion
676
+ export { };