@monocle.sh/studio 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 +15 -0
- package/dist/adonisrc.d.ts +6 -0
- package/dist/adonisrc.js +25 -0
- package/dist/config/app.d.ts +6 -0
- package/dist/config/app.js +5 -0
- package/dist/config/bodyparser.d.ts +4 -0
- package/dist/config/bodyparser.js +5 -0
- package/dist/config/encryption.d.ts +12 -0
- package/dist/config/encryption.js +11 -0
- package/dist/config/logger.d.ts +4 -0
- package/dist/config/logger.js +13 -0
- package/dist/config/static.d.ts +6 -0
- package/dist/config/static.js +11 -0
- package/dist/controllers/api_controller.d.ts +174 -0
- package/dist/controllers/api_controller.js +118 -0
- package/dist/controllers/mcp_controller.d.ts +20 -0
- package/dist/controllers/mcp_controller.js +29 -0
- package/dist/controllers/otlp_controller.d.ts +52 -0
- package/dist/controllers/otlp_controller.js +92 -0
- package/dist/controllers/sse_controller.d.ts +18 -0
- package/dist/controllers/sse_controller.js +36 -0
- package/dist/db/connection.d.ts +20 -0
- package/dist/db/connection.js +38 -0
- package/dist/db/schema.d.ts +10 -0
- package/dist/db/schema.js +45 -0
- package/dist/db/types.d.ts +40 -0
- package/dist/db/types.js +1 -0
- package/dist/mcp/define_tool.d.ts +10 -0
- package/dist/mcp/define_tool.js +9 -0
- package/dist/mcp/tools/clear_data.d.ts +9 -0
- package/dist/mcp/tools/clear_data.js +26 -0
- package/dist/mcp/tools/get_trace.d.ts +12 -0
- package/dist/mcp/tools/get_trace.js +25 -0
- package/dist/mcp/tools/index.d.ts +10 -0
- package/dist/mcp/tools/index.js +37 -0
- package/dist/mcp/tools/list_errors.d.ts +14 -0
- package/dist/mcp/tools/list_errors.js +25 -0
- package/dist/mcp/tools/list_logs.d.ts +16 -0
- package/dist/mcp/tools/list_logs.js +30 -0
- package/dist/mcp/tools/list_queries.d.ts +15 -0
- package/dist/mcp/tools/list_queries.js +27 -0
- package/dist/mcp/tools/list_traces.d.ts +14 -0
- package/dist/mcp/tools/list_traces.js +25 -0
- package/dist/mcp/types.d.ts +21 -0
- package/dist/mcp/types.js +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +2 -0
- package/dist/providers/api_provider.d.ts +14 -0
- package/dist/providers/api_provider.js +13 -0
- package/dist/repositories/log_repository.d.ts +43 -0
- package/dist/repositories/log_repository.js +58 -0
- package/dist/repositories/span_repository.d.ts +136 -0
- package/dist/repositories/span_repository.js +254 -0
- package/dist/semconv.d.ts +38 -0
- package/dist/semconv.js +40 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +68 -0
- package/dist/services/event_bus.d.ts +24 -0
- package/dist/services/event_bus.js +24 -0
- package/dist/services/otlp_parser.d.ts +21 -0
- package/dist/services/otlp_parser.js +121 -0
- package/dist/start/kernel.d.ts +1 -0
- package/dist/start/kernel.js +5 -0
- package/dist/start/routes.d.ts +1 -0
- package/dist/start/routes.js +35 -0
- package/dist/transformers/command_transformer.d.ts +18 -0
- package/dist/transformers/command_transformer.js +20 -0
- package/dist/transformers/exception_transformer.d.ts +17 -0
- package/dist/transformers/exception_transformer.js +19 -0
- package/dist/transformers/job_transformer.d.ts +18 -0
- package/dist/transformers/job_transformer.js +20 -0
- package/dist/transformers/log_transformer.d.ts +18 -0
- package/dist/transformers/log_transformer.js +19 -0
- package/dist/transformers/query_transformer.d.ts +20 -0
- package/dist/transformers/query_transformer.js +27 -0
- package/dist/transformers/span_transformer.d.ts +22 -0
- package/dist/transformers/span_transformer.js +23 -0
- package/dist/transformers/trace_transformer.d.ts +22 -0
- package/dist/transformers/trace_transformer.js +33 -0
- package/dist/types/logs.d.ts +19 -0
- package/dist/types/logs.js +1 -0
- package/dist/types/otlp.d.ts +105 -0
- package/dist/types/otlp.js +1 -0
- package/dist/types/spans.d.ts +29 -0
- package/dist/types/spans.js +1 -0
- package/dist/validators.d.ts +103 -0
- package/dist/validators.js +41 -0
- package/package.json +60 -0
- package/ui/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/ui/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/ui/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/ui/dist/assets/index-Bfk6GRvP.css +1 -0
- package/ui/dist/assets/index-XOaGlb1r.js +115 -0
- package/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/ui/dist/assets/silkscreen-latin-400-normal-CtPo2yA5.woff2 +0 -0
- package/ui/dist/assets/silkscreen-latin-400-normal-D0DfPJut.woff +0 -0
- package/ui/dist/favicon.svg +4 -0
- package/ui/dist/index.html +14 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { FlatSpan } from "../types/spans.js";
|
|
2
|
+
|
|
3
|
+
//#region src/repositories/span_repository.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Data-access layer for distributed trace spans stored in DuckDB.
|
|
6
|
+
* Provides queries for traces, jobs, commands, database queries,
|
|
7
|
+
* and exceptions.
|
|
8
|
+
*/
|
|
9
|
+
declare class SpanRepository {
|
|
10
|
+
#private;
|
|
11
|
+
/**
|
|
12
|
+
* Upserts a batch of flat spans into the spans table.
|
|
13
|
+
* Uses ON CONFLICT to update existing spans (same trace_id + span_id).
|
|
14
|
+
*/
|
|
15
|
+
insertSpans(spans: FlatSpan[]): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Returns all distinct service names that have emitted spans.
|
|
18
|
+
*/
|
|
19
|
+
listServices(): Promise<string[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Lists HTTP request root spans with their span counts,
|
|
22
|
+
* optionally filtered by trace ID or service name.
|
|
23
|
+
*/
|
|
24
|
+
listTraces(options?: {
|
|
25
|
+
limit?: number;
|
|
26
|
+
offset?: number;
|
|
27
|
+
traceId?: string;
|
|
28
|
+
service?: string;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
span_count: number;
|
|
31
|
+
trace_id: string;
|
|
32
|
+
span_name: string;
|
|
33
|
+
service_name: string | null;
|
|
34
|
+
status_code: string;
|
|
35
|
+
duration_ns: bigint;
|
|
36
|
+
start_time_unix: bigint;
|
|
37
|
+
span_attributes: string | null;
|
|
38
|
+
}[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Lists background job root spans (messaging/queue consumers).
|
|
41
|
+
*/
|
|
42
|
+
listJobs(options?: {
|
|
43
|
+
limit?: number;
|
|
44
|
+
offset?: number;
|
|
45
|
+
traceId?: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
span_count: number;
|
|
48
|
+
trace_id: string;
|
|
49
|
+
span_name: string;
|
|
50
|
+
service_name: string | null;
|
|
51
|
+
status_code: string;
|
|
52
|
+
duration_ns: bigint;
|
|
53
|
+
start_time_unix: bigint;
|
|
54
|
+
span_attributes: string | null;
|
|
55
|
+
}[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Lists CLI command root spans.
|
|
58
|
+
*/
|
|
59
|
+
listCommands(options?: {
|
|
60
|
+
limit?: number;
|
|
61
|
+
offset?: number;
|
|
62
|
+
traceId?: string;
|
|
63
|
+
}): Promise<{
|
|
64
|
+
span_count: number;
|
|
65
|
+
trace_id: string;
|
|
66
|
+
span_name: string;
|
|
67
|
+
service_name: string | null;
|
|
68
|
+
status_code: string;
|
|
69
|
+
duration_ns: bigint;
|
|
70
|
+
start_time_unix: bigint;
|
|
71
|
+
span_attributes: string | null;
|
|
72
|
+
}[]>;
|
|
73
|
+
/**
|
|
74
|
+
* Lists database query spans, optionally filtered by DB system
|
|
75
|
+
* (postgresql, mysql, sqlite, etc.).
|
|
76
|
+
*/
|
|
77
|
+
listQueries(options?: {
|
|
78
|
+
limit?: number;
|
|
79
|
+
offset?: number;
|
|
80
|
+
traceId?: string;
|
|
81
|
+
dbSystem?: string;
|
|
82
|
+
}): Promise<{
|
|
83
|
+
trace_id: string;
|
|
84
|
+
span_id: string;
|
|
85
|
+
span_name: string;
|
|
86
|
+
service_name: string | null;
|
|
87
|
+
status_code: string;
|
|
88
|
+
duration_ns: bigint;
|
|
89
|
+
start_time_unix: bigint;
|
|
90
|
+
span_attributes: string | null;
|
|
91
|
+
}[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Lists exceptions extracted from span events by unnesting
|
|
94
|
+
* the JSON events array and filtering for "exception" events.
|
|
95
|
+
*/
|
|
96
|
+
listExceptions(options?: {
|
|
97
|
+
limit?: number;
|
|
98
|
+
offset?: number;
|
|
99
|
+
traceId?: string;
|
|
100
|
+
}): Promise<{
|
|
101
|
+
trace_id: string;
|
|
102
|
+
span_id: string;
|
|
103
|
+
span_name: string;
|
|
104
|
+
service_name: string | null;
|
|
105
|
+
start_time_unix: bigint;
|
|
106
|
+
event_json: string;
|
|
107
|
+
}[]>;
|
|
108
|
+
/**
|
|
109
|
+
* Deletes all rows from the spans table.
|
|
110
|
+
*/
|
|
111
|
+
clearAll(): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Returns all spans belonging to a trace, ordered by start time,
|
|
114
|
+
* with parsed attributes and events.
|
|
115
|
+
*/
|
|
116
|
+
getTraceSpans(traceId: string): Promise<{
|
|
117
|
+
trace_id: string;
|
|
118
|
+
span_id: string;
|
|
119
|
+
span_name: string;
|
|
120
|
+
service_name: string | null;
|
|
121
|
+
status_code: string;
|
|
122
|
+
duration_ns: bigint;
|
|
123
|
+
start_time_unix: bigint;
|
|
124
|
+
parent_span_id: string | null;
|
|
125
|
+
span_kind: string | null;
|
|
126
|
+
status_message: string | null;
|
|
127
|
+
end_time_unix: bigint;
|
|
128
|
+
span_attributes: string | null;
|
|
129
|
+
events: string | null;
|
|
130
|
+
resource_attributes: string | null;
|
|
131
|
+
scope_name: string | null;
|
|
132
|
+
scope_version: string | null;
|
|
133
|
+
}[]>;
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
export { SpanRepository };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { getDb } from "../db/connection.js";
|
|
2
|
+
import { Semconv } from "../semconv.js";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
//#region src/repositories/span_repository.ts
|
|
5
|
+
const S = Semconv.Stable;
|
|
6
|
+
const L = Semconv.Legacy;
|
|
7
|
+
/**
|
|
8
|
+
* Data-access layer for distributed trace spans stored in DuckDB.
|
|
9
|
+
* Provides queries for traces, jobs, commands, database queries,
|
|
10
|
+
* and exceptions.
|
|
11
|
+
*/
|
|
12
|
+
var SpanRepository = class {
|
|
13
|
+
/**
|
|
14
|
+
* Serializes a value to JSON, returning null for falsy values.
|
|
15
|
+
*/
|
|
16
|
+
#jsonOrNull(value) {
|
|
17
|
+
if (!value) return null;
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* SQL expression: json_extract checking both stable and legacy keys via COALESCE.
|
|
22
|
+
*/
|
|
23
|
+
#sqlAttr(stable, legacy) {
|
|
24
|
+
return sql`COALESCE(
|
|
25
|
+
json_extract_string(span_attributes, ${`$."${stable}"`}),
|
|
26
|
+
json_extract_string(span_attributes, ${`$."${legacy}"`})
|
|
27
|
+
)`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* SQL expression: check if at least one of the two keys exists (IS NOT NULL).
|
|
31
|
+
*/
|
|
32
|
+
#sqlAttrExists(stable, legacy) {
|
|
33
|
+
return sql`(
|
|
34
|
+
json_extract_string(span_attributes, ${`$."${stable}"`}) IS NOT NULL
|
|
35
|
+
OR json_extract_string(span_attributes, ${`$."${legacy}"`}) IS NOT NULL
|
|
36
|
+
)`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* SQL condition: root span (no parent).
|
|
40
|
+
*/
|
|
41
|
+
#whereRootSpan() {
|
|
42
|
+
return sql`(parent_span_id IS NULL OR parent_span_id = '')`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* SQL condition: entry is an HTTP request (has http.request.method attribute).
|
|
46
|
+
*/
|
|
47
|
+
#whereHttpRequest() {
|
|
48
|
+
return this.#sqlAttrExists(S.HTTP_METHOD, L.HTTP_METHOD);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* SQL condition: entry is a job (has entry_point.type = 'job' or messaging.system attribute).
|
|
52
|
+
*/
|
|
53
|
+
#whereJob() {
|
|
54
|
+
return sql`(
|
|
55
|
+
json_extract_string(span_attributes, '$."entry_point.type"') = 'job'
|
|
56
|
+
OR json_extract_string(span_attributes, '$."messaging.system"') IS NOT NULL
|
|
57
|
+
)`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* SQL condition: entry is a CLI command (has entry_point.type = 'cli').
|
|
61
|
+
*/
|
|
62
|
+
#whereCommand() {
|
|
63
|
+
return sql`json_extract_string(span_attributes, '$."entry_point.type"') = 'cli'`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Upserts a batch of flat spans into the spans table.
|
|
67
|
+
* Uses ON CONFLICT to update existing spans (same trace_id + span_id).
|
|
68
|
+
*/
|
|
69
|
+
async insertSpans(spans) {
|
|
70
|
+
const db = getDb();
|
|
71
|
+
for (const span of spans) await db.insertInto("spans").values({
|
|
72
|
+
trace_id: span.traceId,
|
|
73
|
+
span_id: span.spanId,
|
|
74
|
+
parent_span_id: span.parentSpanId,
|
|
75
|
+
span_name: span.spanName,
|
|
76
|
+
span_kind: span.spanKind,
|
|
77
|
+
service_name: span.serviceName,
|
|
78
|
+
status_code: span.statusCode,
|
|
79
|
+
status_message: span.statusMessage,
|
|
80
|
+
start_time_unix: span.startTimeUnix,
|
|
81
|
+
end_time_unix: span.endTimeUnix,
|
|
82
|
+
duration_ns: span.durationNs,
|
|
83
|
+
resource_attributes: this.#jsonOrNull(span.resourceAttributes),
|
|
84
|
+
span_attributes: this.#jsonOrNull(span.spanAttributes),
|
|
85
|
+
scope_name: span.scopeName,
|
|
86
|
+
scope_version: span.scopeVersion,
|
|
87
|
+
events: this.#jsonOrNull(span.events)
|
|
88
|
+
}).onConflict((oc) => oc.columns(["trace_id", "span_id"]).doUpdateSet({
|
|
89
|
+
parent_span_id: span.parentSpanId,
|
|
90
|
+
span_name: span.spanName,
|
|
91
|
+
span_kind: span.spanKind,
|
|
92
|
+
service_name: span.serviceName,
|
|
93
|
+
status_code: span.statusCode,
|
|
94
|
+
status_message: span.statusMessage,
|
|
95
|
+
start_time_unix: span.startTimeUnix,
|
|
96
|
+
end_time_unix: span.endTimeUnix,
|
|
97
|
+
duration_ns: span.durationNs,
|
|
98
|
+
resource_attributes: this.#jsonOrNull(span.resourceAttributes),
|
|
99
|
+
span_attributes: this.#jsonOrNull(span.spanAttributes),
|
|
100
|
+
scope_name: span.scopeName,
|
|
101
|
+
scope_version: span.scopeVersion,
|
|
102
|
+
events: this.#jsonOrNull(span.events)
|
|
103
|
+
})).execute();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Returns all distinct service names that have emitted spans.
|
|
107
|
+
*/
|
|
108
|
+
async listServices() {
|
|
109
|
+
return (await getDb().selectFrom("spans").select("service_name").distinct().where("service_name", "is not", null).orderBy("service_name").execute()).map((r) => r.service_name).filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Lists HTTP request root spans with their span counts,
|
|
113
|
+
* optionally filtered by trace ID or service name.
|
|
114
|
+
*/
|
|
115
|
+
async listTraces(options = {}) {
|
|
116
|
+
const db = getDb();
|
|
117
|
+
const limit = options.limit ?? 50;
|
|
118
|
+
const offset = options.offset ?? 0;
|
|
119
|
+
let rootSpansQuery = db.selectFrom("spans").select([
|
|
120
|
+
"trace_id",
|
|
121
|
+
"span_name",
|
|
122
|
+
"service_name",
|
|
123
|
+
"status_code",
|
|
124
|
+
"start_time_unix",
|
|
125
|
+
"duration_ns",
|
|
126
|
+
"span_attributes"
|
|
127
|
+
]).where(this.#whereRootSpan()).where(this.#whereHttpRequest()).orderBy("start_time_unix", "desc").limit(limit).offset(offset);
|
|
128
|
+
if (options.traceId) rootSpansQuery = rootSpansQuery.where("trace_id", "=", options.traceId);
|
|
129
|
+
if (options.service) rootSpansQuery = rootSpansQuery.where("service_name", "=", options.service);
|
|
130
|
+
return await db.with("root_spans", () => rootSpansQuery).with("span_counts", (qb) => qb.selectFrom("spans").select(["trace_id", sql`COUNT(*)`.as("span_count")]).where("trace_id", "in", (eb) => eb.selectFrom("root_spans").select("trace_id")).groupBy("trace_id")).selectFrom("root_spans as r").leftJoin("span_counts as c", "r.trace_id", "c.trace_id").select([
|
|
131
|
+
"r.trace_id",
|
|
132
|
+
"r.span_name",
|
|
133
|
+
"r.service_name",
|
|
134
|
+
"r.status_code",
|
|
135
|
+
"r.start_time_unix",
|
|
136
|
+
"r.duration_ns",
|
|
137
|
+
"r.span_attributes",
|
|
138
|
+
sql`COALESCE(c.span_count, 1)`.as("span_count")
|
|
139
|
+
]).orderBy("r.start_time_unix", "desc").execute();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Lists background job root spans (messaging/queue consumers).
|
|
143
|
+
*/
|
|
144
|
+
async listJobs(options = {}) {
|
|
145
|
+
const db = getDb();
|
|
146
|
+
const limit = options.limit ?? 50;
|
|
147
|
+
const offset = options.offset ?? 0;
|
|
148
|
+
let rootSpansQuery = db.selectFrom("spans").select([
|
|
149
|
+
"trace_id",
|
|
150
|
+
"span_name",
|
|
151
|
+
"service_name",
|
|
152
|
+
"status_code",
|
|
153
|
+
"start_time_unix",
|
|
154
|
+
"duration_ns",
|
|
155
|
+
"span_attributes"
|
|
156
|
+
]).where(this.#whereRootSpan()).where(this.#whereJob()).orderBy("start_time_unix", "desc").limit(limit).offset(offset);
|
|
157
|
+
if (options.traceId) rootSpansQuery = rootSpansQuery.where("trace_id", "=", options.traceId);
|
|
158
|
+
return await db.with("root_spans", () => rootSpansQuery).with("span_counts", (qb) => qb.selectFrom("spans").select(["trace_id", sql`COUNT(*)`.as("span_count")]).where("trace_id", "in", (eb) => eb.selectFrom("root_spans").select("trace_id")).groupBy("trace_id")).selectFrom("root_spans as r").leftJoin("span_counts as c", "r.trace_id", "c.trace_id").select([
|
|
159
|
+
"r.trace_id",
|
|
160
|
+
"r.span_name",
|
|
161
|
+
"r.service_name",
|
|
162
|
+
"r.status_code",
|
|
163
|
+
"r.start_time_unix",
|
|
164
|
+
"r.duration_ns",
|
|
165
|
+
"r.span_attributes",
|
|
166
|
+
sql`COALESCE(c.span_count, 1)`.as("span_count")
|
|
167
|
+
]).orderBy("r.start_time_unix", "desc").execute();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Lists CLI command root spans.
|
|
171
|
+
*/
|
|
172
|
+
async listCommands(options = {}) {
|
|
173
|
+
const db = getDb();
|
|
174
|
+
const limit = options.limit ?? 50;
|
|
175
|
+
const offset = options.offset ?? 0;
|
|
176
|
+
let rootSpansQuery = db.selectFrom("spans").select([
|
|
177
|
+
"trace_id",
|
|
178
|
+
"span_name",
|
|
179
|
+
"service_name",
|
|
180
|
+
"status_code",
|
|
181
|
+
"start_time_unix",
|
|
182
|
+
"duration_ns",
|
|
183
|
+
"span_attributes"
|
|
184
|
+
]).where(this.#whereRootSpan()).where(this.#whereCommand()).orderBy("start_time_unix", "desc").limit(limit).offset(offset);
|
|
185
|
+
if (options.traceId) rootSpansQuery = rootSpansQuery.where("trace_id", "=", options.traceId);
|
|
186
|
+
return await db.with("root_spans", () => rootSpansQuery).with("span_counts", (qb) => qb.selectFrom("spans").select(["trace_id", sql`COUNT(*)`.as("span_count")]).where("trace_id", "in", (eb) => eb.selectFrom("root_spans").select("trace_id")).groupBy("trace_id")).selectFrom("root_spans as r").leftJoin("span_counts as c", "r.trace_id", "c.trace_id").select([
|
|
187
|
+
"r.trace_id",
|
|
188
|
+
"r.span_name",
|
|
189
|
+
"r.service_name",
|
|
190
|
+
"r.status_code",
|
|
191
|
+
"r.start_time_unix",
|
|
192
|
+
"r.duration_ns",
|
|
193
|
+
"r.span_attributes",
|
|
194
|
+
sql`COALESCE(c.span_count, 1)`.as("span_count")
|
|
195
|
+
]).orderBy("r.start_time_unix", "desc").execute();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Lists database query spans, optionally filtered by DB system
|
|
199
|
+
* (postgresql, mysql, sqlite, etc.).
|
|
200
|
+
*/
|
|
201
|
+
async listQueries(options = {}) {
|
|
202
|
+
const db = getDb();
|
|
203
|
+
const limit = options.limit ?? 100;
|
|
204
|
+
const offset = options.offset ?? 0;
|
|
205
|
+
let query = db.selectFrom("spans").select([
|
|
206
|
+
"trace_id",
|
|
207
|
+
"span_id",
|
|
208
|
+
"span_name",
|
|
209
|
+
"service_name",
|
|
210
|
+
"status_code",
|
|
211
|
+
"start_time_unix",
|
|
212
|
+
"duration_ns",
|
|
213
|
+
"span_attributes"
|
|
214
|
+
]).where(this.#sqlAttrExists(S.DB_SYSTEM, L.DB_SYSTEM)).orderBy("start_time_unix", "desc").limit(limit).offset(offset);
|
|
215
|
+
if (options.traceId) query = query.where("trace_id", "=", options.traceId);
|
|
216
|
+
if (options.dbSystem) query = query.where(this.#sqlAttr(S.DB_SYSTEM, L.DB_SYSTEM), "=", options.dbSystem);
|
|
217
|
+
return await query.execute();
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Lists exceptions extracted from span events by unnesting
|
|
221
|
+
* the JSON events array and filtering for "exception" events.
|
|
222
|
+
*/
|
|
223
|
+
async listExceptions(options = {}) {
|
|
224
|
+
const db = getDb();
|
|
225
|
+
const limit = options.limit ?? 100;
|
|
226
|
+
const offset = options.offset ?? 0;
|
|
227
|
+
let query = db.selectFrom(sql`spans s, unnest(from_json(s.events, '["JSON"]')) AS e(json)`).select([
|
|
228
|
+
sql`s.trace_id`.as("trace_id"),
|
|
229
|
+
sql`s.span_id`.as("span_id"),
|
|
230
|
+
sql`s.span_name`.as("span_name"),
|
|
231
|
+
sql`s.service_name`.as("service_name"),
|
|
232
|
+
sql`s.start_time_unix`.as("start_time_unix"),
|
|
233
|
+
sql`e.json`.as("event_json")
|
|
234
|
+
]).where(sql`s.events`, "is not", null).where(sql`json_extract_string(e.json, '$.name')`, "=", "exception").orderBy(sql`s.start_time_unix`, "desc").limit(limit).offset(offset);
|
|
235
|
+
if (options.traceId) query = query.where(sql`s.trace_id`, "=", options.traceId);
|
|
236
|
+
return await query.execute();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Deletes all rows from the spans table.
|
|
240
|
+
*/
|
|
241
|
+
async clearAll() {
|
|
242
|
+
const db = getDb();
|
|
243
|
+
await sql`DELETE FROM spans`.execute(db);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Returns all spans belonging to a trace, ordered by start time,
|
|
247
|
+
* with parsed attributes and events.
|
|
248
|
+
*/
|
|
249
|
+
async getTraceSpans(traceId) {
|
|
250
|
+
return getDb().selectFrom("spans").selectAll().where("trace_id", "=", traceId).orderBy("start_time_unix", "asc").execute();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
//#endregion
|
|
254
|
+
export { SpanRepository };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/semconv.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* OpenTelemetry Semantic Convention attribute constants.
|
|
4
|
+
*
|
|
5
|
+
* Re-exports from `@opentelemetry/semantic-conventions` organized by
|
|
6
|
+
* stability level. The official package splits stable vs incubating,
|
|
7
|
+
* but conflates deprecated and experimental under "incubating".
|
|
8
|
+
* We explicitly separate **Stable** (current spec) from **Legacy**
|
|
9
|
+
* (deprecated, kept for old ClickHouse data reference).
|
|
10
|
+
*
|
|
11
|
+
* @see https://opentelemetry.io/docs/specs/semconv/http/http-spans/
|
|
12
|
+
* @see https://opentelemetry.io/docs/specs/semconv/database/database-spans/
|
|
13
|
+
*/
|
|
14
|
+
declare const Semconv: {
|
|
15
|
+
readonly Stable: {
|
|
16
|
+
readonly HTTP_METHOD: "http.request.method";
|
|
17
|
+
readonly HTTP_STATUS_CODE: "http.response.status_code";
|
|
18
|
+
readonly HTTP_ROUTE: "http.route";
|
|
19
|
+
readonly HTTP_TARGET: "url.path";
|
|
20
|
+
readonly DB_SYSTEM: "db.system.name";
|
|
21
|
+
readonly DB_OPERATION: "db.operation.name";
|
|
22
|
+
readonly DB_STATEMENT: "db.query.text";
|
|
23
|
+
readonly DB_TABLE: "db.collection.name";
|
|
24
|
+
readonly DB_NAMESPACE: "db.namespace";
|
|
25
|
+
};
|
|
26
|
+
readonly Legacy: {
|
|
27
|
+
readonly HTTP_METHOD: "http.method";
|
|
28
|
+
readonly HTTP_STATUS_CODE: "http.status_code";
|
|
29
|
+
readonly HTTP_TARGET: "http.target";
|
|
30
|
+
readonly DB_SYSTEM: "db.system";
|
|
31
|
+
readonly DB_OPERATION: "db.operation";
|
|
32
|
+
readonly DB_STATEMENT: "db.statement";
|
|
33
|
+
readonly DB_TABLE: "db.sql.table";
|
|
34
|
+
readonly DB_NAME: "db.name";
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { Semconv };
|
package/dist/semconv.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ATTR_DB_COLLECTION_NAME, ATTR_DB_NAMESPACE, ATTR_DB_OPERATION_NAME, ATTR_DB_QUERY_TEXT, ATTR_DB_SYSTEM_NAME, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_HTTP_ROUTE, ATTR_URL_PATH } from "@opentelemetry/semantic-conventions";
|
|
2
|
+
import { ATTR_DB_NAME, ATTR_DB_OPERATION, ATTR_DB_SQL_TABLE, ATTR_DB_STATEMENT, ATTR_DB_SYSTEM, ATTR_HTTP_METHOD, ATTR_HTTP_STATUS_CODE, ATTR_HTTP_TARGET } from "@opentelemetry/semantic-conventions/incubating";
|
|
3
|
+
//#region src/semconv.ts
|
|
4
|
+
/**
|
|
5
|
+
* OpenTelemetry Semantic Convention attribute constants.
|
|
6
|
+
*
|
|
7
|
+
* Re-exports from `@opentelemetry/semantic-conventions` organized by
|
|
8
|
+
* stability level. The official package splits stable vs incubating,
|
|
9
|
+
* but conflates deprecated and experimental under "incubating".
|
|
10
|
+
* We explicitly separate **Stable** (current spec) from **Legacy**
|
|
11
|
+
* (deprecated, kept for old ClickHouse data reference).
|
|
12
|
+
*
|
|
13
|
+
* @see https://opentelemetry.io/docs/specs/semconv/http/http-spans/
|
|
14
|
+
* @see https://opentelemetry.io/docs/specs/semconv/database/database-spans/
|
|
15
|
+
*/
|
|
16
|
+
const Semconv = {
|
|
17
|
+
Stable: {
|
|
18
|
+
HTTP_METHOD: ATTR_HTTP_REQUEST_METHOD,
|
|
19
|
+
HTTP_STATUS_CODE: ATTR_HTTP_RESPONSE_STATUS_CODE,
|
|
20
|
+
HTTP_ROUTE: ATTR_HTTP_ROUTE,
|
|
21
|
+
HTTP_TARGET: ATTR_URL_PATH,
|
|
22
|
+
DB_SYSTEM: ATTR_DB_SYSTEM_NAME,
|
|
23
|
+
DB_OPERATION: ATTR_DB_OPERATION_NAME,
|
|
24
|
+
DB_STATEMENT: ATTR_DB_QUERY_TEXT,
|
|
25
|
+
DB_TABLE: ATTR_DB_COLLECTION_NAME,
|
|
26
|
+
DB_NAMESPACE: ATTR_DB_NAMESPACE
|
|
27
|
+
},
|
|
28
|
+
Legacy: {
|
|
29
|
+
HTTP_METHOD: ATTR_HTTP_METHOD,
|
|
30
|
+
HTTP_STATUS_CODE: ATTR_HTTP_STATUS_CODE,
|
|
31
|
+
HTTP_TARGET: ATTR_HTTP_TARGET,
|
|
32
|
+
DB_SYSTEM: ATTR_DB_SYSTEM,
|
|
33
|
+
DB_OPERATION: ATTR_DB_OPERATION,
|
|
34
|
+
DB_STATEMENT: ATTR_DB_STATEMENT,
|
|
35
|
+
DB_TABLE: ATTR_DB_SQL_TABLE,
|
|
36
|
+
DB_NAME: ATTR_DB_NAME
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { Semconv };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/server.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for the local DevTools server.
|
|
4
|
+
*/
|
|
5
|
+
interface DevToolsOptions {
|
|
6
|
+
port?: number;
|
|
7
|
+
host?: string;
|
|
8
|
+
dbPath?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Boots the AdonisJS-based DevTools HTTP server with DuckDB
|
|
12
|
+
* storage, OTLP ingestion, and the Studio UI.
|
|
13
|
+
*/
|
|
14
|
+
declare function startServer(options?: DevToolsOptions): Promise<void>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { DevToolsOptions, startServer };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { closeConnection, initConnection } from "./db/connection.js";
|
|
2
|
+
import { initializeSchema } from "./db/schema.js";
|
|
3
|
+
import { Ignitor, prettyPrintError } from "@adonisjs/core";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import "reflect-metadata";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
//#region src/server.ts
|
|
8
|
+
function printStartupBanner(options) {
|
|
9
|
+
const dim = (s) => `\x1B[2m${s}\x1B[22m`;
|
|
10
|
+
const bold = (s) => `\x1B[1m${s}\x1B[22m`;
|
|
11
|
+
const cyan = (s) => `\x1B[36m${s}\x1B[39m`;
|
|
12
|
+
const green = (s) => `\x1B[32m${s}\x1B[39m`;
|
|
13
|
+
const local = `http://localhost:${options.port}`;
|
|
14
|
+
console.log();
|
|
15
|
+
console.log(` ${cyan(bold("Monocle Studio"))} ${dim("v0.1.0")}`);
|
|
16
|
+
console.log(` ${dim("Local observability for your app. Traces, logs, queries & more.")}`);
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(` ${green("➜")} ${bold("Dashboard:")} ${cyan(local)}`);
|
|
19
|
+
console.log(` ${green("➜")} ${bold("OTLP:")} ${dim(`http://localhost:${options.port}/v1/traces`)}`);
|
|
20
|
+
console.log(` ${green("➜")} ${bold("Docs:")} ${dim("https://docs.monocle.sh/studio/overview")}`);
|
|
21
|
+
console.log();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Boots the AdonisJS-based DevTools HTTP server with DuckDB
|
|
25
|
+
* storage, OTLP ingestion, and the Studio UI.
|
|
26
|
+
*/
|
|
27
|
+
async function startServer(options = {}) {
|
|
28
|
+
const port = options.port ?? 4200;
|
|
29
|
+
const host = options.host ?? "0.0.0.0";
|
|
30
|
+
const dbPath = options.dbPath ?? join(homedir(), ".config", "monocle", "studio.db");
|
|
31
|
+
const APP_ROOT = new URL("./", import.meta.url);
|
|
32
|
+
const IMPORTER = (filePath) => {
|
|
33
|
+
if (filePath.startsWith("./") || filePath.startsWith("../")) return import(new URL(filePath, APP_ROOT).href);
|
|
34
|
+
return import(filePath);
|
|
35
|
+
};
|
|
36
|
+
process.env.PORT = String(port);
|
|
37
|
+
process.env.HOST = host;
|
|
38
|
+
process.env.NODE_ENV = "development";
|
|
39
|
+
process.env.APP_KEY = process.env.APP_KEY || "devtools-local-key-not-used-for-security";
|
|
40
|
+
/**
|
|
41
|
+
* Force exit on CTRL+C. Close DuckDB first.
|
|
42
|
+
*/
|
|
43
|
+
process.on("SIGINT", async () => {
|
|
44
|
+
console.log(`\n \x1B[2mShutting down...\x1B[22m`);
|
|
45
|
+
await closeConnection();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
process.on("SIGTERM", async () => {
|
|
49
|
+
await closeConnection();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
});
|
|
52
|
+
new Ignitor(APP_ROOT, { importer: IMPORTER }).tap((app) => {
|
|
53
|
+
app.booting(async () => {
|
|
54
|
+
await initializeSchema(await initConnection(dbPath));
|
|
55
|
+
});
|
|
56
|
+
app.ready(async () => {
|
|
57
|
+
printStartupBanner({
|
|
58
|
+
port,
|
|
59
|
+
host
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}).httpServer().start().catch((error) => {
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
prettyPrintError(error);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { startServer };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
|
|
3
|
+
//#region src/services/event_bus.d.ts
|
|
4
|
+
interface DevToolsEvent {
|
|
5
|
+
type: 'traces' | 'logs';
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* In-process event bus for broadcasting telemetry ingestion
|
|
10
|
+
* events to SSE subscribers in real time.
|
|
11
|
+
*/
|
|
12
|
+
declare class DevToolsEventBus extends EventEmitter {
|
|
13
|
+
/**
|
|
14
|
+
* Broadcasts a new ingestion event to all listeners.
|
|
15
|
+
*/
|
|
16
|
+
emitIngested(event: DevToolsEvent): void;
|
|
17
|
+
/**
|
|
18
|
+
* Subscribes to ingestion events. Returns an unsubscribe function.
|
|
19
|
+
*/
|
|
20
|
+
onIngested(callback: (event: DevToolsEvent) => void): () => this;
|
|
21
|
+
}
|
|
22
|
+
declare const eventBus: DevToolsEventBus;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { DevToolsEvent, eventBus };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
//#region src/services/event_bus.ts
|
|
3
|
+
/**
|
|
4
|
+
* In-process event bus for broadcasting telemetry ingestion
|
|
5
|
+
* events to SSE subscribers in real time.
|
|
6
|
+
*/
|
|
7
|
+
var DevToolsEventBus = class extends EventEmitter {
|
|
8
|
+
/**
|
|
9
|
+
* Broadcasts a new ingestion event to all listeners.
|
|
10
|
+
*/
|
|
11
|
+
emitIngested(event) {
|
|
12
|
+
this.emit("ingested", event);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Subscribes to ingestion events. Returns an unsubscribe function.
|
|
16
|
+
*/
|
|
17
|
+
onIngested(callback) {
|
|
18
|
+
this.on("ingested", callback);
|
|
19
|
+
return () => this.off("ingested", callback);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const eventBus = new DevToolsEventBus();
|
|
23
|
+
//#endregion
|
|
24
|
+
export { eventBus };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FlatLog } from "../types/logs.js";
|
|
2
|
+
import { FlatSpan } from "../types/spans.js";
|
|
3
|
+
import { OtlpAttribute, OtlpLogRequest, OtlpTraceRequest } from "../types/otlp.js";
|
|
4
|
+
|
|
5
|
+
//#region src/services/otlp_parser.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Flattens an array of OTLP key-value attributes into a plain object.
|
|
8
|
+
*/
|
|
9
|
+
declare function flattenAttributes(attrs?: OtlpAttribute[]): Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Converts an OTLP trace export request into flat span models
|
|
12
|
+
* ready for DuckDB insertion.
|
|
13
|
+
*/
|
|
14
|
+
declare function parseTraceRequest(body: OtlpTraceRequest): FlatSpan[];
|
|
15
|
+
/**
|
|
16
|
+
* Converts an OTLP log export request into flat log models
|
|
17
|
+
* ready for DuckDB insertion.
|
|
18
|
+
*/
|
|
19
|
+
declare function parseLogRequest(body: OtlpLogRequest): FlatLog[];
|
|
20
|
+
//#endregion
|
|
21
|
+
export { flattenAttributes, parseLogRequest, parseTraceRequest };
|