@intentius/chant-lexicon-otel 0.106.4 → 0.108.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/README.md +1 -1
- package/dist/collector.d.ts +6 -0
- package/dist/collector.d.ts.map +1 -1
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/genai-pipeline.d.ts +33 -0
- package/dist/composites/genai-pipeline.d.ts.map +1 -0
- package/dist/composites/index.d.ts +8 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/otlp-collector.d.ts +43 -0
- package/dist/composites/otlp-collector.d.ts.map +1 -0
- package/dist/composites/red-metrics.d.ts +77 -0
- package/dist/composites/red-metrics.d.ts.map +1 -0
- package/dist/composites/tail-sampling-tier.d.ts +90 -0
- package/dist/composites/tail-sampling-tier.d.ts.map +1 -0
- package/dist/configmap.d.ts +16 -0
- package/dist/configmap.d.ts.map +1 -1
- package/dist/genai.d.ts +19 -0
- package/dist/genai.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +8 -0
- package/dist/import/embedded.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +3 -3
- package/dist/lint/post-synth/otel-helpers.d.ts +9 -2
- package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/metric-names.d.ts +68 -0
- package/dist/metric-names.d.ts.map +1 -1
- package/dist/op/activities/collector-audit.d.ts +95 -0
- package/dist/op/activities/collector-audit.d.ts.map +1 -0
- package/dist/op/activities/collector-health.d.ts +74 -0
- package/dist/op/activities/collector-health.d.ts.map +1 -0
- package/dist/op/activities/exec.d.ts +19 -0
- package/dist/op/activities/exec.d.ts.map +1 -0
- package/dist/op/activities/github.d.ts +55 -0
- package/dist/op/activities/github.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +20 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/otelcol.d.ts +96 -0
- package/dist/op/activities/otelcol.d.ts.map +1 -0
- package/dist/op/activity-contracts.d.ts +96 -0
- package/dist/op/activity-contracts.d.ts.map +1 -0
- package/dist/op/audit-op.d.ts +35 -0
- package/dist/op/audit-op.d.ts.map +1 -0
- package/dist/op/builders.d.ts +43 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/platform.d.ts +14 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/rules/otel-helpers.ts +18 -5
- package/package.json +14 -3
- package/src/collector.ts +9 -0
- package/src/composites/catalog.test.ts +1 -1
- package/src/composites/catalog.ts +292 -0
- package/src/composites/genai-pipeline.test.ts +53 -0
- package/src/composites/genai-pipeline.ts +40 -0
- package/src/composites/index.ts +8 -0
- package/src/composites/otlp-collector.test.ts +43 -0
- package/src/composites/otlp-collector.ts +70 -0
- package/src/composites/red-metrics.test.ts +131 -0
- package/src/composites/red-metrics.ts +148 -0
- package/src/composites/tail-sampling-tier.test.ts +100 -0
- package/src/composites/tail-sampling-tier.ts +182 -0
- package/src/configmap-operator.test.ts +44 -0
- package/src/configmap.ts +35 -0
- package/src/detect.ts +13 -1
- package/src/genai.ts +72 -59
- package/src/import/embedded.test.ts +36 -0
- package/src/import/embedded.ts +28 -4
- package/src/index.ts +57 -2
- package/src/lint/post-synth/otel-helpers.ts +18 -5
- package/src/metric-names.ts +141 -0
- package/src/op/activities/collector-audit.test.ts +166 -0
- package/src/op/activities/collector-audit.ts +265 -0
- package/src/op/activities/collector-health.test.ts +115 -0
- package/src/op/activities/collector-health.ts +211 -0
- package/src/op/activities/exec.ts +33 -0
- package/src/op/activities/github.ts +127 -0
- package/src/op/activities/index.ts +19 -0
- package/src/op/activities/otelcol.test.ts +162 -0
- package/src/op/activities/otelcol.ts +233 -0
- package/src/op/activity-contracts.test.ts +103 -0
- package/src/op/activity-contracts.ts +92 -0
- package/src/op/audit-op.ts +53 -0
- package/src/op/builders.ts +52 -0
- package/src/op/testdata/otelcol-components-v0.130.0.yaml +82 -0
- package/src/platform.ts +45 -8
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `otelcol validate` and `otelcol components` as Op steps (#3369).
|
|
3
|
+
*
|
|
4
|
+
* - `otelcolValidate` runs the collector binary's own `validate` over a
|
|
5
|
+
* config file. Before it does, it reads the binary's version and refuses
|
|
6
|
+
* when that is not the version chant's config types follow
|
|
7
|
+
* (`COLLECTOR_PIN`): a different collector accepts and rejects different
|
|
8
|
+
* fields, so its verdict is not about the config chant built. A step that
|
|
9
|
+
* names a `version` checks the binary against that version instead.
|
|
10
|
+
* - `otelcolComponents` reads the binary's component list and refuses a
|
|
11
|
+
* config that uses a receiver, processor, exporter, connector or
|
|
12
|
+
* extension the binary was not built with: the failure a custom or core
|
|
13
|
+
* distribution gives at start, found before it starts.
|
|
14
|
+
*
|
|
15
|
+
* The binary is `bin`, else `$OTELCOL_BIN`, else `otelcol-contrib` on PATH.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { resolve } from "node:path";
|
|
20
|
+
import { load } from "js-yaml";
|
|
21
|
+
import { nonRetryableFailure } from "@intentius/chant/op";
|
|
22
|
+
import { COLLECTOR_PIN } from "../../define";
|
|
23
|
+
import { COMPONENT_KINDS, SECTION_OF, canonicalComponentType, parseComponentId, type ComponentKind } from "../../model";
|
|
24
|
+
import { defaultExec, type ExecRunner } from "./exec";
|
|
25
|
+
|
|
26
|
+
export interface OtelcolValidateArgs {
|
|
27
|
+
/** The collector config file, relative to the working directory. */
|
|
28
|
+
config: string;
|
|
29
|
+
/** The collector binary. Default `$OTELCOL_BIN`, else `otelcol-contrib`. */
|
|
30
|
+
bin?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The collector version the binary must be, e.g. `v0.131.0`. Default
|
|
33
|
+
* `COLLECTOR_PIN`'s version: the one chant's config types follow.
|
|
34
|
+
*/
|
|
35
|
+
version?: string;
|
|
36
|
+
/** Replaces the child process. For tests. */
|
|
37
|
+
_exec?: ExecRunner;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface OtelcolValidateResult {
|
|
41
|
+
config: string;
|
|
42
|
+
bin: string;
|
|
43
|
+
/** The binary's version, `v`-prefixed. */
|
|
44
|
+
version: string;
|
|
45
|
+
/** Always true: a config the binary rejects fails the step. */
|
|
46
|
+
ok: boolean;
|
|
47
|
+
output: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface OtelcolComponentsArgs {
|
|
51
|
+
/** The collector config file, relative to the working directory. */
|
|
52
|
+
config: string;
|
|
53
|
+
/** The collector binary. Default `$OTELCOL_BIN`, else `otelcol-contrib`. */
|
|
54
|
+
bin?: string;
|
|
55
|
+
/** Replaces the child process. For tests. */
|
|
56
|
+
_exec?: ExecRunner;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MissingComponent {
|
|
60
|
+
kind: ComponentKind;
|
|
61
|
+
/** The component id as the config writes it, e.g. `spanmetrics/genai`. */
|
|
62
|
+
id: string;
|
|
63
|
+
type: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface OtelcolComponentsResult {
|
|
67
|
+
config: string;
|
|
68
|
+
bin: string;
|
|
69
|
+
/** The binary's version from its build info, when it prints one. */
|
|
70
|
+
version?: string;
|
|
71
|
+
/** Components the config declares, by kind. */
|
|
72
|
+
used: Record<ComponentKind, string[]>;
|
|
73
|
+
/** Always empty: a config that uses a missing component fails the step. */
|
|
74
|
+
missing: MissingComponent[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The binary a step runs. */
|
|
78
|
+
export function otelcolBin(explicit?: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
79
|
+
return explicit ?? env.OTELCOL_BIN ?? "otelcol-contrib";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Normalise `0.130.0`, `v0.130.0` and `v0.130.0-dev` to `v0.130.0` for comparison. */
|
|
83
|
+
function normalizeVersion(v: string): string {
|
|
84
|
+
const m = /v?(\d+\.\d+\.\d+)/.exec(v.trim());
|
|
85
|
+
return m ? `v${m[1]}` : v.trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The version `otelcol --version` prints: `otelcol-contrib version 0.130.0`.
|
|
90
|
+
* Undefined when the output names none.
|
|
91
|
+
*/
|
|
92
|
+
export function parseOtelcolVersion(output: string): string | undefined {
|
|
93
|
+
const m = /version\s+v?(\d+\.\d+\.\d+[^\s]*)/i.exec(output);
|
|
94
|
+
return m ? `v${m[1]}` : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readConfig(path: string, cwd: string): Record<string, unknown> {
|
|
98
|
+
const text = readFileSync(resolve(cwd, path), "utf8");
|
|
99
|
+
const value = load(text);
|
|
100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
101
|
+
throw nonRetryableFailure(`${path} is not a collector config: it does not hold a YAML mapping`, "OtelcolConfigInvalid");
|
|
102
|
+
}
|
|
103
|
+
return value as Record<string, unknown>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function binaryVersion(exec: ExecRunner, bin: string): Promise<string> {
|
|
107
|
+
const r = await exec(bin, ["--version"], { timeoutMs: 30_000 });
|
|
108
|
+
if (r.code === null) throw new Error(`could not run ${bin}: ${r.error ?? "not found"}. Install the collector or set bin or $OTELCOL_BIN.`);
|
|
109
|
+
const version = parseOtelcolVersion(`${r.stdout}\n${r.stderr}`);
|
|
110
|
+
if (r.code !== 0 || !version) throw new Error(`${bin} --version printed no version (exit ${r.code}): ${(r.stdout + r.stderr).trim().slice(0, 200)}`);
|
|
111
|
+
return version;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Run `otelcol validate` over `config`, after checking the binary is the pinned (or the given) version. */
|
|
115
|
+
export async function otelcolValidate(args: OtelcolValidateArgs): Promise<OtelcolValidateResult> {
|
|
116
|
+
const exec = args._exec ?? defaultExec;
|
|
117
|
+
const bin = otelcolBin(args.bin);
|
|
118
|
+
const want = normalizeVersion(args.version ?? COLLECTOR_PIN.version);
|
|
119
|
+
const have = await binaryVersion(exec, bin);
|
|
120
|
+
if (normalizeVersion(have) !== want) {
|
|
121
|
+
const why = args.version
|
|
122
|
+
? `the step asks for ${want}`
|
|
123
|
+
: `chant's collector config types follow ${COLLECTOR_PIN.version} (COLLECTOR_PIN); pass version to validate with another collector`;
|
|
124
|
+
throw nonRetryableFailure(`${bin} is ${have}, and ${why}`, "OtelcolVersionMismatch");
|
|
125
|
+
}
|
|
126
|
+
// Fail on a missing or unreadable file here, with its name, rather than through the binary's message.
|
|
127
|
+
readConfig(args.config, process.cwd());
|
|
128
|
+
const r = await exec(bin, ["validate", `--config=${resolve(args.config)}`], { timeoutMs: 120_000 });
|
|
129
|
+
const output = `${r.stdout}${r.stderr}`.trim();
|
|
130
|
+
if (r.code !== 0) {
|
|
131
|
+
throw nonRetryableFailure(`${bin} validate rejected ${args.config} (exit ${r.code ?? "none"}):\n${output}`, "OtelcolConfigInvalid");
|
|
132
|
+
}
|
|
133
|
+
return { config: args.config, bin, version: have, ok: true, output };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const KIND_OF_SECTION: Record<string, ComponentKind> = {
|
|
137
|
+
receivers: "receiver",
|
|
138
|
+
processors: "processor",
|
|
139
|
+
exporters: "exporter",
|
|
140
|
+
connectors: "connector",
|
|
141
|
+
extensions: "extension",
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** What `otelcol components` lists, by kind: component type names. */
|
|
145
|
+
export interface OtelcolComponentList {
|
|
146
|
+
version?: string;
|
|
147
|
+
components: Record<ComponentKind, Set<string>>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Read `otelcol components` output. The format is YAML and marked unstable
|
|
152
|
+
* upstream, so this reads what it can: each section may be a list of
|
|
153
|
+
* objects with a `name` (v0.130.0), a list of plain names (older builds),
|
|
154
|
+
* or a mapping keyed by name. Unknown top-level keys are ignored.
|
|
155
|
+
*/
|
|
156
|
+
export function parseComponentsOutput(text: string): OtelcolComponentList {
|
|
157
|
+
let doc: unknown;
|
|
158
|
+
try {
|
|
159
|
+
doc = load(text);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
throw new Error(`otelcol components printed output that is not YAML: ${(err as Error).message}`);
|
|
162
|
+
}
|
|
163
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw new Error("otelcol components printed no YAML mapping");
|
|
164
|
+
const d = doc as Record<string, unknown>;
|
|
165
|
+
const components = Object.fromEntries(COMPONENT_KINDS.map((k) => [k, new Set<string>()])) as Record<ComponentKind, Set<string>>;
|
|
166
|
+
let found = false;
|
|
167
|
+
for (const [section, kind] of Object.entries(KIND_OF_SECTION)) {
|
|
168
|
+
const value = d[section];
|
|
169
|
+
if (value === undefined || value === null) continue;
|
|
170
|
+
found = true;
|
|
171
|
+
const names: unknown[] = Array.isArray(value)
|
|
172
|
+
? value.map((e) => (e && typeof e === "object" ? ((e as Record<string, unknown>).name ?? (e as Record<string, unknown>).type) : e))
|
|
173
|
+
: typeof value === "object"
|
|
174
|
+
? Object.keys(value as Record<string, unknown>)
|
|
175
|
+
: [];
|
|
176
|
+
for (const n of names) if (typeof n === "string" && n !== "") components[kind].add(n);
|
|
177
|
+
}
|
|
178
|
+
if (!found) throw new Error("otelcol components listed no receivers, processors, exporters, connectors or extensions");
|
|
179
|
+
const build = d.buildinfo as Record<string, unknown> | undefined;
|
|
180
|
+
const v = build && (typeof build.version === "string" || typeof build.version === "number") ? String(build.version) : undefined;
|
|
181
|
+
return { ...(v ? { version: normalizeVersion(v) } : {}), components };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The component ids a config declares, by kind. */
|
|
185
|
+
export function configComponents(config: Record<string, unknown>): Record<ComponentKind, string[]> {
|
|
186
|
+
const out = Object.fromEntries(COMPONENT_KINDS.map((k) => [k, [] as string[]])) as Record<ComponentKind, string[]>;
|
|
187
|
+
for (const kind of COMPONENT_KINDS) {
|
|
188
|
+
const section = config[SECTION_OF[kind]];
|
|
189
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) continue;
|
|
190
|
+
out[kind] = Object.keys(section as Record<string, unknown>);
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The components `used` names that `list` lacks. A renamed built-in counts
|
|
197
|
+
* under either name: a binary that lists `otlp_grpc` runs a config that says
|
|
198
|
+
* `otlp`, and the other way round.
|
|
199
|
+
*/
|
|
200
|
+
export function missingComponents(used: Record<ComponentKind, string[]>, list: OtelcolComponentList): MissingComponent[] {
|
|
201
|
+
const missing: MissingComponent[] = [];
|
|
202
|
+
for (const kind of COMPONENT_KINDS) {
|
|
203
|
+
const have = list.components[kind];
|
|
204
|
+
const canonicalHave = new Set([...have].map((t) => canonicalComponentType(kind, t)));
|
|
205
|
+
for (const id of used[kind]) {
|
|
206
|
+
const type = parseComponentId(id)?.type ?? id;
|
|
207
|
+
if (have.has(type) || canonicalHave.has(canonicalComponentType(kind, type))) continue;
|
|
208
|
+
missing.push({ kind, id, type });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return missing;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Check `config` uses only components the binary was built with, from `otelcol components`. */
|
|
215
|
+
export async function otelcolComponents(args: OtelcolComponentsArgs): Promise<OtelcolComponentsResult> {
|
|
216
|
+
const exec = args._exec ?? defaultExec;
|
|
217
|
+
const bin = otelcolBin(args.bin);
|
|
218
|
+
const config = readConfig(args.config, process.cwd());
|
|
219
|
+
const r = await exec(bin, ["components"], { timeoutMs: 60_000 });
|
|
220
|
+
if (r.code === null) throw new Error(`could not run ${bin}: ${r.error ?? "not found"}. Install the collector or set bin or $OTELCOL_BIN.`);
|
|
221
|
+
if (r.code !== 0) throw new Error(`${bin} components exited ${r.code}: ${(r.stdout + r.stderr).trim().slice(0, 400)}`);
|
|
222
|
+
const list = parseComponentsOutput(r.stdout);
|
|
223
|
+
const used = configComponents(config);
|
|
224
|
+
const missing = missingComponents(used, list);
|
|
225
|
+
if (missing.length > 0) {
|
|
226
|
+
const lines = missing.map((m) => ` ${m.kind} ${m.id}`).join("\n");
|
|
227
|
+
throw nonRetryableFailure(
|
|
228
|
+
`${args.config} uses ${missing.length} component(s) ${bin}${list.version ? ` ${list.version}` : ""} was not built with:\n${lines}`,
|
|
229
|
+
"OtelcolComponentMissing",
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return { config: args.config, bin, ...(list.version ? { version: list.version } : {}), used, missing };
|
|
233
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ConvergeOp,
|
|
4
|
+
eq,
|
|
5
|
+
isActivityContract,
|
|
6
|
+
loadActivities,
|
|
7
|
+
loadActivityContracts,
|
|
8
|
+
report,
|
|
9
|
+
stepOutput,
|
|
10
|
+
validateActivitySteps,
|
|
11
|
+
validateStepOutputRefs,
|
|
12
|
+
when,
|
|
13
|
+
type ActivityContract,
|
|
14
|
+
type ResourceSymptom,
|
|
15
|
+
type StepOutputRef,
|
|
16
|
+
} from "@intentius/chant/op";
|
|
17
|
+
import * as contracts from "./activity-contracts";
|
|
18
|
+
import * as activities from "./activities";
|
|
19
|
+
import { collectorAudit, collectorHealthObserve, otelcolComponents, otelcolValidate } from "./builders";
|
|
20
|
+
import { CollectorAuditOp } from "./audit-op";
|
|
21
|
+
|
|
22
|
+
const CONTRACTS: Map<string, ActivityContract> = new Map(
|
|
23
|
+
Object.values(contracts).filter(isActivityContract).map((c) => [c.name, c]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const op = (steps: unknown[]) => ({ name: "check", phases: [{ name: "Check", steps: steps as never[] }] });
|
|
27
|
+
|
|
28
|
+
describe("otel activity contracts (#3369)", () => {
|
|
29
|
+
test("every exported activity has a contract with a return schema, and nothing else is exported", () => {
|
|
30
|
+
for (const [key, value] of Object.entries(contracts)) {
|
|
31
|
+
expect(isActivityContract(value), `${key} is not an ActivityContract`).toBe(true);
|
|
32
|
+
expect((value as ActivityContract).returns).toBeDefined();
|
|
33
|
+
}
|
|
34
|
+
const fns = Object.entries(activities).filter(([, v]) => typeof v === "function").map(([k]) => k).sort();
|
|
35
|
+
expect([...CONTRACTS.keys()].sort()).toEqual(fns);
|
|
36
|
+
expect(fns).toEqual(["collectorAudit", "collectorHealthObserve", "otelcolComponents", "otelcolValidate"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("the builders' steps validate, and a misspelled key is an error", () => {
|
|
40
|
+
const steps = [
|
|
41
|
+
otelcolValidate({ config: "dist/collector.yaml" }),
|
|
42
|
+
otelcolValidate({ config: "dist/collector.yaml", version: "v0.131.0", bin: "/opt/otelcol-contrib" }),
|
|
43
|
+
otelcolComponents({ config: "dist/collector.yaml" }),
|
|
44
|
+
collectorAudit({ mode: "pull-request" }),
|
|
45
|
+
];
|
|
46
|
+
expect(validateActivitySteps(op(steps), CONTRACTS)).toEqual([]);
|
|
47
|
+
const issues = validateActivitySteps(op([{ kind: "activity", fn: "otelcolValidate", args: { conifg: "x.yaml" } }]), CONTRACTS);
|
|
48
|
+
expect(issues.some((i) => i.message.includes("conifg"))).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("ConvergeOp({ observe: collectorHealthObserve }) passes OPS012 and OPS013", () => {
|
|
52
|
+
const { op: converge } = ConvergeOp({
|
|
53
|
+
name: "collector-health",
|
|
54
|
+
env: "local",
|
|
55
|
+
schedule: "*/5 * * * *",
|
|
56
|
+
observe: collectorHealthObserve({ collectors: [{ name: "gateway", config: "dist/gateway.yaml", host: "127.0.0.1" }] }),
|
|
57
|
+
rules: [when<ResourceSymptom>(eq("status", "drifted"), report("collector unhealthy"), { id: "collector-down", why: "say so" })],
|
|
58
|
+
});
|
|
59
|
+
expect(validateActivitySteps(converge.props as never, CONTRACTS)).toEqual([]);
|
|
60
|
+
expect(validateStepOutputRefs(converge.props as never, CONTRACTS)).toEqual([]);
|
|
61
|
+
// Without the contracts the observer's output has no schema (OPS013).
|
|
62
|
+
expect(validateStepOutputRefs(converge.props as never, new Map()).length).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("CollectorAuditOp is one collectorAudit step, its findings the run's outcome", () => {
|
|
66
|
+
const { op: audit } = CollectorAuditOp({ name: "collector-audit", schedule: "0 6 * * 1", onFinding: "pull-request" });
|
|
67
|
+
const props = audit.props as unknown as { schedule?: unknown; phases: Array<{ steps: Array<{ fn: string; args: Record<string, unknown>; outcomeAttribute: unknown }> }> };
|
|
68
|
+
expect(props.schedule).toEqual({ cron: "0 6 * * 1", overlap: "skip" });
|
|
69
|
+
expect(props.phases[0].steps[0]).toMatchObject({ fn: "collectorAudit", args: { mode: "pull-request" }, outcomeAttribute: { name: "Findings", from: "findings" } });
|
|
70
|
+
expect(validateActivitySteps(props as never, CONTRACTS)).toEqual([]);
|
|
71
|
+
expect(CollectorAuditOp({ name: "a" }).op.props).not.toHaveProperty("schedule");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("builders route profile and id to the step, and take step-output refs", () => {
|
|
75
|
+
const s = otelcolValidate({ config: "x.yaml", id: "validate", profile: "longInfra" });
|
|
76
|
+
expect(s).toMatchObject({ fn: "otelcolValidate", args: { config: "x.yaml" }, id: "validate", profile: "longInfra" });
|
|
77
|
+
const ref: StepOutputRef = s.out.version;
|
|
78
|
+
expect(ref.step).toBe("validate");
|
|
79
|
+
const t = otelcolComponents({ config: stepOutput("render", "path") });
|
|
80
|
+
expect(t.args?.config).toMatchObject({ step: "render" });
|
|
81
|
+
expect(collectorAudit().profile).toBe("fastIdempotent");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("loadActivities and loadActivityContracts find them by the otel lexicon's name", async () => {
|
|
85
|
+
const acts = await loadActivities(["otel"]);
|
|
86
|
+
for (const fn of ["otelcolValidate", "otelcolComponents", "collectorHealthObserve", "collectorAudit"]) expect(acts.has(fn), fn).toBe(true);
|
|
87
|
+
const loaded = await loadActivityContracts(["otel"]);
|
|
88
|
+
expect(loaded.get("collectorHealthObserve")?.returns).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── Compile-time only ────────────────────────────────────────────────
|
|
93
|
+
function _typeChecksOnly(): void {
|
|
94
|
+
// @ts-expect-error: config is required.
|
|
95
|
+
otelcolValidate({});
|
|
96
|
+
// @ts-expect-error: a collector needs a name.
|
|
97
|
+
collectorHealthObserve({ collectors: [{ config: "x.yaml" }] });
|
|
98
|
+
// @ts-expect-error: the test seam is not an authoring option.
|
|
99
|
+
otelcolComponents({ config: "x.yaml", _exec: async () => ({ code: 0, stdout: "", stderr: "" }) });
|
|
100
|
+
// @ts-expect-error: mode is report, issue or pull-request.
|
|
101
|
+
collectorAudit({ mode: "merge-request" });
|
|
102
|
+
}
|
|
103
|
+
void _typeChecksOnly;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity contracts for this lexicon's `op/activities` (#3369).
|
|
3
|
+
*
|
|
4
|
+
* Resolved by convention at `@intentius/chant-lexicon-otel/op/activity-contracts`:
|
|
5
|
+
* `loadActivityContracts` merges them into the map OPS012 checks each step's
|
|
6
|
+
* args against and OPS013 checks each step-output reference against. Every
|
|
7
|
+
* activity here has one. `collectorHealthObserve` needs its return schema
|
|
8
|
+
* most: it is the observer of a `ConvergeOp({ observe })`, whose Converge
|
|
9
|
+
* step reads the observer's whole output.
|
|
10
|
+
*
|
|
11
|
+
* Each `args` schema mirrors the `*Args` interface in `./activities/`,
|
|
12
|
+
* without the `_`-prefixed test seams, and is a `z.strictObject`, so a
|
|
13
|
+
* misspelled key fails the build.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { activityContract } from "@intentius/chant/op";
|
|
18
|
+
|
|
19
|
+
const componentKind = z.enum(["receiver", "processor", "exporter", "connector", "extension"]);
|
|
20
|
+
const byKind = z.object({
|
|
21
|
+
receiver: z.array(z.string()),
|
|
22
|
+
processor: z.array(z.string()),
|
|
23
|
+
exporter: z.array(z.string()),
|
|
24
|
+
connector: z.array(z.string()),
|
|
25
|
+
extension: z.array(z.string()),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const otelcolValidateContract = activityContract(
|
|
29
|
+
"otelcolValidate",
|
|
30
|
+
z.strictObject({ config: z.string(), bin: z.string().optional(), version: z.string().optional() }),
|
|
31
|
+
z.object({ config: z.string(), bin: z.string(), version: z.string(), ok: z.boolean(), output: z.string() }),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export const otelcolComponentsContract = activityContract(
|
|
35
|
+
"otelcolComponents",
|
|
36
|
+
z.strictObject({ config: z.string(), bin: z.string().optional() }),
|
|
37
|
+
z.object({
|
|
38
|
+
config: z.string(),
|
|
39
|
+
bin: z.string(),
|
|
40
|
+
version: z.string().optional(),
|
|
41
|
+
used: byKind,
|
|
42
|
+
missing: z.array(z.object({ kind: componentKind, id: z.string(), type: z.string() })),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const endpointOverrides = z.strictObject({ healthCheck: z.string().optional(), zpages: z.string().optional(), telemetry: z.string().optional() });
|
|
47
|
+
|
|
48
|
+
export const collectorHealthObserveContract = activityContract(
|
|
49
|
+
"collectorHealthObserve",
|
|
50
|
+
z.strictObject({
|
|
51
|
+
collectors: z.array(
|
|
52
|
+
z.strictObject({
|
|
53
|
+
name: z.string(),
|
|
54
|
+
config: z.string().optional(),
|
|
55
|
+
configObject: z.record(z.string(), z.unknown()).optional(),
|
|
56
|
+
host: z.string().optional(),
|
|
57
|
+
endpoints: endpointOverrides.optional(),
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
timeoutMs: z.number().optional(),
|
|
61
|
+
probes: z.number().optional(),
|
|
62
|
+
probeIntervalMs: z.number().optional(),
|
|
63
|
+
}),
|
|
64
|
+
z.object({
|
|
65
|
+
resources: z.array(z.object({ name: z.string(), status: z.enum(["in-sync", "drifted", "unknown"]), detail: z.string() })),
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const pinState = z.object({ pin: z.string(), latest: z.string().nullable(), behind: z.number() });
|
|
70
|
+
|
|
71
|
+
export const collectorAuditContract = activityContract(
|
|
72
|
+
"collectorAudit",
|
|
73
|
+
z.strictObject({
|
|
74
|
+
mode: z.enum(["report", "issue", "pull-request"]).optional(),
|
|
75
|
+
lexiconDir: z.string().optional(),
|
|
76
|
+
stability: z.boolean().optional(),
|
|
77
|
+
stabilityBudget: z.number().optional(),
|
|
78
|
+
branch: z.string().optional(),
|
|
79
|
+
}),
|
|
80
|
+
z.object({
|
|
81
|
+
mode: z.enum(["report", "issue", "pull-request"]),
|
|
82
|
+
collector: pinState,
|
|
83
|
+
semconv: pinState,
|
|
84
|
+
findings: z.array(
|
|
85
|
+
z.object({ kind: z.enum(["collector-pin-behind", "semconv-pin-behind", "stability-changed", "deprecated"]), subject: z.string(), detail: z.string() }),
|
|
86
|
+
),
|
|
87
|
+
unchecked: z.array(z.string()),
|
|
88
|
+
summary: z.string(),
|
|
89
|
+
issueUrl: z.string().optional(),
|
|
90
|
+
prUrl: z.string().optional(),
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CollectorAuditOp`: the otel lexicon's pins against upstream, as an Op
|
|
3
|
+
* with an optional schedule (#3369). It sits at observe, like
|
|
4
|
+
* `WorkflowAuditOp`: one `collectorAudit` step reads the contrib release
|
|
5
|
+
* list against `COLLECTOR_PIN`, each built-in component's stability from its
|
|
6
|
+
* `metadata.yaml`, and `GENAI_SEMCONV_PIN` against the semantic-conventions
|
|
7
|
+
* releases. `onFinding` says what happens with findings: `report` (the
|
|
8
|
+
* default), `issue`, or `pull-request`, which bumps the pins on a proposal
|
|
9
|
+
* branch and opens a pull request. Review and merge of that pull request
|
|
10
|
+
* are the approval.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* export const { op } = CollectorAuditOp({ name: "collector-audit", schedule: "0 6 * * 1", onFinding: "pull-request" });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Op, phase, type OpResource } from "@intentius/chant/op";
|
|
19
|
+
import { collectorAudit } from "./builders";
|
|
20
|
+
import type { CollectorAuditMode } from "./activities/collector-audit";
|
|
21
|
+
|
|
22
|
+
export interface CollectorAuditOpConfig {
|
|
23
|
+
/** Op name (kebab-case). */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Cron expression; omit for one-shot `chant run`. */
|
|
26
|
+
schedule?: string;
|
|
27
|
+
/** @default "report" */
|
|
28
|
+
onFinding?: CollectorAuditMode;
|
|
29
|
+
/** The otel lexicon's directory a pull request edits. Default `lexicons/otel`. */
|
|
30
|
+
lexiconDir?: string;
|
|
31
|
+
/** At most this many `metadata.yaml` reads. Default 200. */
|
|
32
|
+
stabilityBudget?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CollectorAuditOpResources {
|
|
36
|
+
op: InstanceType<typeof OpResource>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function CollectorAuditOp(config: CollectorAuditOpConfig): CollectorAuditOpResources {
|
|
40
|
+
const step = collectorAudit({
|
|
41
|
+
mode: config.onFinding ?? "report",
|
|
42
|
+
...(config.lexiconDir ? { lexiconDir: config.lexiconDir } : {}),
|
|
43
|
+
...(config.stabilityBudget !== undefined ? { stabilityBudget: config.stabilityBudget } : {}),
|
|
44
|
+
});
|
|
45
|
+
const op = Op({
|
|
46
|
+
name: config.name,
|
|
47
|
+
overview: "Audit the otel lexicon's collector and semantic-convention pins and its components' stability against upstream releases",
|
|
48
|
+
labels: { Audit: "true", Surface: "otel-collector" },
|
|
49
|
+
...(config.schedule ? { schedule: { cron: config.schedule, overlap: "skip" as const } } : {}),
|
|
50
|
+
phases: [phase("Audit", [{ ...step, outcomeAttribute: { name: "Findings", from: "findings" } }])],
|
|
51
|
+
});
|
|
52
|
+
return { op };
|
|
53
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step builders for this lexicon's Op activities (#3369), following
|
|
3
|
+
* the helm and fly builders: each one's options ARE the activity's own
|
|
4
|
+
* `*Args` interface (minus its `_`-prefixed test seams), so the builder and
|
|
5
|
+
* the implementation cannot drift, and every field also takes a
|
|
6
|
+
* `StepOutputRef`. `profile` and `id` go to the step, never into args, so
|
|
7
|
+
* `.out` works once an id is given.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { activity, takeProfileAndId, type ActivityStep, type NamedActivityStep, type WithStepRefs } from "@intentius/chant/op";
|
|
11
|
+
import type { OtelcolValidateArgs, OtelcolComponentsArgs } from "./activities/otelcol";
|
|
12
|
+
import type { CollectorHealthObserveArgs } from "./activities/collector-health";
|
|
13
|
+
import type { CollectorAuditArgs } from "./activities/collector-audit";
|
|
14
|
+
|
|
15
|
+
type StepOpts = { profile?: ActivityStep["profile"]; id?: string };
|
|
16
|
+
/** An activity's args as an author writes them: without the test seams. */
|
|
17
|
+
type Authored<T> = Omit<T, `_${string}`>;
|
|
18
|
+
|
|
19
|
+
function step<Args>(fn: string, defaultProfile: NonNullable<ActivityStep["profile"]>) {
|
|
20
|
+
return (args: WithStepRefs<Authored<Args>> & StepOpts): NamedActivityStep => {
|
|
21
|
+
const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
|
|
22
|
+
return activity(fn, rest, { profile: profile ?? defaultProfile, ...(id ? { id } : {}) });
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run `otelcol validate` over a collector config file. Refuses when the
|
|
28
|
+
* binary is not `COLLECTOR_PIN`'s version, unless `version` names the one it
|
|
29
|
+
* must be. Defaults to the `fastIdempotent` profile.
|
|
30
|
+
*/
|
|
31
|
+
export const otelcolValidate = step<OtelcolValidateArgs>("otelcolValidate", "fastIdempotent");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check a collector config uses only components the binary was built with,
|
|
35
|
+
* from `otelcol components`. Defaults to the `fastIdempotent` profile.
|
|
36
|
+
*/
|
|
37
|
+
export const otelcolComponents = step<OtelcolComponentsArgs>("otelcolComponents", "fastIdempotent");
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Observe running collectors for a `ConvergeOp({ observe })`: one resource
|
|
41
|
+
* per collector, from the health_check, zpages and internal-telemetry
|
|
42
|
+
* endpoints its config declares. No `health_check` in `service.extensions`
|
|
43
|
+
* is `unknown`. Defaults to the `fastIdempotent` profile.
|
|
44
|
+
*/
|
|
45
|
+
export const collectorHealthObserve = step<CollectorHealthObserveArgs>("collectorHealthObserve", "fastIdempotent");
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Audit the lexicon's pins against upstream releases (the step of
|
|
49
|
+
* `CollectorAuditOp`). Defaults to the `fastIdempotent` profile.
|
|
50
|
+
*/
|
|
51
|
+
export const collectorAudit = (args: WithStepRefs<Authored<CollectorAuditArgs>> & StepOpts = {}): NamedActivityStep =>
|
|
52
|
+
step<CollectorAuditArgs>("collectorAudit", "fastIdempotent")(args);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# The shape `otelcol-contrib components` prints at v0.130.0, cut down to a
|
|
2
|
+
# few entries per section. The format is marked unstable upstream; the
|
|
3
|
+
# components parser is pinned to it by src/op/activities/otelcol.test.ts.
|
|
4
|
+
buildinfo:
|
|
5
|
+
command: otelcol-contrib
|
|
6
|
+
description: OpenTelemetry Collector Contrib
|
|
7
|
+
version: 0.130.0
|
|
8
|
+
receivers:
|
|
9
|
+
- name: otlp
|
|
10
|
+
module: go.opentelemetry.io/collector/receiver/otlpreceiver v0.130.0
|
|
11
|
+
stability:
|
|
12
|
+
logs: Stable
|
|
13
|
+
metrics: Stable
|
|
14
|
+
traces: Stable
|
|
15
|
+
- name: k8s_cluster
|
|
16
|
+
module: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver v0.130.0
|
|
17
|
+
stability:
|
|
18
|
+
logs: Development
|
|
19
|
+
metrics: Beta
|
|
20
|
+
- name: prometheus
|
|
21
|
+
module: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.130.0
|
|
22
|
+
stability:
|
|
23
|
+
metrics: Beta
|
|
24
|
+
processors:
|
|
25
|
+
- name: batch
|
|
26
|
+
module: go.opentelemetry.io/collector/processor/batchprocessor v0.130.0
|
|
27
|
+
stability:
|
|
28
|
+
logs: Beta
|
|
29
|
+
metrics: Beta
|
|
30
|
+
traces: Beta
|
|
31
|
+
- name: memory_limiter
|
|
32
|
+
module: go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.130.0
|
|
33
|
+
stability:
|
|
34
|
+
logs: Beta
|
|
35
|
+
metrics: Beta
|
|
36
|
+
traces: Beta
|
|
37
|
+
- name: tail_sampling
|
|
38
|
+
module: github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor v0.130.0
|
|
39
|
+
stability:
|
|
40
|
+
traces: Beta
|
|
41
|
+
exporters:
|
|
42
|
+
- name: debug
|
|
43
|
+
module: go.opentelemetry.io/collector/exporter/debugexporter v0.130.0
|
|
44
|
+
stability:
|
|
45
|
+
logs: Development
|
|
46
|
+
metrics: Development
|
|
47
|
+
traces: Development
|
|
48
|
+
- name: otlp
|
|
49
|
+
module: go.opentelemetry.io/collector/exporter/otlpexporter v0.130.0
|
|
50
|
+
stability:
|
|
51
|
+
logs: Stable
|
|
52
|
+
metrics: Stable
|
|
53
|
+
traces: Stable
|
|
54
|
+
- name: prometheus
|
|
55
|
+
module: github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter v0.130.0
|
|
56
|
+
stability:
|
|
57
|
+
metrics: Beta
|
|
58
|
+
connectors:
|
|
59
|
+
- name: forward
|
|
60
|
+
module: go.opentelemetry.io/collector/connector/forwardconnector v0.130.0
|
|
61
|
+
stability:
|
|
62
|
+
logs-to-logs: Beta
|
|
63
|
+
metrics-to-metrics: Beta
|
|
64
|
+
traces-to-traces: Beta
|
|
65
|
+
- name: spanmetrics
|
|
66
|
+
module: github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.130.0
|
|
67
|
+
stability:
|
|
68
|
+
traces-to-metrics: Alpha
|
|
69
|
+
extensions:
|
|
70
|
+
- name: health_check
|
|
71
|
+
module: github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckextension v0.130.0
|
|
72
|
+
stability:
|
|
73
|
+
extension: Beta
|
|
74
|
+
- name: zpages
|
|
75
|
+
module: go.opentelemetry.io/collector/extension/zpagesextension v0.130.0
|
|
76
|
+
stability:
|
|
77
|
+
extension: Beta
|
|
78
|
+
providers:
|
|
79
|
+
- scheme: env
|
|
80
|
+
module: go.opentelemetry.io/collector/confmap/provider/envprovider v1.36.0
|
|
81
|
+
- scheme: file
|
|
82
|
+
module: go.opentelemetry.io/collector/confmap/provider/fileprovider v1.36.0
|