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/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 { };
|