@vzn/vx-otel 0.0.519 → 0.0.520
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/README.md +11 -1
- package/package.json +2 -2
- package/src/otlp.ts +72 -0
- package/src/sink.ts +23 -1
package/README.md
CHANGED
|
@@ -105,7 +105,17 @@ otel({
|
|
|
105
105
|
a sandboxed one its violation count (`vx.task.sandbox_violations`), on a
|
|
106
106
|
persistent one that never became ready why (`vx.task.not_ready`). A
|
|
107
107
|
failed task sets span status
|
|
108
|
-
`ERROR`.
|
|
108
|
+
`ERROR`. A task span links to the spans of the tasks it waited on (a
|
|
109
|
+
group seen through to the tasks behind it), and carries an event per
|
|
110
|
+
attempt that failed and was run again (`vx.task.retry`, with
|
|
111
|
+
`vx.task.attempt` and `vx.task.exit_code`) and one when vx's own
|
|
112
|
+
timeout killed it (`vx.task.timeout`);
|
|
113
|
+
- a child span per stage of the run, named by the stage (`startup`,
|
|
114
|
+
`load configs`, `git enumeration`, `classify + probe`, `run graph`,
|
|
115
|
+
`record history`, …, the stages `VX_TIMING` prints), with
|
|
116
|
+
`vx.stage.name`: where the time before the first task and after the
|
|
117
|
+
last went. The stages ahead of the run lock end before the `vx.run`
|
|
118
|
+
span starts.
|
|
109
119
|
|
|
110
120
|
**Metrics per run**: `vx.tasks.total`, `vx.tasks.failed`,
|
|
111
121
|
`vx.tasks.cache_hits{source=local|remote}`, what those hits did to the disk as
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-otel",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.520",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "OpenTelemetry exporter for vx — a telemetry plugin mapping runs to OTLP traces + metrics (CI/CD + VCS semantic conventions).",
|
|
6
6
|
"keywords": [
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@vzn/vx": "^0.0.
|
|
37
|
+
"@vzn/vx": "^0.0.520"
|
|
38
38
|
},
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|
package/src/otlp.ts
CHANGED
|
@@ -126,6 +126,8 @@ const VX_ATTR = {
|
|
|
126
126
|
taskOutputs: 'vx.task.outputs',
|
|
127
127
|
peakRssBytes: 'vx.peak_rss_bytes',
|
|
128
128
|
taskAttempts: 'vx.task.attempts',
|
|
129
|
+
taskAttempt: 'vx.task.attempt',
|
|
130
|
+
stageName: 'vx.stage.name',
|
|
129
131
|
wallclockStartNs: 'vx.task.wallclock_start_ns',
|
|
130
132
|
wallclockEndNs: 'vx.task.wallclock_end_ns',
|
|
131
133
|
// task log records (the OTel Logs signal)
|
|
@@ -151,6 +153,76 @@ export interface OtlpSpan {
|
|
|
151
153
|
endTimeUnixNano: string
|
|
152
154
|
attributes: KeyValue[]
|
|
153
155
|
status: { code: number }
|
|
156
|
+
events?: OtlpSpanEvent[]
|
|
157
|
+
links?: OtlpSpanLink[]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface OtlpSpanEvent {
|
|
161
|
+
timeUnixNano: string
|
|
162
|
+
name: string
|
|
163
|
+
attributes: KeyValue[]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface OtlpSpanLink {
|
|
167
|
+
traceId: string
|
|
168
|
+
spanId: string
|
|
169
|
+
attributes: KeyValue[]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A task span's events: each attempt that failed and was run again
|
|
174
|
+
* (`vx.task.retry`, at its end), and vx's own deadline killing the task
|
|
175
|
+
* (`vx.task.timeout`, at the span's end).
|
|
176
|
+
*/
|
|
177
|
+
export function taskSpanEvents(t: TaskTelemetry, endUnixNano: string): OtlpSpanEvent[] {
|
|
178
|
+
const events: OtlpSpanEvent[] = (t.failedAttempts ?? []).map((a, i) => ({
|
|
179
|
+
timeUnixNano: String(Math.trunc(a.endedAt) * 1_000_000),
|
|
180
|
+
name: 'vx.task.retry',
|
|
181
|
+
attributes: [
|
|
182
|
+
intAttr(VX_ATTR.taskAttempt, i + 1),
|
|
183
|
+
intAttr(VX_ATTR.taskExitCode, a.exitCode),
|
|
184
|
+
...(a.timedOut === true ? [boolAttr(VX_ATTR.taskTimedOut, true)] : []),
|
|
185
|
+
],
|
|
186
|
+
}))
|
|
187
|
+
if (t.timedOut === true) {
|
|
188
|
+
events.push({ timeUnixNano: endUnixNano, name: 'vx.task.timeout', attributes: [] })
|
|
189
|
+
}
|
|
190
|
+
return events
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* A child span of the run for one of its stages (`VX_TIMING`'s marks):
|
|
195
|
+
* where the time before the first task and after the last one went. Named
|
|
196
|
+
* by the stage, a fixed set. The stages ahead of the run lock end before
|
|
197
|
+
* the run span starts.
|
|
198
|
+
*/
|
|
199
|
+
export function stageSpan(
|
|
200
|
+
traceId: string,
|
|
201
|
+
parentSpanId: string,
|
|
202
|
+
stage: { name: string; startedAt: number; endedAt: number },
|
|
203
|
+
spanId: string,
|
|
204
|
+
): OtlpSpan {
|
|
205
|
+
return {
|
|
206
|
+
traceId,
|
|
207
|
+
spanId,
|
|
208
|
+
parentSpanId,
|
|
209
|
+
name: stage.name,
|
|
210
|
+
kind: SPAN_KIND_INTERNAL,
|
|
211
|
+
startTimeUnixNano: exactNanos(stage.startedAt),
|
|
212
|
+
endTimeUnixNano: exactNanos(stage.endedAt),
|
|
213
|
+
attributes: [strAttr(VX_ATTR.stageName, stage.name)],
|
|
214
|
+
status: { code: STATUS_UNSET },
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Epoch ms with a fraction, as OTLP's int64 nanoseconds: a stage can be under 1 ms. */
|
|
219
|
+
function exactNanos(ms: number): string {
|
|
220
|
+
return String(BigInt(Math.round(ms * 1000)) * 1000n)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** A link from a task span to the span of a task it waited on. */
|
|
224
|
+
export function dependencyLink(traceId: string, spanId: string, taskId: string): OtlpSpanLink {
|
|
225
|
+
return { traceId, spanId, attributes: [strAttr(SEMCONV.taskName, taskId)] }
|
|
154
226
|
}
|
|
155
227
|
|
|
156
228
|
// --- attribute mapping (pure) ------------------------------------------
|
package/src/sink.ts
CHANGED
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
type TaskMetricPoint,
|
|
23
23
|
runSpanAttributes,
|
|
24
24
|
SPAN_KIND_INTERNAL,
|
|
25
|
+
dependencyLink,
|
|
26
|
+
stageSpan,
|
|
27
|
+
taskSpanEvents,
|
|
25
28
|
runStatusCode,
|
|
26
29
|
taskSpanAttributes,
|
|
27
30
|
taskStatusCode,
|
|
@@ -276,6 +279,7 @@ export class OtelSink implements TelemetrySink {
|
|
|
276
279
|
private readonly lastSample = new Map<string, { ts: number; cpuMs: number }>()
|
|
277
280
|
private readonly taskSpanId = new Map<string, string>()
|
|
278
281
|
private readonly taskStartNano = new Map<string, string>()
|
|
282
|
+
private readonly taskDeps = new Map<string, readonly string[]>()
|
|
279
283
|
private summary: RunSummaryRecord | undefined
|
|
280
284
|
private uploaded = false
|
|
281
285
|
// Core's own bounded capture buffer, so which task's output survives a
|
|
@@ -331,6 +335,7 @@ export class OtelSink implements TelemetrySink {
|
|
|
331
335
|
case 'task.start':
|
|
332
336
|
this.taskSpanId.set(record.taskId, genId(8))
|
|
333
337
|
this.taskStartNano.set(record.taskId, nanos(record.ts))
|
|
338
|
+
if (record.dependsOn !== undefined) this.taskDeps.set(record.taskId, record.dependsOn)
|
|
334
339
|
this.taskNames.set(record.taskId, {
|
|
335
340
|
project: record.project,
|
|
336
341
|
task: record.task,
|
|
@@ -362,7 +367,18 @@ export class OtelSink implements TelemetrySink {
|
|
|
362
367
|
this.logs.append(record.taskId, record.chunk)
|
|
363
368
|
return
|
|
364
369
|
case 'task.end': {
|
|
365
|
-
|
|
370
|
+
// A task that never started (a skip) still gets a span, and a later
|
|
371
|
+
// task that waited on it links to it.
|
|
372
|
+
let spanId = this.taskSpanId.get(record.taskId)
|
|
373
|
+
if (spanId === undefined) {
|
|
374
|
+
spanId = genId(8)
|
|
375
|
+
this.taskSpanId.set(record.taskId, spanId)
|
|
376
|
+
}
|
|
377
|
+
const links = (this.taskDeps.get(record.taskId) ?? []).flatMap((dep) => {
|
|
378
|
+
const depSpan = this.taskSpanId.get(dep)
|
|
379
|
+
return depSpan === undefined ? [] : [dependencyLink(this.traceId, depSpan, dep)]
|
|
380
|
+
})
|
|
381
|
+
const events = taskSpanEvents(record, nanos(record.ts))
|
|
366
382
|
const startNano =
|
|
367
383
|
this.taskStartNano.get(record.taskId) ?? nanos(record.ts - record.durationMs)
|
|
368
384
|
const t: TaskTelemetry = record
|
|
@@ -380,6 +396,8 @@ export class OtelSink implements TelemetrySink {
|
|
|
380
396
|
startedAt: this.runStartedAt,
|
|
381
397
|
}),
|
|
382
398
|
status: { code: taskStatusCode(t) },
|
|
399
|
+
...(events.length > 0 ? { events } : {}),
|
|
400
|
+
...(links.length > 0 ? { links } : {}),
|
|
383
401
|
})
|
|
384
402
|
// A skipped task never ran: a zero duration would read as a fast one.
|
|
385
403
|
if (record.status !== 'skipped') {
|
|
@@ -437,6 +455,10 @@ export class OtelSink implements TelemetrySink {
|
|
|
437
455
|
attributes: runSpanAttributes(this.run, this.summary),
|
|
438
456
|
status: { code: runStatusCode(this.summary) },
|
|
439
457
|
})
|
|
458
|
+
const stages = (this.summary?.stages ?? []).map((stage) =>
|
|
459
|
+
stageSpan(this.traceId, this.rootSpanId, stage, genId(8)),
|
|
460
|
+
)
|
|
461
|
+
this.spans.splice(1, 0, ...stages)
|
|
440
462
|
}
|
|
441
463
|
const vxVersion = this.run?.vxVersion ?? '0.0.0'
|
|
442
464
|
await Promise.all([this.shipTraces(vxVersion), this.shipMetrics(), this.shipLogs(vxVersion)])
|