@forwardimpact/svctrace 0.1.34 → 0.1.35
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/package.json +7 -1
- package/proto/trace.proto +68 -0
- package/server.js +0 -18
- package/test/trace.test.js +0 -337
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/svctrace",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"description": "Trace service for receiving and storing OpenTelemetry spans in Guide",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "D. Olsson <hi@senzilla.io>",
|
|
@@ -24,5 +24,11 @@
|
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@forwardimpact/libharness": "^0.1.5"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"proto/"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
27
33
|
}
|
|
28
34
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package trace;
|
|
4
|
+
|
|
5
|
+
service Trace {
|
|
6
|
+
rpc RecordSpan(Span) returns (RecordResponse);
|
|
7
|
+
rpc QuerySpans(QueryRequest) returns (QueryResponse);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum Kind {
|
|
11
|
+
UNSPECIFIED = 0;
|
|
12
|
+
INTERNAL = 1;
|
|
13
|
+
SERVER = 2;
|
|
14
|
+
CLIENT = 3;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
enum Code {
|
|
18
|
+
UNSET = 0;
|
|
19
|
+
OK = 1;
|
|
20
|
+
ERROR = 2;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
message Span {
|
|
24
|
+
string trace_id = 1;
|
|
25
|
+
string span_id = 2;
|
|
26
|
+
string parent_span_id = 3;
|
|
27
|
+
string name = 4;
|
|
28
|
+
Kind kind = 5;
|
|
29
|
+
int64 start_time_unix_nano = 6;
|
|
30
|
+
int64 end_time_unix_nano = 7;
|
|
31
|
+
map<string, string> attributes = 8;
|
|
32
|
+
repeated Event events = 9;
|
|
33
|
+
Status status = 10;
|
|
34
|
+
Resource resource = 11;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
message Event {
|
|
38
|
+
string name = 1;
|
|
39
|
+
int64 time_unix_nano = 2;
|
|
40
|
+
map<string, string> attributes = 3;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
message Resource {
|
|
44
|
+
map<string, string> attributes = 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
message Status {
|
|
48
|
+
Code code = 1;
|
|
49
|
+
string message = 2;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
message RecordResponse {
|
|
53
|
+
bool success = 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
message QueryRequest {
|
|
57
|
+
optional string query = 1;
|
|
58
|
+
optional Filter filter = 2;
|
|
59
|
+
|
|
60
|
+
message Filter {
|
|
61
|
+
optional string trace_id = 1;
|
|
62
|
+
optional string resource_id = 2;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
message QueryResponse {
|
|
67
|
+
repeated Span spans = 1;
|
|
68
|
+
}
|
package/server.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Server } from "@forwardimpact/librpc";
|
|
2
|
-
import { createServiceConfig } from "@forwardimpact/libconfig";
|
|
3
|
-
import { createStorage } from "@forwardimpact/libstorage";
|
|
4
|
-
import { TraceIndex } from "@forwardimpact/libtelemetry/index/trace.js";
|
|
5
|
-
|
|
6
|
-
import { TraceService } from "./index.js";
|
|
7
|
-
|
|
8
|
-
const config = await createServiceConfig("trace");
|
|
9
|
-
|
|
10
|
-
// Initialize storage for traces
|
|
11
|
-
const traceStorage = createStorage("traces");
|
|
12
|
-
|
|
13
|
-
// Create trace index
|
|
14
|
-
const traceIndex = new TraceIndex(traceStorage);
|
|
15
|
-
|
|
16
|
-
const service = new TraceService(config, traceIndex);
|
|
17
|
-
const server = new Server(service, config);
|
|
18
|
-
await server.start();
|
package/test/trace.test.js
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import { test, describe, beforeEach } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
|
|
4
|
-
// Module under test
|
|
5
|
-
import { TraceService } from "../index.js";
|
|
6
|
-
import { TraceIndex } from "@forwardimpact/libtelemetry/index/trace.js";
|
|
7
|
-
import { trace } from "@forwardimpact/libtype";
|
|
8
|
-
import { createMockConfig, createMockStorage } from "@forwardimpact/libharness";
|
|
9
|
-
|
|
10
|
-
describe("trace service", () => {
|
|
11
|
-
describe("TraceService", () => {
|
|
12
|
-
test("exports TraceService class", () => {
|
|
13
|
-
assert.strictEqual(typeof TraceService, "function");
|
|
14
|
-
assert.ok(TraceService.prototype);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("TraceService has RecordSpan method", () => {
|
|
18
|
-
assert.strictEqual(typeof TraceService.prototype.RecordSpan, "function");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("TraceService has QuerySpans method", () => {
|
|
22
|
-
assert.strictEqual(typeof TraceService.prototype.QuerySpans, "function");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("TraceService has shutdown method", () => {
|
|
26
|
-
assert.strictEqual(typeof TraceService.prototype.shutdown, "function");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("TraceService constructor accepts expected parameters", () => {
|
|
30
|
-
// Test constructor signature by checking parameter count
|
|
31
|
-
assert.strictEqual(TraceService.length, 2); // config, traceIndex
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("TraceService has proper method signatures", () => {
|
|
35
|
-
const methods = Object.getOwnPropertyNames(TraceService.prototype);
|
|
36
|
-
assert(methods.includes("RecordSpan"));
|
|
37
|
-
assert(methods.includes("QuerySpans"));
|
|
38
|
-
assert(methods.includes("shutdown"));
|
|
39
|
-
assert(methods.includes("constructor"));
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("TraceService business logic", () => {
|
|
44
|
-
let mockConfig;
|
|
45
|
-
let mockTraceIndex;
|
|
46
|
-
let mockStorage;
|
|
47
|
-
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
mockConfig = createMockConfig("trace");
|
|
50
|
-
|
|
51
|
-
// Create mock storage for TraceIndex
|
|
52
|
-
mockStorage = createMockStorage();
|
|
53
|
-
|
|
54
|
-
// Use real TraceIndex with mock storage for more realistic testing
|
|
55
|
-
mockTraceIndex = new TraceIndex(mockStorage, "test-traces.jsonl");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("creates service instance with trace index", () => {
|
|
59
|
-
const service = new TraceService(mockConfig, mockTraceIndex);
|
|
60
|
-
|
|
61
|
-
assert.ok(service);
|
|
62
|
-
assert.strictEqual(service.config, mockConfig);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("throws error if traceIndex is missing", () => {
|
|
66
|
-
assert.throws(
|
|
67
|
-
() => {
|
|
68
|
-
new TraceService(mockConfig);
|
|
69
|
-
},
|
|
70
|
-
{ message: "traceIndex is required" },
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("RecordSpan stores span in index", async () => {
|
|
75
|
-
const service = new TraceService(mockConfig, mockTraceIndex);
|
|
76
|
-
|
|
77
|
-
const spanRequest = trace.Span.fromObject({
|
|
78
|
-
trace_id: "trace123",
|
|
79
|
-
span_id: "span456",
|
|
80
|
-
parent_span_id: "",
|
|
81
|
-
name: "TestOperation",
|
|
82
|
-
kind: "SERVER",
|
|
83
|
-
start_time_unix_nano: "1698345600000000000",
|
|
84
|
-
end_time_unix_nano: "1698345601000000000",
|
|
85
|
-
attributes: { "service.name": "test-service" },
|
|
86
|
-
events: [],
|
|
87
|
-
status: { code: trace.Code.OK, message: "" },
|
|
88
|
-
resource: { attributes: {} },
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const result = await service.RecordSpan(spanRequest);
|
|
92
|
-
|
|
93
|
-
assert.ok(result);
|
|
94
|
-
assert.strictEqual(result.success, true);
|
|
95
|
-
assert.strictEqual(mockTraceIndex.index.size, 1);
|
|
96
|
-
assert.ok(mockTraceIndex.index.has("span456"));
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("QuerySpans returns stored spans", async () => {
|
|
100
|
-
const service = new TraceService(mockConfig, mockTraceIndex);
|
|
101
|
-
|
|
102
|
-
// Record a span first
|
|
103
|
-
await service.RecordSpan(
|
|
104
|
-
trace.Span.fromObject({
|
|
105
|
-
trace_id: "trace123",
|
|
106
|
-
span_id: "span456",
|
|
107
|
-
parent_span_id: "",
|
|
108
|
-
name: "TestOperation",
|
|
109
|
-
kind: "SERVER",
|
|
110
|
-
start_time_unix_nano: "1698345600000000000",
|
|
111
|
-
end_time_unix_nano: "1698345601000000000",
|
|
112
|
-
attributes: { "service.name": "test-service" },
|
|
113
|
-
events: [],
|
|
114
|
-
status: { code: trace.Code.OK, message: "" },
|
|
115
|
-
resource: { attributes: {} },
|
|
116
|
-
}),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const result = await service.QuerySpans({
|
|
120
|
-
query: null,
|
|
121
|
-
filter: { trace_id: "trace123" },
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
assert.ok(result);
|
|
125
|
-
assert.ok(Array.isArray(result.spans));
|
|
126
|
-
assert.strictEqual(result.spans.length, 1);
|
|
127
|
-
assert.strictEqual(result.spans[0].span_id, "span456");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("QuerySpans filters by trace_id", async () => {
|
|
131
|
-
const service = new TraceService(mockConfig, mockTraceIndex);
|
|
132
|
-
|
|
133
|
-
// Record spans with different trace IDs
|
|
134
|
-
await service.RecordSpan(
|
|
135
|
-
trace.Span.fromObject({
|
|
136
|
-
trace_id: "trace123",
|
|
137
|
-
span_id: "span1",
|
|
138
|
-
parent_span_id: "",
|
|
139
|
-
name: "Operation1",
|
|
140
|
-
kind: "SERVER",
|
|
141
|
-
start_time_unix_nano: "1698345600000000000",
|
|
142
|
-
end_time_unix_nano: "1698345601000000000",
|
|
143
|
-
attributes: {},
|
|
144
|
-
events: [],
|
|
145
|
-
status: { code: trace.Code.OK, message: "" },
|
|
146
|
-
resource: { attributes: {} },
|
|
147
|
-
}),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
await service.RecordSpan(
|
|
151
|
-
trace.Span.fromObject({
|
|
152
|
-
trace_id: "trace456",
|
|
153
|
-
span_id: "span2",
|
|
154
|
-
parent_span_id: "",
|
|
155
|
-
name: "Operation2",
|
|
156
|
-
kind: "SERVER",
|
|
157
|
-
start_time_unix_nano: "1698345600000000000",
|
|
158
|
-
end_time_unix_nano: "1698345601000000000",
|
|
159
|
-
attributes: {},
|
|
160
|
-
events: [],
|
|
161
|
-
status: { code: trace.Code.OK, message: "" },
|
|
162
|
-
resource: { attributes: {} },
|
|
163
|
-
}),
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const result = await service.QuerySpans({
|
|
167
|
-
query: null,
|
|
168
|
-
filter: { trace_id: "trace123" },
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
assert.ok(result);
|
|
172
|
-
assert.ok(Array.isArray(result.spans));
|
|
173
|
-
assert.strictEqual(result.spans.length, 1);
|
|
174
|
-
assert.strictEqual(result.spans[0].trace_id, "trace123");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("QuerySpans respects limit parameter", async () => {
|
|
178
|
-
const service = new TraceService(mockConfig, mockTraceIndex);
|
|
179
|
-
|
|
180
|
-
// Record multiple spans
|
|
181
|
-
for (let i = 0; i < 5; i++) {
|
|
182
|
-
await service.RecordSpan(
|
|
183
|
-
trace.Span.fromObject({
|
|
184
|
-
trace_id: "trace123",
|
|
185
|
-
span_id: `span${i}`,
|
|
186
|
-
parent_span_id: "",
|
|
187
|
-
name: `Operation${i}`,
|
|
188
|
-
kind: "INTERNAL",
|
|
189
|
-
start_time_unix_nano: "1698345600000000000",
|
|
190
|
-
end_time_unix_nano: "1698345601000000000",
|
|
191
|
-
attributes: {},
|
|
192
|
-
events: [],
|
|
193
|
-
status: { code: trace.Code.OK, message: "" },
|
|
194
|
-
resource: { attributes: {} },
|
|
195
|
-
}),
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const result = await service.QuerySpans({
|
|
200
|
-
query: null,
|
|
201
|
-
filter: { trace_id: "trace123" },
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
assert.ok(result);
|
|
205
|
-
assert.ok(Array.isArray(result.spans));
|
|
206
|
-
assert.strictEqual(result.spans.length, 5);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("RecordSpan handles span with events", async () => {
|
|
210
|
-
const service = new TraceService(mockConfig, mockTraceIndex);
|
|
211
|
-
|
|
212
|
-
const spanRequest = trace.Span.fromObject({
|
|
213
|
-
trace_id: "trace123",
|
|
214
|
-
span_id: "span456",
|
|
215
|
-
parent_span_id: "span123",
|
|
216
|
-
name: "TestOperation",
|
|
217
|
-
kind: "CLIENT",
|
|
218
|
-
start_time_unix_nano: "1698345600000000000",
|
|
219
|
-
end_time_unix_nano: "1698345601000000000",
|
|
220
|
-
attributes: { "service.name": "test-service", "rpc.method": "Test" },
|
|
221
|
-
events: [
|
|
222
|
-
{
|
|
223
|
-
name: "cache_hit",
|
|
224
|
-
time_unix_nano: "1698345600500000000",
|
|
225
|
-
attributes: { hit_rate: "0.95" },
|
|
226
|
-
},
|
|
227
|
-
],
|
|
228
|
-
status: { code: trace.Code.OK, message: "" },
|
|
229
|
-
resource: { attributes: {} },
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const result = await service.RecordSpan(spanRequest);
|
|
233
|
-
|
|
234
|
-
assert.ok(result);
|
|
235
|
-
assert.strictEqual(result.success, true);
|
|
236
|
-
|
|
237
|
-
const stored = mockTraceIndex.index.get("span456");
|
|
238
|
-
assert.ok(stored);
|
|
239
|
-
assert.strictEqual(stored.span.events.length, 1);
|
|
240
|
-
assert.strictEqual(stored.span.events[0].name, "cache_hit");
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("RecordSpan handles error status", async () => {
|
|
244
|
-
const service = new TraceService(mockConfig, mockTraceIndex);
|
|
245
|
-
|
|
246
|
-
const spanRequest = trace.Span.fromObject({
|
|
247
|
-
trace_id: "trace123",
|
|
248
|
-
span_id: "span789",
|
|
249
|
-
parent_span_id: "",
|
|
250
|
-
name: "FailedOperation",
|
|
251
|
-
kind: "INTERNAL",
|
|
252
|
-
start_time_unix_nano: "1698345600000000000",
|
|
253
|
-
end_time_unix_nano: "1698345601000000000",
|
|
254
|
-
attributes: { "service.name": "test-service" },
|
|
255
|
-
events: [],
|
|
256
|
-
status: {
|
|
257
|
-
code: trace.Code.ERROR,
|
|
258
|
-
message: "Connection timeout",
|
|
259
|
-
},
|
|
260
|
-
resource: { attributes: {} },
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const result = await service.RecordSpan(spanRequest);
|
|
264
|
-
|
|
265
|
-
assert.ok(result);
|
|
266
|
-
assert.strictEqual(result.success, true);
|
|
267
|
-
|
|
268
|
-
const stored = mockTraceIndex.index.get("span789");
|
|
269
|
-
assert.ok(stored);
|
|
270
|
-
assert.strictEqual(stored.span.status.code, trace.Code.ERROR);
|
|
271
|
-
assert.strictEqual(stored.span.status.message, "Connection timeout");
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test("QuerySpans requires either trace_id or resource_id", async () => {
|
|
275
|
-
const service = new TraceService(mockConfig, mockTraceIndex);
|
|
276
|
-
|
|
277
|
-
await assert.rejects(
|
|
278
|
-
async () => {
|
|
279
|
-
await service.QuerySpans({});
|
|
280
|
-
},
|
|
281
|
-
{ message: "Either query, trace_id, or resource_id is required" },
|
|
282
|
-
);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("QuerySpans filters by resource_id", async () => {
|
|
286
|
-
const service = new TraceService(mockConfig, mockTraceIndex);
|
|
287
|
-
|
|
288
|
-
// Record spans with resource_id
|
|
289
|
-
await service.RecordSpan(
|
|
290
|
-
trace.Span.fromObject({
|
|
291
|
-
trace_id: "trace123",
|
|
292
|
-
span_id: "span1",
|
|
293
|
-
parent_span_id: "",
|
|
294
|
-
name: "agent.ProcessStream",
|
|
295
|
-
kind: "SERVER",
|
|
296
|
-
start_time_unix_nano: "1698345600000000000",
|
|
297
|
-
end_time_unix_nano: "1698345601000000000",
|
|
298
|
-
attributes: {},
|
|
299
|
-
events: [],
|
|
300
|
-
status: { code: trace.Code.OK, message: "" },
|
|
301
|
-
resource: { attributes: {} },
|
|
302
|
-
}),
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
await service.RecordSpan(
|
|
306
|
-
trace.Span.fromObject({
|
|
307
|
-
trace_id: "trace123",
|
|
308
|
-
span_id: "span2",
|
|
309
|
-
parent_span_id: "span1",
|
|
310
|
-
name: "memory.AppendMemory",
|
|
311
|
-
kind: "CLIENT",
|
|
312
|
-
start_time_unix_nano: "1698345600000000000",
|
|
313
|
-
end_time_unix_nano: "1698345601000000000",
|
|
314
|
-
attributes: {},
|
|
315
|
-
events: [],
|
|
316
|
-
status: { code: trace.Code.OK, message: "" },
|
|
317
|
-
resource: { attributes: { id: "common.Conversation.test123" } },
|
|
318
|
-
}),
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
const result = await service.QuerySpans({
|
|
322
|
-
query: null,
|
|
323
|
-
filter: { resource_id: "common.Conversation.test123" },
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
assert.ok(result);
|
|
327
|
-
assert.ok(Array.isArray(result.spans));
|
|
328
|
-
assert.strictEqual(
|
|
329
|
-
result.spans.length,
|
|
330
|
-
2,
|
|
331
|
-
"Should return both child with resource_id and parent without it",
|
|
332
|
-
);
|
|
333
|
-
const spanIds = result.spans.map((s) => s.span_id).sort();
|
|
334
|
-
assert.deepStrictEqual(spanIds, ["span1", "span2"]);
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
});
|