@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.
Files changed (96) hide show
  1. package/dist/attribution.js +391 -0
  2. package/dist/catalog.js +42 -0
  3. package/dist/codegen/docs-cli.js +3 -0
  4. package/dist/codegen/docs.js +101 -0
  5. package/dist/codegen/generate-cli.js +7 -0
  6. package/dist/codegen/generate.js +43 -0
  7. package/dist/codegen/package.js +28 -0
  8. package/dist/collector.js +150 -0
  9. package/dist/components/common.js +50 -0
  10. package/dist/components/connectors.js +242 -0
  11. package/dist/components/exporters.js +57 -0
  12. package/dist/components/extensions.js +58 -0
  13. package/dist/components/filtering.js +106 -0
  14. package/dist/components/index.js +9 -0
  15. package/dist/components/k8s-receivers.js +36 -0
  16. package/dist/components/processors.js +103 -0
  17. package/dist/components/receivers.js +61 -0
  18. package/dist/components/sampling.js +257 -0
  19. package/dist/composites/catalog.js +368 -0
  20. package/dist/composites/genai-pipeline.js +28 -0
  21. package/dist/composites/index.js +8 -0
  22. package/dist/composites/node-agent.js +156 -0
  23. package/dist/composites/otlp-collector.js +57 -0
  24. package/dist/composites/red-metrics.js +95 -0
  25. package/dist/composites/tail-sampling-tier.js +132 -0
  26. package/dist/config-hygiene.js +464 -0
  27. package/dist/configmap.js +86 -0
  28. package/dist/define.js +204 -0
  29. package/dist/detect.js +22 -0
  30. package/dist/genai.js +569 -0
  31. package/dist/import/embedded.js +57 -0
  32. package/dist/import/generator.js +459 -0
  33. package/dist/import/parser.js +199 -0
  34. package/dist/index.js +34 -0
  35. package/dist/init-templates.js +119 -0
  36. package/dist/integrity.json +2 -2
  37. package/dist/lint/audit-catalog.js +49 -0
  38. package/dist/lint/post-synth/index.js +52 -0
  39. package/dist/lint/post-synth/otel-helpers.js +103 -0
  40. package/dist/lint/post-synth/otel101.js +13 -0
  41. package/dist/lint/post-synth/otel102.js +13 -0
  42. package/dist/lint/post-synth/otel103.js +13 -0
  43. package/dist/lint/post-synth/otel104.js +13 -0
  44. package/dist/lint/post-synth/otel105.js +13 -0
  45. package/dist/lint/post-synth/otel106.js +13 -0
  46. package/dist/lint/post-synth/otel107.js +13 -0
  47. package/dist/lint/post-synth/otel108.js +13 -0
  48. package/dist/lint/post-synth/otel109.js +13 -0
  49. package/dist/lint/post-synth/otel112.js +13 -0
  50. package/dist/lint/post-synth/otel113.js +13 -0
  51. package/dist/lint/post-synth/otel114.js +13 -0
  52. package/dist/lint/post-synth/otel115.js +13 -0
  53. package/dist/lint/post-synth/otel116.js +13 -0
  54. package/dist/lint/post-synth/otel117.js +13 -0
  55. package/dist/lint/post-synth/otel118.js +13 -0
  56. package/dist/lint/post-synth/otel119.js +13 -0
  57. package/dist/lint/post-synth/otel120.js +13 -0
  58. package/dist/lint/post-synth/otel121.js +13 -0
  59. package/dist/lint/post-synth/otel122.js +13 -0
  60. package/dist/lint/post-synth/otel123.js +13 -0
  61. package/dist/lint/post-synth/otel124.js +13 -0
  62. package/dist/lint/post-synth/otel125.js +13 -0
  63. package/dist/lint/post-synth/otel126.js +13 -0
  64. package/dist/lint/post-synth/otel127.js +13 -0
  65. package/dist/lint/rules/component-id-syntax.js +66 -0
  66. package/dist/lint/rules/index.js +6 -0
  67. package/dist/lint/rules/literal-credential.js +91 -0
  68. package/dist/lint/rules/otel-ast.js +30 -0
  69. package/dist/lsp/completions.js +11 -0
  70. package/dist/lsp/hover.js +23 -0
  71. package/dist/manifest.json +1 -1
  72. package/dist/metric-names.js +244 -0
  73. package/dist/model.js +90 -0
  74. package/dist/op/activities/collector-audit.js +213 -0
  75. package/dist/op/activities/collector-health.js +176 -0
  76. package/dist/op/activities/exec.js +16 -0
  77. package/dist/op/activities/github.js +110 -0
  78. package/dist/op/activities/index.js +16 -0
  79. package/dist/op/activities/otelcol.js +173 -0
  80. package/dist/op/activity-contracts.js +64 -0
  81. package/dist/op/audit-op.js +33 -0
  82. package/dist/op/builders.js +38 -0
  83. package/dist/package-cli.js +14 -0
  84. package/dist/pipeline.js +31 -0
  85. package/dist/platform.js +144 -0
  86. package/dist/plugin.js +126 -0
  87. package/dist/semconv.js +50 -0
  88. package/dist/serializer.js +26 -0
  89. package/dist/skill-defs.js +34 -0
  90. package/dist/topology.js +104 -0
  91. package/dist/validate-cli.js +7 -0
  92. package/dist/validate-config.js +706 -0
  93. package/dist/validate.js +85 -0
  94. package/dist/yaml.js +157 -0
  95. package/package.json +8 -8
  96. package/src/codegen/package.ts +2 -2
@@ -0,0 +1,459 @@
1
+ /**
2
+ * `TemplateIR` -> TypeScript, for `chant import`.
3
+ *
4
+ * Writes the collector the way the lexicon's examples are written: a module
5
+ * per section with one constant per component, typed by its built-in class
6
+ * (`const otlpTempo = new OtlpExporter({ name: "tempo", ... })`), nested
7
+ * settings lifted into consts typed by the component's config type (COR001),
8
+ * at most eight declarables per module (COR009), and an `export { ... }` list
9
+ * at the end of each. `pipelines.ts` declares a `Pipeline` per pipeline that
10
+ * references those constants, and `service.ts` a `Service` when
11
+ * `service.extensions` or `service.telemetry` needs one. A connector is one
12
+ * constant, listed in the exporters of the pipeline that feeds it and the
13
+ * receivers of the pipeline it feeds.
14
+ *
15
+ * A component type chant does not ship gets a `defineComponent` in
16
+ * `custom-components.ts`, with its config carried as data and a comment
17
+ * saying so. Its pin is the one the config's `# chant:` header named, or
18
+ * `COLLECTOR_PIN`.
19
+ */
20
+ import * as components from "../components/index.js";
21
+ import { COLLECTOR_PIN } from "../define.js";
22
+ import { COMPONENT_KINDS, SECTION_OF, canonicalComponentType, SIGNALS, parseComponentId, pipelineSignal, } from "../model.js";
23
+ import { COLLECTOR_RESOURCE_TYPE, } from "./parser.js";
24
+ const PACKAGE = "@intentius/chant-lexicon-otel";
25
+ const KIND_SEGMENT = {
26
+ receiver: "Receiver",
27
+ processor: "Processor",
28
+ exporter: "Exporter",
29
+ connector: "Connector",
30
+ extension: "Extension",
31
+ };
32
+ /** `kind:type` -> the exported class name of every built-in. */
33
+ const BUILTIN_CLASSES = (() => {
34
+ const map = new Map();
35
+ for (const [exportName, value] of Object.entries(components)) {
36
+ const def = value?.definition;
37
+ if (typeof value === "function" && def?.builtin)
38
+ map.set(`${def.kind}:${def.type}`, exportName);
39
+ }
40
+ return map;
41
+ })();
42
+ /**
43
+ * The class name of the built-in for `kind` + `type`, if chant ships one.
44
+ * A renamed built-in's new name (`span_metrics`) gives the class for the old
45
+ * one, which emits the old name.
46
+ */
47
+ export function builtinClassName(kind, type) {
48
+ return BUILTIN_CLASSES.get(`${kind}:${type}`) ?? BUILTIN_CLASSES.get(`${kind}:${canonicalComponentType(kind, type)}`);
49
+ }
50
+ const RESERVED = new Set(("break case catch class const continue debugger default delete do else enum export extends false finally for " +
51
+ "function if import in instanceof new null return super switch this throw true try typeof var void while with " +
52
+ "yield let static implements interface package private protected public await arguments eval undefined").split(" "));
53
+ const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
54
+ function words(text) {
55
+ return text.split(/[^A-Za-z0-9]+/).filter((w) => w !== "");
56
+ }
57
+ function camel(text) {
58
+ const parts = words(text);
59
+ if (parts.length === 0)
60
+ return "";
61
+ return parts
62
+ .map((w, i) => (i === 0 ? w.charAt(0).toLowerCase() + w.slice(1) : w.charAt(0).toUpperCase() + w.slice(1)))
63
+ .join("");
64
+ }
65
+ function pascal(text) {
66
+ return words(text)
67
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
68
+ .join("");
69
+ }
70
+ // ── TypeScript literals ──────────────────────────────────────────────
71
+ const MAX_LINE = 100;
72
+ function propertyKey(k) {
73
+ return IDENT.test(k) ? k : JSON.stringify(k);
74
+ }
75
+ /** A string literal: double-quoted, or single-quoted when that saves escaping `"` (OTTL reads better). */
76
+ function stringLiteral(s) {
77
+ const double = JSON.stringify(s);
78
+ if (!s.includes('"') || s.includes("'"))
79
+ return double;
80
+ return `'${double.slice(1, -1).replace(/\\"/g, '"')}'`;
81
+ }
82
+ function scalarLiteral(v) {
83
+ if (v === null)
84
+ return "null";
85
+ if (typeof v === "string")
86
+ return stringLiteral(v);
87
+ if (typeof v === "number") {
88
+ if (Number.isNaN(v))
89
+ return "NaN";
90
+ if (v === Infinity)
91
+ return "Infinity";
92
+ if (v === -Infinity)
93
+ return "-Infinity";
94
+ return String(v);
95
+ }
96
+ if (typeof v === "boolean")
97
+ return v ? "true" : "false";
98
+ return JSON.stringify(String(v));
99
+ }
100
+ /** One-line rendering of a value. */
101
+ function inline(v) {
102
+ if (Array.isArray(v))
103
+ return `[${v.map(inline).join(", ")}]`;
104
+ if (typeof v === "object" && v !== null) {
105
+ const items = Object.entries(v)
106
+ .filter(([, x]) => x !== undefined)
107
+ .map(([k, x]) => `${propertyKey(k)}: ${inline(x)}`);
108
+ return items.length === 0 ? "{}" : `{ ${items.join(", ")} }`;
109
+ }
110
+ return scalarLiteral(v);
111
+ }
112
+ /** A value as a TypeScript expression, starting at column `col`, indented by `indent` spaces. */
113
+ export function tsLiteral(v, indent, col = indent) {
114
+ const one = inline(v);
115
+ if (col + one.length + 2 <= MAX_LINE)
116
+ return one;
117
+ const pad = " ".repeat(indent + 2);
118
+ const close = " ".repeat(indent);
119
+ if (Array.isArray(v)) {
120
+ const items = v.map((x) => `${pad}${tsLiteral(x, indent + 2)},`);
121
+ return `[\n${items.join("\n")}\n${close}]`;
122
+ }
123
+ if (typeof v === "object" && v !== null) {
124
+ const lines = [];
125
+ for (const [k, x] of Object.entries(v)) {
126
+ if (x === undefined)
127
+ continue;
128
+ const key = `${pad}${propertyKey(k)}: `;
129
+ lines.push(`${key}${tsLiteral(x, indent + 2, key.length)},`);
130
+ }
131
+ return `{\n${lines.join("\n")}\n${close}}`;
132
+ }
133
+ return scalarLiteral(v);
134
+ }
135
+ /** An object literal whose entries are already-rendered expressions, starting at column `col`. */
136
+ function objectOf(entries, indent, col) {
137
+ if (entries.length === 0)
138
+ return "{}";
139
+ const prop = ([k, v]) => (k === v && IDENT.test(k) ? k : `${propertyKey(k)}: ${v}`);
140
+ const one = `{ ${entries.map(prop).join(", ")} }`;
141
+ if (!one.includes("\n") && col + one.length + 2 <= MAX_LINE)
142
+ return one;
143
+ const pad = " ".repeat(indent + 2);
144
+ return `{\n${entries.map((e) => `${pad}${prop(e)},`).join("\n")}\n${" ".repeat(indent)}}`;
145
+ }
146
+ function assignNames(comps, pipes, taken) {
147
+ const base = (s, fallback) => {
148
+ const c = camel(s);
149
+ return c === "" || /^[0-9]/.test(c) ? camel(`${fallback} ${s}`) : c;
150
+ };
151
+ const candidates = new Map();
152
+ const compBase = comps.map((c) => base(c.id, c.kind));
153
+ const pipeBase = pipes.map((p) => base(p.id, "pipeline"));
154
+ for (const b of [...compBase, ...pipeBase])
155
+ candidates.set(b, (candidates.get(b) ?? 0) + 1);
156
+ const unique = (name) => {
157
+ let out = name;
158
+ for (let n = 2; taken.has(out) || RESERVED.has(out); n++)
159
+ out = `${name}${n}`;
160
+ taken.add(out);
161
+ return out;
162
+ };
163
+ const clash = (b) => (candidates.get(b) ?? 0) > 1 || RESERVED.has(b) || taken.has(b);
164
+ comps.forEach((c, i) => {
165
+ const b = compBase[i];
166
+ c.varName = unique(clash(b) ? `${b}${KIND_SEGMENT[c.kind]}` : b);
167
+ });
168
+ pipes.forEach((p, i) => {
169
+ const b = pipeBase[i];
170
+ p.varName = unique(clash(b) ? `${b}Pipeline` : b);
171
+ });
172
+ }
173
+ // ── files ────────────────────────────────────────────────────────────
174
+ /** COR009's default: at most this many declarables per file. */
175
+ const DECLARABLES_PER_FILE = 8;
176
+ /** One generated module: what it imports, declares and exports. */
177
+ class Module {
178
+ constructor(path, summary) {
179
+ this.path = path;
180
+ this.summary = summary;
181
+ this.values = new Set();
182
+ this.types = new Set();
183
+ this.local = new Map();
184
+ this.body = [];
185
+ this.exports = [];
186
+ }
187
+ use(name, from) {
188
+ if (!from)
189
+ this.values.add(name);
190
+ else if (from !== this) {
191
+ const spec = `./${from.path.replace(/\.ts$/, "")}`;
192
+ const set = this.local.get(spec) ?? new Set();
193
+ set.add(name);
194
+ this.local.set(spec, set);
195
+ }
196
+ }
197
+ block(lines) {
198
+ if (this.body.length > 0)
199
+ this.body.push("");
200
+ this.body.push(...lines);
201
+ }
202
+ render() {
203
+ const byName = (a, b) => a.localeCompare(b, "en", { sensitivity: "base" });
204
+ const names = [...[...this.values].map((v) => v), ...[...this.types].map((t) => `type ${t}`)].sort((a, b) => byName(a.replace(/^type /, ""), b.replace(/^type /, "")));
205
+ const lines = [`/** ${this.summary} */`];
206
+ if (names.length > 0) {
207
+ const one = `import { ${names.join(", ")} } from "${PACKAGE}";`;
208
+ lines.push(one.length <= MAX_LINE ? one : `import {\n${names.map((n) => ` ${n},`).join("\n")}\n} from "${PACKAGE}";`);
209
+ }
210
+ for (const [spec, set] of [...this.local].sort(([a], [b]) => byName(a, b))) {
211
+ lines.push(`import { ${[...set].sort(byName).join(", ")} } from "${spec}";`);
212
+ }
213
+ lines.push("", ...this.body);
214
+ if (this.exports.length > 0) {
215
+ const one = `export { ${this.exports.join(", ")} };`;
216
+ lines.push("", one.length <= MAX_LINE ? one : `export {\n${this.exports.map((e) => ` ${e},`).join("\n")}\n};`);
217
+ }
218
+ return `${lines.join("\n")}\n`;
219
+ }
220
+ }
221
+ /** Split `items` into files of at most `DECLARABLES_PER_FILE`, named `base.ts` or `base-1.ts`, `base-2.ts`, … */
222
+ function chunk(items, base, summary) {
223
+ if (items.length === 0)
224
+ return [];
225
+ if (items.length <= DECLARABLES_PER_FILE)
226
+ return [[new Module(`${base}.ts`, summary), items]];
227
+ const out = [];
228
+ for (let i = 0; i < items.length; i += DECLARABLES_PER_FILE) {
229
+ out.push([new Module(`${base}-${out.length + 1}.ts`, summary), items.slice(i, i + DECLARABLES_PER_FILE)]);
230
+ }
231
+ return out;
232
+ }
233
+ // ── generation ───────────────────────────────────────────────────────
234
+ function isCollectorResource(r) {
235
+ return r.type === COLLECTOR_RESOURCE_TYPE;
236
+ }
237
+ function samePin(a, b) {
238
+ return a.source === b.source && a.version === b.version && (a.digest ?? "") === (b.digest ?? "");
239
+ }
240
+ /** True for a value COR001 wants in a named const rather than inline in a constructor. */
241
+ function needsHoist(v) {
242
+ if (Array.isArray(v))
243
+ return v.some((x) => typeof x === "object" && x !== null);
244
+ return typeof v === "object" && v !== null;
245
+ }
246
+ const SECTION_SUMMARY = {
247
+ receiver: "Receivers",
248
+ processor: "Processors",
249
+ exporter: "Exporters",
250
+ connector: "Connectors: each one is an exporter in the pipeline that feeds it and a receiver in the pipeline it feeds",
251
+ extension: "Extensions",
252
+ };
253
+ /**
254
+ * Generate the TypeScript declaring one collector config: one module per
255
+ * section (split at eight declarables, as COR009 asks), `pipelines.ts`,
256
+ * `service.ts` when the config needs a Service, and `custom-components.ts`
257
+ * for component types chant does not ship.
258
+ */
259
+ export function generateCollectorFiles(config, meta = {}) {
260
+ const pins = meta.pins ?? [];
261
+ const comps = [];
262
+ const customClasses = new Map();
263
+ const taken = new Set(["Pipeline", "Service", "defineComponent", "COLLECTOR_PIN", "ServiceTelemetry", "telemetry"]);
264
+ for (const cls of BUILTIN_CLASSES.values())
265
+ taken.add(cls).add(`${cls}Config`);
266
+ for (const kind of COMPONENT_KINDS) {
267
+ const section = config[SECTION_OF[kind]];
268
+ for (const [id, raw] of Object.entries(section ?? {})) {
269
+ const parsed = parseComponentId(id);
270
+ const slash = id.indexOf("/");
271
+ const type = parsed?.type ?? (slash === -1 ? id : id.slice(0, slash));
272
+ const name = parsed ? parsed.name : slash === -1 ? undefined : id.slice(slash + 1);
273
+ const { name: _dropped, ...cfg } = (raw ?? {});
274
+ const builtin = builtinClassName(kind, type);
275
+ let className = builtin;
276
+ if (!className) {
277
+ const key = `${kind}:${type}`;
278
+ let custom = customClasses.get(key);
279
+ if (!custom) {
280
+ const base = `${/^[0-9]/.test(type) ? "Custom" : ""}${pascal(type) || "Custom"}${KIND_SEGMENT[kind]}`;
281
+ let cn = base;
282
+ for (let n = 2; taken.has(cn); n++)
283
+ cn = `${base}${n}`;
284
+ taken.add(cn);
285
+ const headerPin = pins.find((p) => p.kind === kind && parseComponentId(p.id)?.type === type)?.pin;
286
+ custom = { className: cn, kind, type, pin: headerPin ?? COLLECTOR_PIN };
287
+ customClasses.set(key, custom);
288
+ }
289
+ className = custom.className;
290
+ }
291
+ comps.push({ kind, id, type, name, config: cfg, className, builtin: !!builtin, varName: "" });
292
+ }
293
+ }
294
+ const pipes = Object.entries(config.service?.pipelines ?? {}).map(([id, lists]) => {
295
+ const slash = id.indexOf("/");
296
+ return {
297
+ id,
298
+ signal: pipelineSignal(id),
299
+ name: slash === -1 ? undefined : id.slice(slash + 1),
300
+ lists: lists ?? {},
301
+ varName: "",
302
+ };
303
+ });
304
+ assignNames(comps, pipes, taken);
305
+ const unique = (name) => {
306
+ let out = name;
307
+ for (let n = 2; taken.has(out) || RESERVED.has(out); n++)
308
+ out = `${name}${n}`;
309
+ taken.add(out);
310
+ return out;
311
+ };
312
+ const modules = [];
313
+ const home = new Map(); // variable or class name -> module declaring it
314
+ // Custom component types, exported for every module that declares an instance.
315
+ if (customClasses.size > 0) {
316
+ const mod = new Module("custom-components.ts", "Component types chant does not ship, declared with defineComponent");
317
+ modules.push(mod);
318
+ mod.use("defineComponent");
319
+ for (const custom of customClasses.values()) {
320
+ const pinIsCollector = samePin(custom.pin, COLLECTOR_PIN);
321
+ if (pinIsCollector)
322
+ mod.use("COLLECTOR_PIN");
323
+ mod.block([
324
+ `// ${custom.kind} "${custom.type}" is not a component chant ships, so its config is carried as data`,
325
+ "// and is not type-checked. Replace Record<string, unknown> with its config type to check it.",
326
+ pinIsCollector
327
+ ? "// The imported config named no schema pin for it, so it is pinned to COLLECTOR_PIN."
328
+ : "// The pin is the one the imported config's `# chant:` header named.",
329
+ `const ${custom.className} = defineComponent<Record<string, unknown>>()({`,
330
+ ` kind: ${JSON.stringify(custom.kind)},`,
331
+ ` type: ${JSON.stringify(custom.type)},`,
332
+ ` pin: ${pinIsCollector ? "COLLECTOR_PIN" : tsLiteral({ ...custom.pin }, 2, 7)},`,
333
+ "});",
334
+ ]);
335
+ mod.exports.push(custom.className);
336
+ home.set(custom.className, mod);
337
+ }
338
+ }
339
+ // Components, one module per section.
340
+ for (const kind of COMPONENT_KINDS) {
341
+ const ofKind = comps.filter((c) => c.kind === kind);
342
+ for (const [mod, items] of chunk(ofKind, SECTION_OF[kind], SECTION_SUMMARY[kind])) {
343
+ modules.push(mod);
344
+ for (const c of items) {
345
+ const lines = [];
346
+ if (c.builtin)
347
+ mod.use(c.className);
348
+ else
349
+ mod.use(c.className, home.get(c.className));
350
+ const entries = [];
351
+ if (c.name !== undefined)
352
+ entries.push(["name", JSON.stringify(c.name)]);
353
+ for (const [key, value] of Object.entries(c.config)) {
354
+ if (value === undefined)
355
+ continue;
356
+ if (!needsHoist(value)) {
357
+ entries.push([key, tsLiteral(value, 2, 4 + key.length)]);
358
+ continue;
359
+ }
360
+ const constName = unique(`${c.varName}${pascal(key) || "Value"}`);
361
+ let annotation = "";
362
+ if (c.builtin) {
363
+ mod.types.add(`${c.className}Config`);
364
+ annotation = `: ${c.className}Config[${JSON.stringify(key)}]`;
365
+ }
366
+ const head = `const ${constName}${annotation} = `;
367
+ lines.push(`${head}${tsLiteral(value, 0, head.length)};`);
368
+ entries.push([key, constName]);
369
+ }
370
+ const head = `const ${c.varName} = new ${c.className}(`;
371
+ lines.push(`${head}${entries.length === 0 ? "" : objectOf(entries, 0, head.length)});`);
372
+ mod.block(lines);
373
+ mod.exports.push(c.varName);
374
+ home.set(c.varName, mod);
375
+ }
376
+ }
377
+ }
378
+ const byKindId = new Map();
379
+ for (const c of comps)
380
+ byKindId.set(`${c.kind}:${c.id}`, c);
381
+ const ref = (mod, id, kinds) => {
382
+ for (const k of kinds) {
383
+ const c = byKindId.get(`${k}:${id}`);
384
+ if (c) {
385
+ mod.use(c.varName, home.get(c.varName));
386
+ return c.varName;
387
+ }
388
+ }
389
+ return JSON.stringify(id);
390
+ };
391
+ // Pipelines.
392
+ for (const [mod, items] of chunk(pipes, "pipelines", "Pipelines")) {
393
+ modules.push(mod);
394
+ mod.use("Pipeline");
395
+ for (const p of items) {
396
+ const entries = [["signal", JSON.stringify(p.signal)]];
397
+ if (p.name !== undefined)
398
+ entries.push(["name", JSON.stringify(p.name)]);
399
+ const list = (ids, kinds) => `[${(ids ?? []).map((id) => ref(mod, id, kinds)).join(", ")}]`;
400
+ entries.push(["receivers", list(p.lists.receivers, ["receiver", "connector"])]);
401
+ if (p.lists.processors && p.lists.processors.length > 0) {
402
+ entries.push(["processors", list(p.lists.processors, ["processor"])]);
403
+ }
404
+ entries.push(["exporters", list(p.lists.exporters, ["exporter", "connector"])]);
405
+ const head = `const ${p.varName} = new Pipeline(`;
406
+ if (SIGNALS.includes(p.signal)) {
407
+ mod.block([`${head}${objectOf(entries, 0, head.length)});`]);
408
+ }
409
+ else {
410
+ mod.block([
411
+ `${head}{`,
412
+ ` // @ts-expect-error "${p.signal}" is a collector signal Pipeline does not type`,
413
+ ...entries.map(([k, v]) => ` ${k}: ${v},`),
414
+ "});",
415
+ ]);
416
+ }
417
+ mod.exports.push(p.varName);
418
+ }
419
+ }
420
+ // Service: only when the config says something the default does not.
421
+ const declaredExtensions = comps.filter((c) => c.kind === "extension").map((c) => c.id);
422
+ const enabled = config.service?.extensions;
423
+ const telemetry = config.service?.telemetry;
424
+ const extensionsDiffer = enabled === undefined
425
+ ? declaredExtensions.length > 0
426
+ : enabled.length !== declaredExtensions.length || enabled.some((id, i) => declaredExtensions[i] !== id);
427
+ if (extensionsDiffer || telemetry !== undefined) {
428
+ const mod = new Module("service.ts", "service.extensions and service.telemetry");
429
+ modules.push(mod);
430
+ mod.use("Service");
431
+ const lines = [];
432
+ const entries = [];
433
+ if (extensionsDiffer)
434
+ entries.push(["extensions", `[${(enabled ?? []).map((id) => ref(mod, id, ["extension"])).join(", ")}]`]);
435
+ if (telemetry !== undefined) {
436
+ mod.types.add("ServiceTelemetry");
437
+ const head = "const telemetry: ServiceTelemetry = ";
438
+ lines.push(`${head}${tsLiteral(telemetry, 0, head.length)};`);
439
+ entries.push(["telemetry", "telemetry"]);
440
+ }
441
+ const serviceName = unique("service");
442
+ const head = `const ${serviceName} = new Service(`;
443
+ lines.push(`${head}${objectOf(entries, 0, head.length)});`);
444
+ mod.block(lines);
445
+ mod.exports.push(serviceName);
446
+ }
447
+ return modules.map((m) => ({ path: m.path, content: m.render() }));
448
+ }
449
+ /** The OpenTelemetry Collector TypeScript generator `chant import` runs. */
450
+ export class OtelCollectorGenerator {
451
+ generate(ir) {
452
+ const resource = ir.resources.find(isCollectorResource);
453
+ const props = resource?.properties;
454
+ const meta = resource?.metadata;
455
+ const files = generateCollectorFiles(props?.config ?? {}, meta ?? {});
456
+ // Core writes the first file of a generate() call as a whole module for some layouts; never hand it nothing.
457
+ return files.length > 0 ? files : [{ path: "pipelines.ts", content: "// The imported config declares nothing.\n" }];
458
+ }
459
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Collector YAML -> `TemplateIR`, for `chant import`.
3
+ *
4
+ * A collector config is one document whose parts name each other: pipelines
5
+ * list component ids, a connector sits in two pipelines, `service.extensions`
6
+ * lists extension ids. So the IR carries the whole config as one resource of
7
+ * type `OTel::Collector`, and the generator lays out the modules itself, with
8
+ * those names turned into imports of declared entities. (Core splits an IR
9
+ * with more than three resources into one generate() call per category and
10
+ * keeps only the first file of each, which would lose the imports between a
11
+ * pipeline and the components it references.)
12
+ *
13
+ * The config is carried as parsed, with nothing dropped from a component's
14
+ * config. What the lexicon cannot express is named in `warnings`.
15
+ *
16
+ * The `# chant:` lines the serializer writes above a config are read back:
17
+ * a custom component's schema pin becomes the pin of the `defineComponent`
18
+ * the generator writes for it. A config without them imports against
19
+ * `COLLECTOR_PIN`.
20
+ */
21
+ import * as jsYaml from "js-yaml";
22
+ import { SEMCONV_VOCABULARIES } from "../semconv.js";
23
+ import { COMPONENT_KINDS, SECTION_OF, SIGNALS, canonicalComponentType, parseComponentId, pipelineSignal, } from "../model.js";
24
+ /** The IR resource type for a whole collector config. */
25
+ export const COLLECTOR_RESOURCE_TYPE = "OTel::Collector";
26
+ // YAML 1.2 core types plus `<<` merge keys. The default schema would also
27
+ // turn an unquoted date into a Date, which the collector never does.
28
+ // js-yaml exports its built-in types at runtime; @types/js-yaml does not declare them.
29
+ const MERGE_TYPE = jsYaml.types.merge;
30
+ const COLLECTOR_YAML_SCHEMA = jsYaml.CORE_SCHEMA.extend({ implicit: [MERGE_TYPE] });
31
+ const SECTIONS = new Set(["receivers", "processors", "exporters", "connectors", "extensions", "service"]);
32
+ const SERVICE_KEYS = new Set(["extensions", "telemetry", "pipelines"]);
33
+ const PIPELINE_KEYS = new Set(["receivers", "processors", "exporters"]);
34
+ const PIN_LINE = /^#\s*chant:\s*(receiver|processor|exporter|connector|extension)\s+(\S+)\s+schema\s+(\S+)@([^@\s]+)(?:\s+(\S+))?\s*$/;
35
+ const SEMCONV_LINE = /^#\s*chant:\s*semconv\s+(\S+)\s+(\S+)@([^@\s]+)(?:\s+([^\s(]+))?(?:\s+\(.*\))?\s*$/;
36
+ function isPlainObject(v) {
37
+ return typeof v === "object" && v !== null && !Array.isArray(v);
38
+ }
39
+ /** The `# chant:` lines of the comment block at the top of the file. */
40
+ function readHeader(content, warnings) {
41
+ const pins = [];
42
+ const semconv = [];
43
+ for (const raw of content.split(/\r?\n/)) {
44
+ const line = raw.trim();
45
+ if (line === "")
46
+ continue;
47
+ if (!line.startsWith("#"))
48
+ break;
49
+ if (!/^#\s*chant:/.test(line))
50
+ continue;
51
+ const pin = PIN_LINE.exec(line);
52
+ if (pin) {
53
+ const [, kind, id, source, version, digest] = pin;
54
+ if (source === "(unpinned)" || version === "(unpinned)")
55
+ continue;
56
+ pins.push({ kind: kind, id, pin: digest ? { source, version, digest } : { source, version } });
57
+ continue;
58
+ }
59
+ const sc = SEMCONV_LINE.exec(line);
60
+ if (sc) {
61
+ const [, namespace, source, version, digest] = sc;
62
+ semconv.push({ namespace, pin: digest ? { source, version, digest } : { source, version } });
63
+ const ours = SEMCONV_VOCABULARIES.find((v) => v.namespace === namespace)?.pin;
64
+ if (ours && (source !== ours.source || version !== ours.version)) {
65
+ warnings.push(`the config was built against ${namespace} semantic conventions ${source}@${version}; ` +
66
+ `this lexicon follows ${ours.source}@${ours.version}, which the rebuilt config will name`);
67
+ }
68
+ continue;
69
+ }
70
+ warnings.push(`header line "${line}" is not a schema pin chant recognises; it is not carried`);
71
+ }
72
+ return { pins, semconv };
73
+ }
74
+ /** Parse collector YAML into the plain config model, the header pins and what cannot be carried. */
75
+ export function parseCollectorYaml(content) {
76
+ const warnings = [];
77
+ const { pins, semconv } = readHeader(content, warnings);
78
+ const doc = content.trim() === "" ? {} : jsYaml.load(content, { schema: COLLECTOR_YAML_SCHEMA });
79
+ if (doc === null || doc === undefined)
80
+ return { config: {}, pins, semconv, warnings };
81
+ if (!isPlainObject(doc)) {
82
+ throw new Error("an OpenTelemetry Collector config is a YAML mapping; this document is not one");
83
+ }
84
+ const config = {};
85
+ for (const [key, value] of Object.entries(doc)) {
86
+ if (!SECTIONS.has(key)) {
87
+ warnings.push(`top-level section "${key}" is not part of a collector config; it is not carried`);
88
+ continue;
89
+ }
90
+ if (value === null || value === undefined)
91
+ continue;
92
+ if (!isPlainObject(value)) {
93
+ warnings.push(`"${key}" is not a mapping; it is not carried`);
94
+ continue;
95
+ }
96
+ if (key === "service")
97
+ continue;
98
+ const section = {};
99
+ for (const [id, cfg] of Object.entries(value)) {
100
+ const parsed = parseComponentId(id);
101
+ if (!parsed)
102
+ warnings.push(`${key}: "${id}" is not a type[/name] component id`);
103
+ const builtin = parsed && canonicalComponentType(KIND_OF_SECTION[key], parsed.type);
104
+ if (parsed && builtin && builtin !== parsed.type) {
105
+ warnings.push(`${key}.${id} uses "${parsed.type}", the collector's newer name for "${builtin}"; it imports as the built-in, which writes "${builtin}"`);
106
+ const renamed = parsed.name === undefined ? builtin : `${builtin}/${parsed.name}`;
107
+ if (renamed in value)
108
+ warnings.push(`${key}.${id} and ${key}.${renamed} become the same id "${renamed}"; rename one of them`);
109
+ }
110
+ if (cfg !== null && cfg !== undefined && !isPlainObject(cfg)) {
111
+ warnings.push(`${key}.${id} is not a mapping; it is carried as an empty config`);
112
+ section[id] = null;
113
+ continue;
114
+ }
115
+ if (isPlainObject(cfg) && "name" in cfg) {
116
+ warnings.push(`${key}.${id} has a config key "name", which chant uses for the instance name; the key is not carried`);
117
+ }
118
+ section[id] = cfg ?? null;
119
+ }
120
+ config[key] = section;
121
+ }
122
+ const service = doc.service;
123
+ if (isPlainObject(service)) {
124
+ config.service = {};
125
+ for (const [key, value] of Object.entries(service)) {
126
+ if (!SERVICE_KEYS.has(key)) {
127
+ warnings.push(`service.${key} is not carried; chant's Service declares extensions and telemetry`);
128
+ continue;
129
+ }
130
+ if (value === null || value === undefined)
131
+ continue;
132
+ if (key === "extensions") {
133
+ if (!Array.isArray(value))
134
+ warnings.push("service.extensions is not a list; it is not carried");
135
+ else
136
+ config.service.extensions = value.map(String);
137
+ }
138
+ else if (key === "telemetry") {
139
+ if (!isPlainObject(value))
140
+ warnings.push("service.telemetry is not a mapping; it is not carried");
141
+ else
142
+ config.service.telemetry = value;
143
+ }
144
+ else if (key === "pipelines") {
145
+ if (!isPlainObject(value)) {
146
+ warnings.push("service.pipelines is not a mapping; it is not carried");
147
+ continue;
148
+ }
149
+ config.service.pipelines = {};
150
+ for (const [pid, p] of Object.entries(value)) {
151
+ const signal = pipelineSignal(pid);
152
+ if (!SIGNALS.includes(signal)) {
153
+ warnings.push(`pipeline "${pid}" carries signal "${signal}", which Pipeline does not type; it is imported with a type suppression`);
154
+ }
155
+ const entry = {};
156
+ if (isPlainObject(p)) {
157
+ for (const [k, list] of Object.entries(p)) {
158
+ if (!PIPELINE_KEYS.has(k)) {
159
+ warnings.push(`service.pipelines.${pid}.${k} is not carried; a Pipeline has receivers, processors and exporters`);
160
+ continue;
161
+ }
162
+ if (list === null || list === undefined)
163
+ continue;
164
+ if (!Array.isArray(list)) {
165
+ warnings.push(`service.pipelines.${pid}.${k} is not a list; it is not carried`);
166
+ continue;
167
+ }
168
+ entry[k] = list.map(String);
169
+ }
170
+ }
171
+ config.service.pipelines[pid] = entry;
172
+ }
173
+ }
174
+ }
175
+ }
176
+ return { config, pins, semconv, warnings };
177
+ }
178
+ /** Which kind a section's components are. */
179
+ export const KIND_OF_SECTION = Object.fromEntries(COMPONENT_KINDS.map((k) => [SECTION_OF[k], k]));
180
+ /** The OpenTelemetry Collector config parser `chant import` runs. */
181
+ export class OtelCollectorParser {
182
+ parse(content) {
183
+ const parsed = parseCollectorYaml(content);
184
+ const properties = { config: parsed.config };
185
+ const metadata = { pins: parsed.pins, semconv: parsed.semconv };
186
+ return {
187
+ resources: [
188
+ {
189
+ logicalId: "collector",
190
+ type: COLLECTOR_RESOURCE_TYPE,
191
+ properties: properties,
192
+ metadata: metadata,
193
+ },
194
+ ],
195
+ parameters: [],
196
+ warnings: parsed.warnings,
197
+ };
198
+ }
199
+ }