@vzn/vx-otel 0.0.527 → 0.0.528

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 (3) hide show
  1. package/README.md +14 -5
  2. package/package.json +2 -2
  3. package/src/otlp.ts +37 -5
package/README.md CHANGED
@@ -107,8 +107,13 @@ otel({
107
107
  command (`vx.task.command`, secrets masked), on a flaky one its record
108
108
  (`vx.task.flaky.passes`, `vx.task.flaky.failures`), on a hit what the
109
109
  stored run took (`vx.cache.stored_duration_ms`, `vx.cache.stored_cpu_ms`,
110
- `vx.cache.stored_peak_rss_bytes`), and how long an `admit` policy held it
111
- (`vx.task.admission_held_ms`). A
110
+ `vx.cache.stored_peak_rss_bytes`), how long an `admit` policy held it
111
+ (`vx.task.admission_held_ms`), how long it waited ready for a worker
112
+ (`vx.task.queued_ms`, the hold included), and on a cacheable task that
113
+ ran how many files its key read (`vx.task.input_files`) and what its key
114
+ changed since the last entry the cache saved for it
115
+ (`vx.cache.miss.change_count`, and `vx.cache.miss.changes`, the first ten
116
+ as `changed file packages/a/src/x.ts`). A
112
117
  failed task sets span status
113
118
  `ERROR`. A task span links to the spans of the tasks it waited on (a
114
119
  group seen through to the tasks behind it), and carries an event per
@@ -138,8 +143,9 @@ backend adds runs rather than reading each as the series' new total.
138
143
  `vx.task.project` and `vx.task.task`: at each task's end
139
144
  `vx.task.duration` (ms, with `vx.cache.source`), and for a task the runner
140
145
  measured `vx.task.cpu_time` (ms) and `vx.task.peak_memory` (bytes), on a
141
- hit `vx.task.time_saved` (ms), and `vx.task.admission_held` (ms) when an
142
- `admit` policy held it; a skipped task sends none. While a task runs, its process tree is sampled
146
+ hit `vx.task.time_saved` (ms), `vx.task.admission_held` (ms) when an
147
+ `admit` policy held it, and `vx.task.queued` (ms) when it waited for a
148
+ worker; a skipped task sends none. While a task runs, its process tree is sampled
143
149
  each second: `vx.task.cpu_usage` (cores busy since the last sample, 1 =
144
150
  one core) and `vx.task.memory` (resident bytes). Sampling runs only while
145
151
  metrics export; a remote task, or a task under 1 s, has no samples.
@@ -155,7 +161,10 @@ secrets, and this sends it to the collector; `logs: false` or
155
161
  (`host.name`, `host.arch`, `os.type`), the commit (`vcs.ref.head.*`) and
156
162
  the repository (`vcs.repository.url.full`, `vcs.repository.name`,
157
163
  `vcs.owner.name`, `vcs.provider.name`, from the `origin` remote) with
158
- `vx.workspace.path`, the workspace root inside it (`.` at the top), under `OTEL_RESOURCE_ATTRIBUTES`, which wins. Each task metric point carries
164
+ `vx.workspace.path`, the workspace root inside it (`.` at the top), and on
165
+ CI the run (`cicd.pipeline.run.url.full`, `cicd.pipeline.name`,
166
+ `vx.ci.job`, `vx.ci.attempt`) and the pull request it builds
167
+ (`vcs.change.id`; GitHub Actions, GitLab, Buildkite, CircleCI), under `OTEL_RESOURCE_ATTRIBUTES`, which wins. Each task metric point carries
159
168
  its task's span as an exemplar, and each log record its span, so a chart
160
169
  opens the trace and a span opens its output.
161
170
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-otel",
3
- "version": "0.0.527",
3
+ "version": "0.0.528",
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.527"
37
+ "@vzn/vx": "^0.0.528"
38
38
  },
39
39
  "repository": {
40
40
  "type": "git",
package/src/otlp.ts CHANGED
@@ -24,6 +24,7 @@ type AnyValue =
24
24
  | { intValue: string }
25
25
  | { boolValue: boolean }
26
26
  | { doubleValue: number }
27
+ | { arrayValue: { values: AnyValue[] } }
27
28
 
28
29
  export interface KeyValue {
29
30
  key: string
@@ -33,6 +34,9 @@ export interface KeyValue {
33
34
  function strAttr(key: string, v: string): KeyValue {
34
35
  return { key, value: { stringValue: v } }
35
36
  }
37
+ function strArrayAttr(key: string, v: readonly string[]): KeyValue {
38
+ return { key, value: { arrayValue: { values: v.map((stringValue) => ({ stringValue })) } } }
39
+ }
36
40
  function intAttr(key: string, v: number): KeyValue {
37
41
  return { key, value: { intValue: String(Math.trunc(v)) } }
38
42
  }
@@ -50,6 +54,9 @@ function int64Attr(key: string, v: string): KeyValue {
50
54
 
51
55
  const SEMCONV = {
52
56
  pipelineRunId: 'cicd.pipeline.run.id',
57
+ pipelineRunUrl: 'cicd.pipeline.run.url.full',
58
+ pipelineName: 'cicd.pipeline.name',
59
+ vcsChangeId: 'vcs.change.id',
53
60
  pipelineResult: 'cicd.pipeline.result',
54
61
  taskName: 'cicd.pipeline.task.name',
55
62
  taskRunResult: 'cicd.pipeline.task.run.result',
@@ -141,6 +148,12 @@ const VX_ATTR = {
141
148
  taskFlakyPasses: 'vx.task.flaky.passes',
142
149
  taskFlakyFailures: 'vx.task.flaky.failures',
143
150
  taskAdmissionHeldMs: 'vx.task.admission_held_ms',
151
+ taskQueuedMs: 'vx.task.queued_ms',
152
+ taskInputFiles: 'vx.task.input_files',
153
+ missChangeCount: 'vx.cache.miss.change_count',
154
+ missChanges: 'vx.cache.miss.changes',
155
+ ciJob: 'vx.ci.job',
156
+ ciAttempt: 'vx.ci.attempt',
144
157
  storedDurationMs: 'vx.cache.stored_duration_ms',
145
158
  storedCpuMs: 'vx.cache.stored_cpu_ms',
146
159
  storedPeakRssBytes: 'vx.cache.stored_peak_rss_bytes',
@@ -299,7 +312,7 @@ export function runResource(run: RunContextRecord): Record<string, string> {
299
312
  [SEMCONV.hostArch]: run.arch === 'x64' ? 'amd64' : run.arch,
300
313
  }
301
314
  if (run.host !== null) r[SEMCONV.hostName] = run.host
302
- for (const [k, v] of repositoryFields(run)) r[k] = v
315
+ for (const [k, v] of originFields(run)) r[k] = v
303
316
  if (run.commitSha !== null) r[SEMCONV.vcsHeadRevision] = run.commitSha
304
317
  if (run.branch !== null) r[SEMCONV.vcsHeadName] = run.branch
305
318
  return r
@@ -313,10 +326,11 @@ const VCS_PROVIDERS: Readonly<Record<string, string>> = {
313
326
  }
314
327
 
315
328
  /**
316
- * The repository (from the normalized origin remote, `host/owner/name`) as
317
- * the VCS conventions name it, and where in it the workspace sits.
329
+ * Where the run came from: the repository (from the normalized origin remote,
330
+ * `host/owner/name`) as the VCS conventions name it, where in it the
331
+ * workspace sits, and the CI run, pull request, workflow and job.
318
332
  */
319
- function repositoryFields(run: RunContextRecord): [key: string, value: string][] {
333
+ function originFields(run: RunContextRecord): [key: string, value: string][] {
320
334
  const fields: [string, string][] = []
321
335
  if (run.repository !== undefined) {
322
336
  const parts = run.repository.split('/')
@@ -329,6 +343,11 @@ function repositoryFields(run: RunContextRecord): [key: string, value: string][]
329
343
  if (provider !== undefined) fields.push([SEMCONV.vcsProviderName, provider])
330
344
  }
331
345
  if (run.workspacePath !== undefined) fields.push([VX_ATTR.workspacePath, run.workspacePath])
346
+ if (run.ciRunUrl !== undefined) fields.push([SEMCONV.pipelineRunUrl, run.ciRunUrl])
347
+ if (run.ciChange !== undefined) fields.push([SEMCONV.vcsChangeId, run.ciChange])
348
+ if (run.ciPipeline !== undefined) fields.push([SEMCONV.pipelineName, run.ciPipeline])
349
+ if (run.ciJob !== undefined) fields.push([VX_ATTR.ciJob, run.ciJob])
350
+ if (run.ciAttempt !== undefined) fields.push([VX_ATTR.ciAttempt, String(run.ciAttempt)])
332
351
  return fields
333
352
  }
334
353
 
@@ -352,7 +371,7 @@ export function runSpanAttributes(run: RunContextRecord, summary?: RunSummaryRec
352
371
  intAttr(VX_ATTR.schema, TELEMETRY_SCHEMA_VERSION),
353
372
  strAttr(VX_ATTR.workspaceId, run.workspaceId),
354
373
  strAttr(VX_ATTR.workspaceName, run.workspaceName),
355
- ...repositoryFields(run).map(([k, v]) => strAttr(k, v)),
374
+ ...originFields(run).map(([k, v]) => strAttr(k, v)),
356
375
  strAttr(VX_ATTR.command, run.command),
357
376
  strAttr(VX_ATTR.requestedTasks, run.requestedTasks.join(',')),
358
377
  strAttr(VX_ATTR.cachePolicy, run.cachePolicy),
@@ -499,6 +518,18 @@ export function taskSpanAttributes(t: TaskTelemetry, run: TaskSpanRunContext): K
499
518
  attrs.push(intAttr(VX_ATTR.storedPeakRssBytes, t.storedPeakRssBytes))
500
519
  if (t.admissionHeldMs !== undefined)
501
520
  attrs.push(intAttr(VX_ATTR.taskAdmissionHeldMs, t.admissionHeldMs))
521
+ if (t.queuedMs !== undefined) attrs.push(intAttr(VX_ATTR.taskQueuedMs, t.queuedMs))
522
+ if (t.inputFiles !== undefined) attrs.push(intAttr(VX_ATTR.taskInputFiles, t.inputFiles))
523
+ if (t.inputChanges !== undefined) {
524
+ // `changed file src/a.ts`: what moved the key since the last saved entry.
525
+ attrs.push(
526
+ intAttr(VX_ATTR.missChangeCount, t.inputChanges.count),
527
+ strArrayAttr(
528
+ VX_ATTR.missChanges,
529
+ t.inputChanges.first.map((c) => `${c.change} ${c.kind} ${c.name}`),
530
+ ),
531
+ )
532
+ }
502
533
  if (t.wallclockStartNs !== undefined)
503
534
  attrs.push(int64Attr(VX_ATTR.wallclockStartNs, t.wallclockStartNs))
504
535
  if (t.wallclockEndNs !== undefined)
@@ -693,6 +724,7 @@ function taskMetrics(points: readonly TaskMetricPoint[]): unknown[] {
693
724
  if (t.peakRssBytes !== undefined) add('vx.task.peak_memory', 'By', at(t.peakRssBytes))
694
725
  if (t.storedDurationMs !== undefined) add('vx.task.time_saved', 'ms', at(t.storedDurationMs))
695
726
  if (t.admissionHeldMs !== undefined) add('vx.task.admission_held', 'ms', at(t.admissionHeldMs))
727
+ if (t.queuedMs !== undefined) add('vx.task.queued', 'ms', at(t.queuedMs))
696
728
  }
697
729
  return [...series].map(([name, { unit, dataPoints }]) => ({ name, unit, gauge: { dataPoints } }))
698
730
  }