@intentius/chant-lexicon-otel 0.124.31 → 0.124.32
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/dist/attribution.js +391 -0
- package/dist/catalog.js +42 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +101 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate.js +43 -0
- package/dist/codegen/package.js +28 -0
- package/dist/collector.js +150 -0
- package/dist/components/common.js +50 -0
- package/dist/components/connectors.js +242 -0
- package/dist/components/exporters.js +57 -0
- package/dist/components/extensions.js +58 -0
- package/dist/components/filtering.js +106 -0
- package/dist/components/index.js +9 -0
- package/dist/components/k8s-receivers.js +36 -0
- package/dist/components/processors.js +103 -0
- package/dist/components/receivers.js +61 -0
- package/dist/components/sampling.js +257 -0
- package/dist/composites/catalog.js +368 -0
- package/dist/composites/genai-pipeline.js +28 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/node-agent.js +156 -0
- package/dist/composites/otlp-collector.js +57 -0
- package/dist/composites/red-metrics.js +95 -0
- package/dist/composites/tail-sampling-tier.js +132 -0
- package/dist/config-hygiene.js +464 -0
- package/dist/configmap.js +86 -0
- package/dist/define.js +204 -0
- package/dist/detect.js +22 -0
- package/dist/genai.js +569 -0
- package/dist/import/embedded.js +57 -0
- package/dist/import/generator.js +459 -0
- package/dist/import/parser.js +199 -0
- package/dist/index.js +34 -0
- package/dist/init-templates.js +119 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +49 -0
- package/dist/lint/post-synth/index.js +52 -0
- package/dist/lint/post-synth/otel-helpers.js +103 -0
- package/dist/lint/post-synth/otel101.js +13 -0
- package/dist/lint/post-synth/otel102.js +13 -0
- package/dist/lint/post-synth/otel103.js +13 -0
- package/dist/lint/post-synth/otel104.js +13 -0
- package/dist/lint/post-synth/otel105.js +13 -0
- package/dist/lint/post-synth/otel106.js +13 -0
- package/dist/lint/post-synth/otel107.js +13 -0
- package/dist/lint/post-synth/otel108.js +13 -0
- package/dist/lint/post-synth/otel109.js +13 -0
- package/dist/lint/post-synth/otel112.js +13 -0
- package/dist/lint/post-synth/otel113.js +13 -0
- package/dist/lint/post-synth/otel114.js +13 -0
- package/dist/lint/post-synth/otel115.js +13 -0
- package/dist/lint/post-synth/otel116.js +13 -0
- package/dist/lint/post-synth/otel117.js +13 -0
- package/dist/lint/post-synth/otel118.js +13 -0
- package/dist/lint/post-synth/otel119.js +13 -0
- package/dist/lint/post-synth/otel120.js +13 -0
- package/dist/lint/post-synth/otel121.js +13 -0
- package/dist/lint/post-synth/otel122.js +13 -0
- package/dist/lint/post-synth/otel123.js +13 -0
- package/dist/lint/post-synth/otel124.js +13 -0
- package/dist/lint/post-synth/otel125.js +13 -0
- package/dist/lint/post-synth/otel126.js +13 -0
- package/dist/lint/post-synth/otel127.js +13 -0
- package/dist/lint/rules/component-id-syntax.js +66 -0
- package/dist/lint/rules/index.js +6 -0
- package/dist/lint/rules/literal-credential.js +91 -0
- package/dist/lint/rules/otel-ast.js +30 -0
- package/dist/lsp/completions.js +11 -0
- package/dist/lsp/hover.js +23 -0
- package/dist/manifest.json +1 -1
- package/dist/metric-names.js +244 -0
- package/dist/model.js +90 -0
- package/dist/op/activities/collector-audit.js +213 -0
- package/dist/op/activities/collector-health.js +176 -0
- package/dist/op/activities/exec.js +16 -0
- package/dist/op/activities/github.js +110 -0
- package/dist/op/activities/index.js +16 -0
- package/dist/op/activities/otelcol.js +173 -0
- package/dist/op/activity-contracts.js +64 -0
- package/dist/op/audit-op.js +33 -0
- package/dist/op/builders.js +38 -0
- package/dist/package-cli.js +14 -0
- package/dist/pipeline.js +31 -0
- package/dist/platform.js +144 -0
- package/dist/plugin.js +126 -0
- package/dist/semconv.js +50 -0
- package/dist/serializer.js +26 -0
- package/dist/skill-defs.js +34 -0
- package/dist/topology.js +104 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate-config.js +706 -0
- package/dist/validate.js +85 -0
- package/dist/yaml.js +157 -0
- package/package.json +8 -8
- package/src/codegen/package.ts +2 -2
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { resolve } from "node:path";
|
|
19
|
+
import { load } from "js-yaml";
|
|
20
|
+
import { nonRetryableFailure } from "@intentius/chant/op";
|
|
21
|
+
import { COLLECTOR_PIN } from "../../define.js";
|
|
22
|
+
import { COMPONENT_KINDS, SECTION_OF, canonicalComponentType, parseComponentId } from "../../model.js";
|
|
23
|
+
import { defaultExec } from "./exec.js";
|
|
24
|
+
/** The binary a step runs. */
|
|
25
|
+
export function otelcolBin(explicit, env = process.env) {
|
|
26
|
+
return explicit ?? env.OTELCOL_BIN ?? "otelcol-contrib";
|
|
27
|
+
}
|
|
28
|
+
/** Normalise `0.130.0`, `v0.130.0` and `v0.130.0-dev` to `v0.130.0` for comparison. */
|
|
29
|
+
function normalizeVersion(v) {
|
|
30
|
+
const m = /v?(\d+\.\d+\.\d+)/.exec(v.trim());
|
|
31
|
+
return m ? `v${m[1]}` : v.trim();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The version `otelcol --version` prints: `otelcol-contrib version 0.130.0`.
|
|
35
|
+
* Undefined when the output names none.
|
|
36
|
+
*/
|
|
37
|
+
export function parseOtelcolVersion(output) {
|
|
38
|
+
const m = /version\s+v?(\d+\.\d+\.\d+[^\s]*)/i.exec(output);
|
|
39
|
+
return m ? `v${m[1]}` : undefined;
|
|
40
|
+
}
|
|
41
|
+
function readConfig(path, cwd) {
|
|
42
|
+
const text = readFileSync(resolve(cwd, path), "utf8");
|
|
43
|
+
const value = load(text);
|
|
44
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
45
|
+
throw nonRetryableFailure(`${path} is not a collector config: it does not hold a YAML mapping`, "OtelcolConfigInvalid");
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
async function binaryVersion(exec, bin) {
|
|
50
|
+
const r = await exec(bin, ["--version"], { timeoutMs: 30000 });
|
|
51
|
+
if (r.code === null)
|
|
52
|
+
throw new Error(`could not run ${bin}: ${r.error ?? "not found"}. Install the collector or set bin or $OTELCOL_BIN.`);
|
|
53
|
+
const version = parseOtelcolVersion(`${r.stdout}\n${r.stderr}`);
|
|
54
|
+
if (r.code !== 0 || !version)
|
|
55
|
+
throw new Error(`${bin} --version printed no version (exit ${r.code}): ${(r.stdout + r.stderr).trim().slice(0, 200)}`);
|
|
56
|
+
return version;
|
|
57
|
+
}
|
|
58
|
+
/** Run `otelcol validate` over `config`, after checking the binary is the pinned (or the given) version. */
|
|
59
|
+
export async function otelcolValidate(args) {
|
|
60
|
+
const exec = args._exec ?? defaultExec;
|
|
61
|
+
const bin = otelcolBin(args.bin);
|
|
62
|
+
const want = normalizeVersion(args.version ?? COLLECTOR_PIN.version);
|
|
63
|
+
const have = await binaryVersion(exec, bin);
|
|
64
|
+
if (normalizeVersion(have) !== want) {
|
|
65
|
+
const why = args.version
|
|
66
|
+
? `the step asks for ${want}`
|
|
67
|
+
: `chant's collector config types follow ${COLLECTOR_PIN.version} (COLLECTOR_PIN); pass version to validate with another collector`;
|
|
68
|
+
throw nonRetryableFailure(`${bin} is ${have}, and ${why}`, "OtelcolVersionMismatch");
|
|
69
|
+
}
|
|
70
|
+
// Fail on a missing or unreadable file here, with its name, rather than through the binary's message.
|
|
71
|
+
readConfig(args.config, process.cwd());
|
|
72
|
+
const r = await exec(bin, ["validate", `--config=${resolve(args.config)}`], { timeoutMs: 120000 });
|
|
73
|
+
const output = `${r.stdout}${r.stderr}`.trim();
|
|
74
|
+
if (r.code !== 0) {
|
|
75
|
+
throw nonRetryableFailure(`${bin} validate rejected ${args.config} (exit ${r.code ?? "none"}):\n${output}`, "OtelcolConfigInvalid");
|
|
76
|
+
}
|
|
77
|
+
return { config: args.config, bin, version: have, ok: true, output };
|
|
78
|
+
}
|
|
79
|
+
const KIND_OF_SECTION = {
|
|
80
|
+
receivers: "receiver",
|
|
81
|
+
processors: "processor",
|
|
82
|
+
exporters: "exporter",
|
|
83
|
+
connectors: "connector",
|
|
84
|
+
extensions: "extension",
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Read `otelcol components` output. The format is YAML and marked unstable
|
|
88
|
+
* upstream, so this reads what it can: each section may be a list of
|
|
89
|
+
* objects with a `name` (v0.130.0), a list of plain names (older builds),
|
|
90
|
+
* or a mapping keyed by name. Unknown top-level keys are ignored.
|
|
91
|
+
*/
|
|
92
|
+
export function parseComponentsOutput(text) {
|
|
93
|
+
let doc;
|
|
94
|
+
try {
|
|
95
|
+
doc = load(text);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
throw new Error(`otelcol components printed output that is not YAML: ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc))
|
|
101
|
+
throw new Error("otelcol components printed no YAML mapping");
|
|
102
|
+
const d = doc;
|
|
103
|
+
const components = Object.fromEntries(COMPONENT_KINDS.map((k) => [k, new Set()]));
|
|
104
|
+
let found = false;
|
|
105
|
+
for (const [section, kind] of Object.entries(KIND_OF_SECTION)) {
|
|
106
|
+
const value = d[section];
|
|
107
|
+
if (value === undefined || value === null)
|
|
108
|
+
continue;
|
|
109
|
+
found = true;
|
|
110
|
+
const names = Array.isArray(value)
|
|
111
|
+
? value.map((e) => (e && typeof e === "object" ? (e.name ?? e.type) : e))
|
|
112
|
+
: typeof value === "object"
|
|
113
|
+
? Object.keys(value)
|
|
114
|
+
: [];
|
|
115
|
+
for (const n of names)
|
|
116
|
+
if (typeof n === "string" && n !== "")
|
|
117
|
+
components[kind].add(n);
|
|
118
|
+
}
|
|
119
|
+
if (!found)
|
|
120
|
+
throw new Error("otelcol components listed no receivers, processors, exporters, connectors or extensions");
|
|
121
|
+
const build = d.buildinfo;
|
|
122
|
+
const v = build && (typeof build.version === "string" || typeof build.version === "number") ? String(build.version) : undefined;
|
|
123
|
+
return { ...(v ? { version: normalizeVersion(v) } : {}), components };
|
|
124
|
+
}
|
|
125
|
+
/** The component ids a config declares, by kind. */
|
|
126
|
+
export function configComponents(config) {
|
|
127
|
+
const out = Object.fromEntries(COMPONENT_KINDS.map((k) => [k, []]));
|
|
128
|
+
for (const kind of COMPONENT_KINDS) {
|
|
129
|
+
const section = config[SECTION_OF[kind]];
|
|
130
|
+
if (!section || typeof section !== "object" || Array.isArray(section))
|
|
131
|
+
continue;
|
|
132
|
+
out[kind] = Object.keys(section);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The components `used` names that `list` lacks. A renamed built-in counts
|
|
138
|
+
* under either name: a binary that lists `otlp_grpc` runs a config that says
|
|
139
|
+
* `otlp`, and the other way round.
|
|
140
|
+
*/
|
|
141
|
+
export function missingComponents(used, list) {
|
|
142
|
+
const missing = [];
|
|
143
|
+
for (const kind of COMPONENT_KINDS) {
|
|
144
|
+
const have = list.components[kind];
|
|
145
|
+
const canonicalHave = new Set([...have].map((t) => canonicalComponentType(kind, t)));
|
|
146
|
+
for (const id of used[kind]) {
|
|
147
|
+
const type = parseComponentId(id)?.type ?? id;
|
|
148
|
+
if (have.has(type) || canonicalHave.has(canonicalComponentType(kind, type)))
|
|
149
|
+
continue;
|
|
150
|
+
missing.push({ kind, id, type });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return missing;
|
|
154
|
+
}
|
|
155
|
+
/** Check `config` uses only components the binary was built with, from `otelcol components`. */
|
|
156
|
+
export async function otelcolComponents(args) {
|
|
157
|
+
const exec = args._exec ?? defaultExec;
|
|
158
|
+
const bin = otelcolBin(args.bin);
|
|
159
|
+
const config = readConfig(args.config, process.cwd());
|
|
160
|
+
const r = await exec(bin, ["components"], { timeoutMs: 60000 });
|
|
161
|
+
if (r.code === null)
|
|
162
|
+
throw new Error(`could not run ${bin}: ${r.error ?? "not found"}. Install the collector or set bin or $OTELCOL_BIN.`);
|
|
163
|
+
if (r.code !== 0)
|
|
164
|
+
throw new Error(`${bin} components exited ${r.code}: ${(r.stdout + r.stderr).trim().slice(0, 400)}`);
|
|
165
|
+
const list = parseComponentsOutput(r.stdout);
|
|
166
|
+
const used = configComponents(config);
|
|
167
|
+
const missing = missingComponents(used, list);
|
|
168
|
+
if (missing.length > 0) {
|
|
169
|
+
const lines = missing.map((m) => ` ${m.kind} ${m.id}`).join("\n");
|
|
170
|
+
throw nonRetryableFailure(`${args.config} uses ${missing.length} component(s) ${bin}${list.version ? ` ${list.version}` : ""} was not built with:\n${lines}`, "OtelcolComponentMissing");
|
|
171
|
+
}
|
|
172
|
+
return { config: args.config, bin, ...(list.version ? { version: list.version } : {}), used, missing };
|
|
173
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
import { z } from "zod";
|
|
16
|
+
import { activityContract } from "@intentius/chant/op";
|
|
17
|
+
const componentKind = z.enum(["receiver", "processor", "exporter", "connector", "extension"]);
|
|
18
|
+
const byKind = z.object({
|
|
19
|
+
receiver: z.array(z.string()),
|
|
20
|
+
processor: z.array(z.string()),
|
|
21
|
+
exporter: z.array(z.string()),
|
|
22
|
+
connector: z.array(z.string()),
|
|
23
|
+
extension: z.array(z.string()),
|
|
24
|
+
});
|
|
25
|
+
export const otelcolValidateContract = activityContract("otelcolValidate", z.strictObject({ config: z.string(), bin: z.string().optional(), version: z.string().optional() }), z.object({ config: z.string(), bin: z.string(), version: z.string(), ok: z.boolean(), output: z.string() }));
|
|
26
|
+
export const otelcolComponentsContract = activityContract("otelcolComponents", z.strictObject({ config: z.string(), bin: z.string().optional() }), z.object({
|
|
27
|
+
config: z.string(),
|
|
28
|
+
bin: z.string(),
|
|
29
|
+
version: z.string().optional(),
|
|
30
|
+
used: byKind,
|
|
31
|
+
missing: z.array(z.object({ kind: componentKind, id: z.string(), type: z.string() })),
|
|
32
|
+
}));
|
|
33
|
+
const endpointOverrides = z.strictObject({ healthCheck: z.string().optional(), zpages: z.string().optional(), telemetry: z.string().optional() });
|
|
34
|
+
export const collectorHealthObserveContract = activityContract("collectorHealthObserve", z.strictObject({
|
|
35
|
+
collectors: z.array(z.strictObject({
|
|
36
|
+
name: z.string(),
|
|
37
|
+
config: z.string().optional(),
|
|
38
|
+
configObject: z.record(z.string(), z.unknown()).optional(),
|
|
39
|
+
host: z.string().optional(),
|
|
40
|
+
endpoints: endpointOverrides.optional(),
|
|
41
|
+
})),
|
|
42
|
+
timeoutMs: z.number().optional(),
|
|
43
|
+
probes: z.number().optional(),
|
|
44
|
+
probeIntervalMs: z.number().optional(),
|
|
45
|
+
}), z.object({
|
|
46
|
+
resources: z.array(z.object({ name: z.string(), status: z.enum(["in-sync", "drifted", "unknown"]), detail: z.string() })),
|
|
47
|
+
}));
|
|
48
|
+
const pinState = z.object({ pin: z.string(), latest: z.string().nullable(), behind: z.number() });
|
|
49
|
+
export const collectorAuditContract = activityContract("collectorAudit", z.strictObject({
|
|
50
|
+
mode: z.enum(["report", "issue", "pull-request"]).optional(),
|
|
51
|
+
lexiconDir: z.string().optional(),
|
|
52
|
+
stability: z.boolean().optional(),
|
|
53
|
+
stabilityBudget: z.number().optional(),
|
|
54
|
+
branch: z.string().optional(),
|
|
55
|
+
}), z.object({
|
|
56
|
+
mode: z.enum(["report", "issue", "pull-request"]),
|
|
57
|
+
collector: pinState,
|
|
58
|
+
semconv: pinState,
|
|
59
|
+
findings: z.array(z.object({ kind: z.enum(["collector-pin-behind", "semconv-pin-behind", "stability-changed", "deprecated"]), subject: z.string(), detail: z.string() })),
|
|
60
|
+
unchecked: z.array(z.string()),
|
|
61
|
+
summary: z.string(),
|
|
62
|
+
issueUrl: z.string().optional(),
|
|
63
|
+
prUrl: z.string().optional(),
|
|
64
|
+
}));
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
import { Op, phase } from "@intentius/chant/op";
|
|
18
|
+
import { collectorAudit } from "./builders.js";
|
|
19
|
+
export function CollectorAuditOp(config) {
|
|
20
|
+
const step = collectorAudit({
|
|
21
|
+
mode: config.onFinding ?? "report",
|
|
22
|
+
...(config.lexiconDir ? { lexiconDir: config.lexiconDir } : {}),
|
|
23
|
+
...(config.stabilityBudget !== undefined ? { stabilityBudget: config.stabilityBudget } : {}),
|
|
24
|
+
});
|
|
25
|
+
const op = Op({
|
|
26
|
+
name: config.name,
|
|
27
|
+
overview: "Audit the otel lexicon's collector and semantic-convention pins and its components' stability against upstream releases",
|
|
28
|
+
labels: { Audit: "true", Surface: "otel-collector" },
|
|
29
|
+
...(config.schedule ? { schedule: { cron: config.schedule, overlap: "skip" } } : {}),
|
|
30
|
+
phases: [phase("Audit", [{ ...step, outcomeAttribute: { name: "Findings", from: "findings" } }])],
|
|
31
|
+
});
|
|
32
|
+
return { op };
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
import { activity, takeProfileAndId } from "@intentius/chant/op";
|
|
10
|
+
function step(fn, defaultProfile) {
|
|
11
|
+
return (args) => {
|
|
12
|
+
const { args: rest, profile, id } = takeProfileAndId(args);
|
|
13
|
+
return activity(fn, rest, { profile: profile ?? defaultProfile, ...(id ? { id } : {}) });
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Run `otelcol validate` over a collector config file. Refuses when the
|
|
18
|
+
* binary is not `COLLECTOR_PIN`'s version, unless `version` names the one it
|
|
19
|
+
* must be. Defaults to the `fastIdempotent` profile.
|
|
20
|
+
*/
|
|
21
|
+
export const otelcolValidate = step("otelcolValidate", "fastIdempotent");
|
|
22
|
+
/**
|
|
23
|
+
* Check a collector config uses only components the binary was built with,
|
|
24
|
+
* from `otelcol components`. Defaults to the `fastIdempotent` profile.
|
|
25
|
+
*/
|
|
26
|
+
export const otelcolComponents = step("otelcolComponents", "fastIdempotent");
|
|
27
|
+
/**
|
|
28
|
+
* Observe running collectors for a `ConvergeOp({ observe })`: one resource
|
|
29
|
+
* per collector, from the health_check, zpages and internal-telemetry
|
|
30
|
+
* endpoints its config declares. No `health_check` in `service.extensions`
|
|
31
|
+
* is `unknown`. Defaults to the `fastIdempotent` profile.
|
|
32
|
+
*/
|
|
33
|
+
export const collectorHealthObserve = step("collectorHealthObserve", "fastIdempotent");
|
|
34
|
+
/**
|
|
35
|
+
* Audit the lexicon's pins against upstream releases (the step of
|
|
36
|
+
* `CollectorAuditOp`). Defaults to the `fastIdempotent` profile.
|
|
37
|
+
*/
|
|
38
|
+
export const collectorAudit = (args = {}) => step("collectorAudit", "fastIdempotent")(args);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Thin entry point for `npm run bundle` in lexicon-otel.
|
|
4
|
+
*/
|
|
5
|
+
import { join, dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { writeBundleSpec } from "@intentius/chant/codegen/package";
|
|
8
|
+
import { generate, writeGeneratedFiles } from "./codegen/generate.js";
|
|
9
|
+
import { packageLexicon } from "./codegen/package.js";
|
|
10
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
writeGeneratedFiles(await generate({ verbose: true }));
|
|
12
|
+
const { spec, stats } = await packageLexicon({ verbose: true });
|
|
13
|
+
writeBundleSpec(spec, join(pkgDir, "dist"));
|
|
14
|
+
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Pipeline` and `Service`: the `service:` half of a collector config.
|
|
3
|
+
*
|
|
4
|
+
* A pipeline names its components either by the declared entity, which
|
|
5
|
+
* keeps the reference checked by TypeScript, or by id string (`otlp/backend`)
|
|
6
|
+
* for a component declared somewhere chant can't see. OTEL101 catches a
|
|
7
|
+
* string that names nothing declared.
|
|
8
|
+
*
|
|
9
|
+
* A connector goes in `exporters` of the pipeline that feeds it and in
|
|
10
|
+
* `receivers` of the pipeline it feeds. The same entity on both sides is what
|
|
11
|
+
* joins the two pipelines.
|
|
12
|
+
*/
|
|
13
|
+
import { createResource } from "@intentius/chant/runtime";
|
|
14
|
+
import { componentId } from "./model.js";
|
|
15
|
+
export const PIPELINE_TYPE = "OTel::Pipeline";
|
|
16
|
+
export const SERVICE_TYPE = "OTel::Service";
|
|
17
|
+
const PipelineBase = createResource(PIPELINE_TYPE, "otel", {});
|
|
18
|
+
/** One entry under `service.pipelines`. */
|
|
19
|
+
export const Pipeline = function (props) {
|
|
20
|
+
PipelineBase.call(this, props);
|
|
21
|
+
Object.defineProperty(this, "pipelineId", { value: componentId(props.signal, props.name), enumerable: false });
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(Pipeline, "name", { value: "Pipeline" });
|
|
24
|
+
/** `service.extensions` and `service.telemetry`. Optional; declare at most one. */
|
|
25
|
+
export const Service = createResource(SERVICE_TYPE, "otel", {});
|
|
26
|
+
export function isPipelineEntity(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && value.entityType === PIPELINE_TYPE;
|
|
28
|
+
}
|
|
29
|
+
export function isServiceEntity(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && value.entityType === SERVICE_TYPE;
|
|
31
|
+
}
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the platform composites share: a default collector config, the image
|
|
3
|
+
* that runs it, where the config is mounted, and the ports a rendered config
|
|
4
|
+
* listens on.
|
|
5
|
+
*
|
|
6
|
+
* The docker, k8s and fly collector composites build on this. A composite
|
|
7
|
+
* declares its config with this lexicon, renders it with `collectorYaml`, and
|
|
8
|
+
* reads the listening ports back from the built config, so the ports it
|
|
9
|
+
* publishes follow whatever config the caller declared.
|
|
10
|
+
*/
|
|
11
|
+
import { COLLECTOR_PIN } from "./define.js";
|
|
12
|
+
import { OtlpReceiver } from "./components/receivers.js";
|
|
13
|
+
import { BatchProcessor, MemoryLimiterProcessor } from "./components/processors.js";
|
|
14
|
+
import { DebugExporter } from "./components/exporters.js";
|
|
15
|
+
import { HealthCheckExtension } from "./components/extensions.js";
|
|
16
|
+
import { Pipeline } from "./pipeline.js";
|
|
17
|
+
import { SIGNALS, parseComponentId } from "./model.js";
|
|
18
|
+
import { listenerPaths } from "./validate-config.js";
|
|
19
|
+
/** The contrib collector image at the version the built-in components are typed against. */
|
|
20
|
+
export const COLLECTOR_IMAGE = `otel/opentelemetry-collector-contrib:${COLLECTOR_PIN.version.replace(/^v/, "")}`;
|
|
21
|
+
/** Where the platform composites mount the rendered config inside the container. */
|
|
22
|
+
export const COLLECTOR_CONFIG_PATH = "/etc/otel/config.yaml";
|
|
23
|
+
/** The receiver and processors `otlpCollector()` and `OtlpCollector` start from. */
|
|
24
|
+
function collectorBase() {
|
|
25
|
+
const otlp = new OtlpReceiver({
|
|
26
|
+
protocols: {
|
|
27
|
+
grpc: { endpoint: "0.0.0.0:4317" },
|
|
28
|
+
http: { endpoint: "0.0.0.0:4318" },
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const memoryLimiter = new MemoryLimiterProcessor({
|
|
32
|
+
check_interval: "1s",
|
|
33
|
+
limit_percentage: 80,
|
|
34
|
+
spike_limit_percentage: 20,
|
|
35
|
+
});
|
|
36
|
+
const batch = new BatchProcessor({});
|
|
37
|
+
return { otlp, memoryLimiter, batch };
|
|
38
|
+
}
|
|
39
|
+
/** `otlpCollector()`'s config by part, for the `OtlpCollector` composite. A signal listed twice gets one pipeline. */
|
|
40
|
+
export function otlpCollectorParts(options = {}) {
|
|
41
|
+
const { exporters = [new DebugExporter({ verbosity: "basic" })], signals = [...SIGNALS], healthCheck = true } = options;
|
|
42
|
+
const { otlp, memoryLimiter, batch } = collectorBase();
|
|
43
|
+
const pipelines = {};
|
|
44
|
+
for (const signal of signals) {
|
|
45
|
+
pipelines[signal] ?? (pipelines[signal] = new Pipeline({ signal, receivers: [otlp], processors: [memoryLimiter, batch], exporters }));
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
otlp,
|
|
49
|
+
memoryLimiter,
|
|
50
|
+
batch,
|
|
51
|
+
exporters,
|
|
52
|
+
...(healthCheck ? { health: new HealthCheckExtension({ endpoint: "0.0.0.0:13133" }) } : {}),
|
|
53
|
+
pipelines,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The entities of a small OTLP collector: an `otlp` receiver on 4317 (gRPC)
|
|
58
|
+
* and 4318 (HTTP), `memory_limiter` then `batch`, the given exporters, and a
|
|
59
|
+
* `health_check` extension. Pass the result to `collectorYaml`.
|
|
60
|
+
*/
|
|
61
|
+
export function otlpCollector(options = {}) {
|
|
62
|
+
const { exporters = [new DebugExporter({ verbosity: "basic" })], signals = [...SIGNALS], healthCheck = true } = options;
|
|
63
|
+
const { otlp, memoryLimiter, batch } = collectorBase();
|
|
64
|
+
const entities = [otlp, memoryLimiter, batch, ...exporters];
|
|
65
|
+
if (healthCheck)
|
|
66
|
+
entities.push(new HealthCheckExtension({ endpoint: "0.0.0.0:13133" }));
|
|
67
|
+
for (const signal of signals) {
|
|
68
|
+
entities.push(new Pipeline({ signal, receivers: [otlp], processors: [memoryLimiter, batch], exporters }));
|
|
69
|
+
}
|
|
70
|
+
return entities;
|
|
71
|
+
}
|
|
72
|
+
function portOf(endpoint) {
|
|
73
|
+
if (typeof endpoint !== "string")
|
|
74
|
+
return undefined;
|
|
75
|
+
const m = endpoint.match(/^(.*):(\d+)$/);
|
|
76
|
+
if (!m)
|
|
77
|
+
return undefined;
|
|
78
|
+
return { host: m[1], port: Number(m[2]) };
|
|
79
|
+
}
|
|
80
|
+
function portName(parts) {
|
|
81
|
+
const name = parts
|
|
82
|
+
.join("-")
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
85
|
+
.replace(/-+/g, "-")
|
|
86
|
+
.replace(/^-|-$/g, "");
|
|
87
|
+
return name.slice(0, 15).replace(/-$/, "");
|
|
88
|
+
}
|
|
89
|
+
/** The value at a key path, if every key on the way is there. */
|
|
90
|
+
function valueAt(value, path) {
|
|
91
|
+
let cur = value;
|
|
92
|
+
for (const key of path) {
|
|
93
|
+
if (!cur || typeof cur !== "object" || Array.isArray(cur))
|
|
94
|
+
return undefined;
|
|
95
|
+
cur = cur[key];
|
|
96
|
+
}
|
|
97
|
+
return cur;
|
|
98
|
+
}
|
|
99
|
+
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
100
|
+
/**
|
|
101
|
+
* The ports a built collector config listens on: the address every receiver
|
|
102
|
+
* and exporter that listens sets (the types OTEL117 knows: otlp, jaeger,
|
|
103
|
+
* statsd, syslog, the `prometheus` exporter's scrape endpoint and the rest),
|
|
104
|
+
* named after the component and the key above the address (`otlp-grpc`,
|
|
105
|
+
* `syslog-udp`), and the `health_check` port if the service enables it. A
|
|
106
|
+
* component whose `endpoint` is a server it connects to, such as
|
|
107
|
+
* kubeletstats' kubelet, adds no port, and neither does a type the table
|
|
108
|
+
* doesn't list. A UDP listener gets `protocol: "UDP"`.
|
|
109
|
+
*/
|
|
110
|
+
export function collectorEndpoints(config) {
|
|
111
|
+
const ports = [];
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const sections = [
|
|
114
|
+
["receiver", config.receivers],
|
|
115
|
+
["exporter", config.exporters],
|
|
116
|
+
];
|
|
117
|
+
for (const [kind, section] of sections) {
|
|
118
|
+
for (const [id, body] of Object.entries(section ?? {})) {
|
|
119
|
+
const parsed = parseComponentId(id);
|
|
120
|
+
const base = parsed ? [parsed.type, ...(parsed.name ? [parsed.name] : [])] : [id];
|
|
121
|
+
for (const { path, transport } of listenerPaths(kind, parsed?.type ?? id, body)) {
|
|
122
|
+
const p = portOf(valueAt(body, path));
|
|
123
|
+
if (!p || seen.has(`${p.port}/${transport}`))
|
|
124
|
+
continue;
|
|
125
|
+
seen.add(`${p.port}/${transport}`);
|
|
126
|
+
const protocol = path.slice(0, -1).filter((k) => k !== "protocols");
|
|
127
|
+
ports.push({ name: portName([...base, ...protocol]), port: p.port, ...(transport === "udp" ? { protocol: "UDP" } : {}) });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let healthCheck;
|
|
132
|
+
const enabled = config.service?.extensions ?? [];
|
|
133
|
+
for (const id of enabled) {
|
|
134
|
+
if (parseComponentId(id)?.type !== "health_check")
|
|
135
|
+
continue;
|
|
136
|
+
const ext = (config.extensions?.[id] ?? {});
|
|
137
|
+
const p = portOf(ext.endpoint ?? "localhost:13133");
|
|
138
|
+
if (!p || LOCAL_HOSTS.has(p.host))
|
|
139
|
+
continue;
|
|
140
|
+
healthCheck = { port: p.port, path: typeof ext.path === "string" ? ext.path : "/" };
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
return { ports, ...(healthCheck ? { healthCheck } : {}) };
|
|
144
|
+
}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
|
|
2
|
+
import { otelSerializer } from "./serializer.js";
|
|
3
|
+
import { rules } from "./lint/rules/index.js";
|
|
4
|
+
import { postSynthChecks } from "./lint/post-synth/index.js";
|
|
5
|
+
import { otelAuditCatalog } from "./lint/audit-catalog.js";
|
|
6
|
+
import { completions } from "./lsp/completions.js";
|
|
7
|
+
import { hover } from "./lsp/hover.js";
|
|
8
|
+
import { detectTemplate } from "./detect.js";
|
|
9
|
+
import { OtelCollectorParser } from "./import/parser.js";
|
|
10
|
+
import { OtelCollectorGenerator } from "./import/generator.js";
|
|
11
|
+
import { collectorConfigImporter } from "./import/embedded.js";
|
|
12
|
+
import { otelSkills } from "./skill-defs.js";
|
|
13
|
+
import { BUILTIN_CATALOG } from "./catalog.js";
|
|
14
|
+
import { compositeCatalog } from "./composites/catalog.js";
|
|
15
|
+
import { DEFAULT_TEMPLATE, GENAI_TEMPLATE, K8S_AGENT_TEMPLATE } from "./init-templates.js";
|
|
16
|
+
import { COLLECTOR_PIN } from "./define.js";
|
|
17
|
+
import { collectorTopologyOf } from "./topology.js";
|
|
18
|
+
const catalogResource = {
|
|
19
|
+
uri: "otel:resource-catalog",
|
|
20
|
+
name: "OpenTelemetry Collector component catalog",
|
|
21
|
+
description: "The collector components, pipelines and service block this lexicon types, with the collector release they follow",
|
|
22
|
+
mimeType: "application/json",
|
|
23
|
+
async handler() {
|
|
24
|
+
return JSON.stringify({ pin: COLLECTOR_PIN, entities: BUILTIN_CATALOG });
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* OpenTelemetry Collector lexicon plugin.
|
|
29
|
+
*
|
|
30
|
+
* Typed receivers, processors, exporters, connectors and extensions, pipelines and the
|
|
31
|
+
* service block, serialized to one collector config file. A component chant
|
|
32
|
+
* doesn't ship comes in through `defineComponent`, and is serialized and
|
|
33
|
+
* checked the same way as the built-ins.
|
|
34
|
+
*/
|
|
35
|
+
export const otelPlugin = {
|
|
36
|
+
name: "otel",
|
|
37
|
+
serializer: otelSerializer,
|
|
38
|
+
// ── Required lifecycle methods ────────────────────────────────
|
|
39
|
+
async generate(options) {
|
|
40
|
+
const { generate, writeGeneratedFiles } = await import("./codegen/generate.js");
|
|
41
|
+
writeGeneratedFiles(await generate(options));
|
|
42
|
+
},
|
|
43
|
+
async validate(_options) {
|
|
44
|
+
const { validate } = await import("./validate.js");
|
|
45
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
46
|
+
printValidationResult(await validate());
|
|
47
|
+
},
|
|
48
|
+
async coverage(_options) {
|
|
49
|
+
const components = BUILTIN_CATALOG.filter((c) => c.type !== undefined);
|
|
50
|
+
const byKind = new Map();
|
|
51
|
+
for (const c of components)
|
|
52
|
+
byKind.set(c.kind, [...(byKind.get(c.kind) ?? []), c.type]);
|
|
53
|
+
console.error(`otel: ${components.length} built-in components, typed against ${COLLECTOR_PIN.source} ${COLLECTOR_PIN.version}`);
|
|
54
|
+
for (const [kind, types] of byKind)
|
|
55
|
+
console.error(` ${kind}s: ${types.join(", ")}`);
|
|
56
|
+
console.error(" anything else: defineComponent (see docs: custom components)");
|
|
57
|
+
},
|
|
58
|
+
async package(options) {
|
|
59
|
+
const { packageLexicon } = await import("./codegen/package.js");
|
|
60
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
61
|
+
const { join, dirname } = await import("path");
|
|
62
|
+
const { fileURLToPath } = await import("url");
|
|
63
|
+
const { spec, stats } = await packageLexicon(options);
|
|
64
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
65
|
+
writeBundleSpec(spec, join(pkgDir, "dist"));
|
|
66
|
+
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
67
|
+
},
|
|
68
|
+
// ── Optional extensions ────────────────────────────────────
|
|
69
|
+
lintRules() {
|
|
70
|
+
return rules;
|
|
71
|
+
},
|
|
72
|
+
// Where this project's telemetry goes, for `chant workspace graph` (#2559).
|
|
73
|
+
// A project with no collector pipeline and no component reports nothing.
|
|
74
|
+
graphMeta(entities) {
|
|
75
|
+
const topology = collectorTopologyOf(entities.values());
|
|
76
|
+
if (topology.pipelines.length === 0 && topology.components.length === 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
return { collector: topology };
|
|
79
|
+
},
|
|
80
|
+
postSynthChecks() {
|
|
81
|
+
return postSynthChecks;
|
|
82
|
+
},
|
|
83
|
+
auditCatalog() {
|
|
84
|
+
return otelAuditCatalog;
|
|
85
|
+
},
|
|
86
|
+
skills: otelSkills,
|
|
87
|
+
composites() {
|
|
88
|
+
return compositeCatalog;
|
|
89
|
+
},
|
|
90
|
+
mcpTools() {
|
|
91
|
+
return [createDiffTool(otelSerializer, "Compare current collector config output against the previous build", "otel")];
|
|
92
|
+
},
|
|
93
|
+
mcpResources() {
|
|
94
|
+
return [catalogResource];
|
|
95
|
+
},
|
|
96
|
+
// `chant init --lexicon otel [--template k8s-agent|genai]`; see ./init-templates.ts.
|
|
97
|
+
initTemplates(template) {
|
|
98
|
+
if (template === "k8s-agent")
|
|
99
|
+
return K8S_AGENT_TEMPLATE;
|
|
100
|
+
if (template === "genai")
|
|
101
|
+
return GENAI_TEMPLATE;
|
|
102
|
+
return DEFAULT_TEMPLATE;
|
|
103
|
+
},
|
|
104
|
+
detectTemplate(data) {
|
|
105
|
+
return detectTemplate(data);
|
|
106
|
+
},
|
|
107
|
+
templateParser() {
|
|
108
|
+
return new OtelCollectorParser();
|
|
109
|
+
},
|
|
110
|
+
templateGenerator() {
|
|
111
|
+
return new OtelCollectorGenerator();
|
|
112
|
+
},
|
|
113
|
+
embeddedImporters() {
|
|
114
|
+
return [collectorConfigImporter];
|
|
115
|
+
},
|
|
116
|
+
completionProvider(ctx) {
|
|
117
|
+
return completions(ctx);
|
|
118
|
+
},
|
|
119
|
+
hoverProvider(ctx) {
|
|
120
|
+
return hover(ctx);
|
|
121
|
+
},
|
|
122
|
+
async docs(options) {
|
|
123
|
+
const { generateDocs } = await import("./codegen/docs.js");
|
|
124
|
+
return generateDocs(options);
|
|
125
|
+
},
|
|
126
|
+
};
|