@intentius/chant-lexicon-otel 0.124.30 → 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,706 @@
1
+ /**
2
+ * Collector config checks, as plain functions over `CollectorConfig` and over
3
+ * declared entities.
4
+ *
5
+ * The post-synth checks in `lint/post-synth/` are thin wrappers around these,
6
+ * so the same rules can run anywhere a config exists: on a build's output, on
7
+ * a composite's generated YAML, or on a collector file parsed by some other
8
+ * tool.
9
+ */
10
+ import { definitionFor, definitionOf, isOTelComponent, isUsablePin, runValidator } from "./define.js";
11
+ import { componentConfig } from "./collector.js";
12
+ import { canonicalComponentType, isComponentId, parseComponentId, pipelineSignal, SIGNALS } from "./model.js";
13
+ import { isPipelineEntity } from "./pipeline.js";
14
+ import { signalToMetricsEntries } from "./components/connectors.js";
15
+ import { genAiCardinalityRisk } from "./genai.js";
16
+ import { collectorTopology } from "./topology.js";
17
+ import { configHygieneIssues } from "./config-hygiene.js";
18
+ // OTEL112 reads the built-in connectors' signal pairs from the registry.
19
+ import "./components/connectors.js";
20
+ const KNOWN_SIGNALS = new Set([...SIGNALS, "profiles"]);
21
+ function ids(section) {
22
+ return new Set(Object.keys(section ?? {}));
23
+ }
24
+ function describePairs(pairs) {
25
+ return pairs.map((p) => `${p.from} to ${p.to}`).join(", ");
26
+ }
27
+ /**
28
+ * Check a collector config's references and pipeline shape (OTEL101-OTEL106),
29
+ * each connector's signals against its definition (OTEL112), cycles through
30
+ * connectors (OTEL113), connector ids shared with a receiver or exporter
31
+ * (OTEL114), `routing` connector targets (OTEL115), the attributes
32
+ * connectors split metrics by (OTEL116), started components that listen
33
+ * on the same address (OTEL117), and the deprecation, exposure, delivery and
34
+ * name-list checks in `config-hygiene.ts` (OTEL119-OTEL127).
35
+ */
36
+ export function validateCollectorConfig(config) {
37
+ const issues = [];
38
+ const receivers = ids(config.receivers);
39
+ const processors = ids(config.processors);
40
+ const exporters = ids(config.exporters);
41
+ const extensions = ids(config.extensions);
42
+ const connectors = ids(config.connectors);
43
+ const pipelines = config.service?.pipelines ?? {};
44
+ const used = { receivers: new Set(), processors: new Set(), exporters: new Set() };
45
+ const connectorUse = new Map();
46
+ for (const [pipelineId, pipeline] of Object.entries(pipelines)) {
47
+ const signal = pipelineSignal(pipelineId);
48
+ if (!KNOWN_SIGNALS.has(signal) || !isComponentId(pipelineId)) {
49
+ issues.push({
50
+ code: "OTEL106",
51
+ severity: "error",
52
+ pipeline: pipelineId,
53
+ message: `pipeline "${pipelineId}" does not name a signal; a pipeline id is traces, metrics or logs, optionally followed by /name`,
54
+ });
55
+ }
56
+ const p = pipeline ?? {};
57
+ const lists = [
58
+ ["receivers", p.receivers, receivers, "receiver"],
59
+ ["processors", p.processors, processors, "processor"],
60
+ ["exporters", p.exporters, exporters, "exporter"],
61
+ ];
62
+ for (const [field, refs, declared, noun] of lists) {
63
+ for (const ref of refs ?? []) {
64
+ const id = String(ref);
65
+ used[field].add(id);
66
+ if (!isComponentId(id)) {
67
+ issues.push({
68
+ code: "OTEL106",
69
+ severity: "error",
70
+ pipeline: pipelineId,
71
+ component: id,
72
+ message: `pipeline "${pipelineId}" lists ${noun} "${id}", which is not a component id (type or type/name)`,
73
+ });
74
+ continue;
75
+ }
76
+ const viaConnector = field !== "processors" && connectors.has(id);
77
+ if (viaConnector) {
78
+ const use = connectorUse.get(id) ?? { asExporter: [], asReceiver: [] };
79
+ (field === "exporters" ? use.asExporter : use.asReceiver).push(pipelineId);
80
+ connectorUse.set(id, use);
81
+ }
82
+ if (!declared.has(id) && !viaConnector) {
83
+ issues.push({
84
+ code: "OTEL101",
85
+ severity: "error",
86
+ pipeline: pipelineId,
87
+ component: id,
88
+ message: `pipeline "${pipelineId}" uses ${noun} "${id}", which is not declared under ${field}; the collector refuses to start`,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ if ((p.receivers ?? []).length === 0) {
94
+ issues.push({
95
+ code: "OTEL102",
96
+ severity: "error",
97
+ pipeline: pipelineId,
98
+ message: `pipeline "${pipelineId}" has no receivers, so nothing enters it`,
99
+ });
100
+ }
101
+ if ((p.exporters ?? []).length === 0) {
102
+ issues.push({
103
+ code: "OTEL102",
104
+ severity: "error",
105
+ pipeline: pipelineId,
106
+ message: `pipeline "${pipelineId}" has no exporters, so what enters it goes nowhere`,
107
+ });
108
+ }
109
+ const procs = (p.processors ?? []).map(String);
110
+ const limiterAt = procs.findIndex((id) => id === "memory_limiter" || id.startsWith("memory_limiter/"));
111
+ if (limiterAt > 0) {
112
+ issues.push({
113
+ code: "OTEL105",
114
+ severity: "warning",
115
+ pipeline: pipelineId,
116
+ component: procs[limiterAt],
117
+ message: `pipeline "${pipelineId}" runs ${procs[limiterAt]} at position ${limiterAt + 1}; put it first so it can refuse data before other processors buffer it`,
118
+ });
119
+ }
120
+ }
121
+ // A connector joins two pipelines, so the collector refuses one that is
122
+ // listed on only one side.
123
+ for (const [id, use] of connectorUse) {
124
+ if (use.asReceiver.length === 0) {
125
+ issues.push({
126
+ code: "OTEL101",
127
+ severity: "error",
128
+ pipeline: use.asExporter[0],
129
+ component: id,
130
+ message: `connector "${id}" is an exporter in pipeline "${use.asExporter[0]}" but no pipeline lists it as a receiver; a connector must appear on both sides, and the collector refuses to start`,
131
+ });
132
+ }
133
+ else if (use.asExporter.length === 0) {
134
+ issues.push({
135
+ code: "OTEL101",
136
+ severity: "error",
137
+ pipeline: use.asReceiver[0],
138
+ component: id,
139
+ message: `connector "${id}" is a receiver in pipeline "${use.asReceiver[0]}" but no pipeline lists it as an exporter; a connector must appear on both sides, and the collector refuses to start`,
140
+ });
141
+ }
142
+ else {
143
+ issues.push(...connectorSignalIssues(id, use));
144
+ }
145
+ }
146
+ const unused = [
147
+ [receivers, used.receivers, "receiver"],
148
+ [processors, used.processors, "processor"],
149
+ [exporters, used.exporters, "exporter"],
150
+ [connectors, new Set(connectorUse.keys()), "connector"],
151
+ ];
152
+ for (const [declared, usedIds, noun] of unused) {
153
+ for (const id of declared) {
154
+ if (!usedIds.has(id)) {
155
+ issues.push({
156
+ code: "OTEL103",
157
+ severity: "warning",
158
+ component: id,
159
+ message: `${noun} "${id}" is declared but no pipeline uses it; the collector ignores it`,
160
+ });
161
+ }
162
+ }
163
+ }
164
+ const enabled = new Set((config.service?.extensions ?? []).map(String));
165
+ for (const id of enabled) {
166
+ if (!extensions.has(id)) {
167
+ issues.push({
168
+ code: "OTEL104",
169
+ severity: "error",
170
+ component: id,
171
+ message: `service.extensions enables "${id}", which is not declared under extensions; the collector refuses to start`,
172
+ });
173
+ }
174
+ }
175
+ for (const id of extensions) {
176
+ if (!enabled.has(id)) {
177
+ issues.push({
178
+ code: "OTEL103",
179
+ severity: "warning",
180
+ component: id,
181
+ message: `extension "${id}" is declared but not listed in service.extensions, so it never starts`,
182
+ });
183
+ }
184
+ }
185
+ issues.push(...connectorCycleIssues(config), ...connectorIdIssues(config), ...routingTargetIssues(config), ...metricAttributeIssues(config), ...listenerIssues(config), ...configHygieneIssues(config));
186
+ return issues;
187
+ }
188
+ /**
189
+ * OTEL113: the pipelines and the connectors between them must form a graph
190
+ * with no cycle, or the collector refuses to start. Reads the edges
191
+ * `collectorTopology()` returns, so an edge counts only for a signal pair the
192
+ * connector supports, as in the collector's own graph. Reports each strongly
193
+ * connected group of pipelines once, with one cycle through it.
194
+ */
195
+ function connectorCycleIssues(config) {
196
+ const { pipelines, edges } = collectorTopology(config);
197
+ const out = new Map();
198
+ for (const e of edges)
199
+ out.set(e.from, [...(out.get(e.from) ?? []), e]);
200
+ // Tarjan's strongly connected components, in pipeline order.
201
+ const order = pipelines.map((p) => p.id);
202
+ const index = new Map();
203
+ const low = new Map();
204
+ const stack = [];
205
+ const onStack = new Set();
206
+ const groups = [];
207
+ const visit = (v) => {
208
+ index.set(v, index.size);
209
+ low.set(v, index.get(v));
210
+ stack.push(v);
211
+ onStack.add(v);
212
+ for (const { to } of out.get(v) ?? []) {
213
+ if (!index.has(to)) {
214
+ visit(to);
215
+ low.set(v, Math.min(low.get(v), low.get(to)));
216
+ }
217
+ else if (onStack.has(to)) {
218
+ low.set(v, Math.min(low.get(v), index.get(to)));
219
+ }
220
+ }
221
+ if (low.get(v) === index.get(v)) {
222
+ const group = [];
223
+ let w;
224
+ do {
225
+ w = stack.pop();
226
+ onStack.delete(w);
227
+ group.push(w);
228
+ } while (w !== v);
229
+ groups.push(group);
230
+ }
231
+ };
232
+ for (const id of order)
233
+ if (!index.has(id))
234
+ visit(id);
235
+ const issues = [];
236
+ for (const group of groups) {
237
+ const members = new Set(group);
238
+ const start = order.find((id) => members.has(id));
239
+ const path = shortestCycle(start, members, out);
240
+ if (!path)
241
+ continue; // a single pipeline with no edge to itself
242
+ const hops = path.map((e) => `${e.from} -> ${e.connector}`).join(" -> ");
243
+ issues.push({
244
+ code: "OTEL113",
245
+ severity: "error",
246
+ pipeline: start,
247
+ message: `pipelines form a cycle through connectors: ${hops} -> ${start}; the collector refuses to start`,
248
+ });
249
+ }
250
+ return issues;
251
+ }
252
+ /** The shortest path of edges from `start` back to itself, staying inside `members`. */
253
+ function shortestCycle(start, members, out) {
254
+ const via = new Map();
255
+ const queue = [start];
256
+ while (queue.length > 0) {
257
+ const v = queue.shift();
258
+ for (const e of out.get(v) ?? []) {
259
+ if (!members.has(e.to))
260
+ continue;
261
+ if (e.to === start) {
262
+ const path = [e];
263
+ for (let at = v; at !== start; at = via.get(at).from)
264
+ path.unshift(via.get(at));
265
+ return path;
266
+ }
267
+ if (!via.has(e.to)) {
268
+ via.set(e.to, e);
269
+ queue.push(e.to);
270
+ }
271
+ }
272
+ }
273
+ return undefined;
274
+ }
275
+ /**
276
+ * OTEL114: a pipeline names a connector the same way it names a receiver or
277
+ * an exporter, so the collector refuses a connector id that is also declared
278
+ * under receivers or exporters, used or not.
279
+ */
280
+ function connectorIdIssues(config) {
281
+ const receivers = ids(config.receivers);
282
+ const exporters = ids(config.exporters);
283
+ const issues = [];
284
+ for (const id of ids(config.connectors)) {
285
+ const clashes = [receivers.has(id) ? "receiver" : "", exporters.has(id) ? "exporter" : ""].filter(Boolean);
286
+ if (clashes.length === 0)
287
+ continue;
288
+ const type = parseComponentId(id)?.type ?? id;
289
+ issues.push({
290
+ code: "OTEL114",
291
+ severity: "error",
292
+ component: id,
293
+ message: `connector "${id}" has the same id as a declared ${clashes.join(" and ")}, so a pipeline listing "${id}" is ambiguous and the collector refuses to start; rename one of them (e.g. "${type}/connector")`,
294
+ });
295
+ }
296
+ return issues;
297
+ }
298
+ /**
299
+ * OTEL115: each pipeline a `routing` connector routes to, in `table[].pipelines`
300
+ * or `default_pipelines`, must list that connector in its receivers. The
301
+ * connector can only hand data to the pipelines it feeds, and the collector
302
+ * refuses to start when a route names any other.
303
+ */
304
+ function routingTargetIssues(config) {
305
+ const pipelines = config.service?.pipelines ?? {};
306
+ const issues = [];
307
+ for (const [id, raw] of Object.entries(config.connectors ?? {})) {
308
+ if (parseComponentId(id)?.type !== "routing")
309
+ continue;
310
+ const cfg = (raw ?? {});
311
+ const targets = [];
312
+ (Array.isArray(cfg.table) ? cfg.table : []).forEach((item, i) => {
313
+ for (const t of Array.isArray(item?.pipelines) ? item.pipelines : [])
314
+ targets.push([String(t), `table[${i}].pipelines`]);
315
+ });
316
+ for (const t of Array.isArray(cfg.default_pipelines) ? cfg.default_pipelines : [])
317
+ targets.push([String(t), "default_pipelines"]);
318
+ const reported = new Set();
319
+ for (const [target, where] of targets) {
320
+ if (reported.has(target))
321
+ continue;
322
+ const pipeline = Object.prototype.hasOwnProperty.call(pipelines, target) ? pipelines[target] : undefined;
323
+ if (pipeline && (pipeline.receivers ?? []).map(String).includes(id))
324
+ continue;
325
+ reported.add(target);
326
+ issues.push({
327
+ code: "OTEL115",
328
+ severity: "error",
329
+ pipeline: target,
330
+ component: id,
331
+ message: pipeline
332
+ ? `routing connector "${id}" routes to pipeline "${target}" (${where}), which does not list "${id}" in its receivers; the collector refuses to start`
333
+ : `routing connector "${id}" routes to pipeline "${target}" (${where}), which is not declared under service.pipelines; the collector refuses to start`,
334
+ });
335
+ }
336
+ }
337
+ return issues;
338
+ }
339
+ function list(value) {
340
+ return Array.isArray(value) ? value : [];
341
+ }
342
+ function keysOf(items, prop, field) {
343
+ return list(items).flatMap((item) => {
344
+ const key = typeof item === "string" ? item : item?.[prop];
345
+ return typeof key === "string" ? [{ key, field }] : [];
346
+ });
347
+ }
348
+ /**
349
+ * The attribute keys a built-in metrics connector puts on the metrics it
350
+ * makes, by type. An empty or unset `include_resource_attributes` keeps
351
+ * every resource attribute, whichever they are, so only listed keys can be
352
+ * named.
353
+ */
354
+ function metricAttributeUses(type, config) {
355
+ switch (type) {
356
+ case "spanmetrics": {
357
+ const histogram = config.histogram;
358
+ const events = config.events;
359
+ return [
360
+ ...keysOf(config.dimensions, "name", "dimensions"),
361
+ ...keysOf(config.calls_dimensions, "name", "calls_dimensions"),
362
+ ...keysOf(histogram?.dimensions, "name", "histogram.dimensions"),
363
+ ...keysOf(events?.dimensions, "name", "events.dimensions"),
364
+ ];
365
+ }
366
+ case "servicegraph":
367
+ return keysOf(config.dimensions, "name", "dimensions");
368
+ case "count":
369
+ case "sum":
370
+ return Object.entries(config).flatMap(([section, metrics]) => typeof metrics === "object" && metrics !== null && !Array.isArray(metrics)
371
+ ? Object.entries(metrics).flatMap(([name, info]) => keysOf(info?.attributes, "key", `${section}.${name}.attributes`))
372
+ : []);
373
+ case "signaltometrics":
374
+ return signalToMetricsEntries(config).flatMap(({ signal, index, metric }) => {
375
+ const at = metric.name ? `${signal}[${index}] (${metric.name})` : `${signal}[${index}]`;
376
+ return [
377
+ ...keysOf(metric.attributes, "key", `${at}.attributes`),
378
+ ...keysOf(metric.include_resource_attributes, "key", `${at}.include_resource_attributes`),
379
+ ];
380
+ });
381
+ default:
382
+ return [];
383
+ }
384
+ }
385
+ /**
386
+ * OTEL116: a connector splits metrics by a GenAI attribute that takes a new
387
+ * value per response, tool call, conversation, session or user, or by a
388
+ * content attribute. Each value starts new time series, so the metric's
389
+ * series grow with traffic. The keys are `GENAI_HIGH_CARDINALITY_ATTRIBUTES`
390
+ * and the content keys in `genai.ts`.
391
+ */
392
+ function metricAttributeIssues(config) {
393
+ const issues = [];
394
+ for (const [id, raw] of Object.entries(config.connectors ?? {})) {
395
+ const type = canonicalComponentType("connector", parseComponentId(id)?.type ?? id);
396
+ const body = typeof raw === "object" && raw !== null ? raw : {};
397
+ for (const { key, field } of metricAttributeUses(type, body)) {
398
+ const risk = genAiCardinalityRisk(key);
399
+ if (!risk)
400
+ continue;
401
+ const why = risk === "content"
402
+ ? "it carries message content, which is unbounded and sensitive"
403
+ : "it takes a new value per request, conversation or user";
404
+ issues.push({
405
+ code: "OTEL116",
406
+ severity: "warning",
407
+ component: id,
408
+ message: `connector "${id}" splits metrics by "${key}" (${field}); ${why}, so every value starts new time series. Drop it from the metric's attributes, or keep it on spans and logs`,
409
+ });
410
+ }
411
+ }
412
+ return issues;
413
+ }
414
+ const LISTENERS = {
415
+ receiver: {
416
+ otlp: [
417
+ { path: ["protocols", "grpc", "endpoint"], default: "localhost:4317", present: true },
418
+ { path: ["protocols", "http", "endpoint"], default: "localhost:4318", present: true },
419
+ ],
420
+ zipkin: [{ path: ["endpoint"], default: "localhost:9411" }],
421
+ jaeger: [
422
+ { path: ["protocols", "grpc", "endpoint"], default: "localhost:14250", present: true },
423
+ { path: ["protocols", "thrift_http", "endpoint"], default: "localhost:14268", present: true },
424
+ { path: ["protocols", "thrift_binary", "endpoint"], default: "localhost:6832", present: true, transport: "udp" },
425
+ { path: ["protocols", "thrift_compact", "endpoint"], default: "localhost:6831", present: true, transport: "udp" },
426
+ ],
427
+ opencensus: [{ path: ["endpoint"], default: "localhost:55678" }],
428
+ otelarrow: [{ path: ["protocols", "grpc", "endpoint"], default: "0.0.0.0:4317" }],
429
+ skywalking: [
430
+ { path: ["protocols", "grpc", "endpoint"], default: "localhost:11800", present: true },
431
+ { path: ["protocols", "http", "endpoint"], default: "localhost:12800", present: true },
432
+ ],
433
+ loki: [
434
+ { path: ["protocols", "grpc", "endpoint"], default: "localhost:3600", present: true },
435
+ { path: ["protocols", "http", "endpoint"], default: "localhost:3500", present: true },
436
+ ],
437
+ statsd: [{ path: ["endpoint"], default: "localhost:8125", transport: "udp", transportKey: "transport" }],
438
+ carbon: [{ path: ["endpoint"], default: "localhost:2003", transportKey: "transport" }],
439
+ awsxray: [{ path: ["endpoint"], default: "localhost:2000", transport: "udp", transportKey: "transport" }],
440
+ collectd: [{ path: ["endpoint"], default: "localhost:8081" }],
441
+ datadog: [{ path: ["endpoint"], default: "localhost:8126" }],
442
+ influxdb: [{ path: ["endpoint"], default: "localhost:8086" }],
443
+ signalfx: [{ path: ["endpoint"], default: "localhost:9943" }],
444
+ splunk_hec: [{ path: ["endpoint"], default: "localhost:8088" }],
445
+ sapm: [{ path: ["endpoint"], default: "localhost:7276" }],
446
+ wavefront: [{ path: ["endpoint"], default: "localhost:2003" }],
447
+ prometheusremotewrite: [{ path: ["endpoint"], default: "localhost:9090" }],
448
+ awsfirehose: [{ path: ["endpoint"], default: "localhost:4433" }],
449
+ faro: [{ path: ["endpoint"], default: "localhost:8080" }],
450
+ libhoney: [{ path: ["http", "endpoint"], default: "localhost:8080" }],
451
+ fluentforward: [{ path: ["endpoint"] }],
452
+ webhookevent: [{ path: ["endpoint"] }],
453
+ tcplog: [{ path: ["listen_address"] }],
454
+ udplog: [{ path: ["listen_address"], transport: "udp" }],
455
+ syslog: [
456
+ { path: ["tcp", "listen_address"], present: true },
457
+ { path: ["udp", "listen_address"], present: true, transport: "udp" },
458
+ ],
459
+ },
460
+ exporter: {
461
+ prometheus: [{ path: ["endpoint"] }],
462
+ },
463
+ extension: {
464
+ health_check: [{ path: ["endpoint"], default: "localhost:13133" }],
465
+ zpages: [{ path: ["endpoint"], default: "localhost:55679" }],
466
+ pprof: [{ path: ["endpoint"], default: "localhost:1777" }],
467
+ },
468
+ };
469
+ /** The transport a spec listens with in this component config: its `transportKey` value, else its default. */
470
+ function specTransport(spec, body) {
471
+ if (spec.transportKey && typeof body === "object" && body !== null) {
472
+ const value = body[spec.transportKey];
473
+ if (typeof value === "string" && /^udp[46]?$/.test(value))
474
+ return "udp";
475
+ if (typeof value === "string" && /^tcp[46]?$/.test(value))
476
+ return "tcp";
477
+ }
478
+ return spec.transport ?? "tcp";
479
+ }
480
+ /**
481
+ * The config key paths to the addresses a receiver or exporter of this type
482
+ * listens on, from the table OTEL117 checks, with the transport each listens
483
+ * with in `body`. Empty for a type not in the table, whose `endpoint` may be
484
+ * a server it connects to. `collectorEndpoints` reads the ports from here, so
485
+ * the ports a platform composite publishes and the listeners OTEL117 compares
486
+ * are the same set.
487
+ */
488
+ export function listenerPaths(kind, type, body) {
489
+ return (LISTENERS[kind][type] ?? []).map((spec) => ({ path: spec.path, transport: specTransport(spec, body) }));
490
+ }
491
+ /** The collector's own metrics endpoint when `service.telemetry.metrics` names no reader. */
492
+ const DEFAULT_TELEMETRY_METRICS = { host: "localhost", port: 8888 };
493
+ const WILDCARD_HOSTS = new Set(["", "0.0.0.0", "::", "[::]"]);
494
+ function splitAddress(address) {
495
+ if (typeof address !== "string")
496
+ return undefined;
497
+ const m = address.match(/^(.*):(\d+)$/);
498
+ if (!m)
499
+ return undefined;
500
+ return { host: m[1], port: Number(m[2]) };
501
+ }
502
+ function at(value, path) {
503
+ let cur = value;
504
+ for (const key of path) {
505
+ if (typeof cur !== "object" || cur === null || !(key in cur))
506
+ return { found: false, value: undefined };
507
+ cur = cur[key];
508
+ }
509
+ return { found: true, value: cur };
510
+ }
511
+ /** The addresses a component of this kind listens on, from the OTEL117 table and its defaults. */
512
+ export function componentListeners(kind, id, body) {
513
+ const type = parseComponentId(id)?.type ?? id;
514
+ const out = [];
515
+ for (const spec of LISTENERS[kind][type] ?? []) {
516
+ if (spec.present && !at(body, spec.path.slice(0, -1)).found)
517
+ continue;
518
+ const { found, value } = at(body, spec.path);
519
+ const address = splitAddress(found && value !== undefined && value !== null ? value : spec.default);
520
+ if (!address)
521
+ continue;
522
+ out.push({ owner: `${kind} "${id}" (${spec.path.join(".")})`, component: id, ...address, transport: specTransport(spec, body) });
523
+ }
524
+ return out;
525
+ }
526
+ /** The collector's own metrics endpoints, from `service.telemetry.metrics`. */
527
+ function telemetryListeners(config) {
528
+ const metrics = config.service?.telemetry?.metrics;
529
+ if (metrics?.level === "none")
530
+ return [];
531
+ const readers = Array.isArray(metrics?.readers) ? metrics.readers : undefined;
532
+ if (!readers)
533
+ return [{ owner: "the collector's own metrics (service.telemetry.metrics, by default)", ...DEFAULT_TELEMETRY_METRICS, transport: "tcp" }];
534
+ const out = [];
535
+ readers.forEach((reader, i) => {
536
+ const prom = at(reader, ["pull", "exporter", "prometheus"]);
537
+ if (!prom.found || typeof prom.value !== "object" || prom.value === null)
538
+ return;
539
+ const { host, port } = prom.value;
540
+ const portNumber = typeof port === "number" ? port : typeof port === "string" && /^\d+$/.test(port) ? Number(port) : undefined;
541
+ if (portNumber === undefined)
542
+ return;
543
+ out.push({
544
+ owner: `the collector's own metrics (service.telemetry.metrics.readers[${i}])`,
545
+ host: typeof host === "string" ? host : "localhost",
546
+ port: portNumber,
547
+ transport: "tcp",
548
+ });
549
+ });
550
+ return out;
551
+ }
552
+ function sameAddress(a, b) {
553
+ if (a.port !== b.port || a.transport !== b.transport)
554
+ return false;
555
+ return a.host === b.host || WILDCARD_HOSTS.has(a.host) || WILDCARD_HOSTS.has(b.host);
556
+ }
557
+ /**
558
+ * OTEL117: two things the collector starts listen on the same address, so
559
+ * the second bind fails with "address already in use" and the collector
560
+ * exits. `otelcol validate` builds the config without binding, so it does
561
+ * not catch this. Started means: a receiver or exporter some pipeline lists,
562
+ * an extension in `service.extensions`, and the collector's own metrics
563
+ * endpoint. A wildcard host (`0.0.0.0`, `::`, empty) overlaps every host on
564
+ * its port; an `${env:...}` host is compared as written.
565
+ */
566
+ function listenerIssues(config) {
567
+ const pipelines = Object.values(config.service?.pipelines ?? {});
568
+ const started = (refs) => new Set(refs.flatMap((r) => (r ?? []).map(String)));
569
+ const receivers = started(pipelines.map((p) => p?.receivers));
570
+ const exporters = started(pipelines.map((p) => p?.exporters));
571
+ const extensions = new Set((config.service?.extensions ?? []).map(String));
572
+ const listeners = [];
573
+ const sections = [
574
+ ["receiver", config.receivers, receivers],
575
+ ["exporter", config.exporters, exporters],
576
+ ["extension", config.extensions, extensions],
577
+ ];
578
+ for (const [kind, section, startedIds] of sections) {
579
+ for (const [id, body] of Object.entries(section ?? {})) {
580
+ if (startedIds.has(id))
581
+ listeners.push(...componentListeners(kind, id, body));
582
+ }
583
+ }
584
+ listeners.push(...telemetryListeners(config));
585
+ const issues = [];
586
+ for (let i = 0; i < listeners.length; i++) {
587
+ for (let j = 0; j < i; j++) {
588
+ const [first, second] = [listeners[j], listeners[i]];
589
+ if (!sameAddress(first, second))
590
+ continue;
591
+ const udp = first.transport === "udp" ? " (UDP)" : "";
592
+ issues.push({
593
+ code: "OTEL117",
594
+ severity: "error",
595
+ ...(second.component ? { component: second.component } : first.component ? { component: first.component } : {}),
596
+ message: `${first.owner} listens on ${first.host}:${first.port}${udp} and ${second.owner} on ${second.host}:${second.port}${udp}; the second bind fails with "address already in use" and the collector exits. Move one of them to another port`,
597
+ });
598
+ }
599
+ }
600
+ return issues;
601
+ }
602
+ /**
603
+ * OTEL112: every pipeline a connector joins must pair with a pipeline on the
604
+ * other side through a signal pair the connector supports. This is the
605
+ * collector's own rule: `spanmetrics` fed by a traces pipeline needs a metrics
606
+ * pipeline to receive from it, and cannot be the receiver of a traces
607
+ * pipeline. A connector whose definition this process doesn't have, or whose
608
+ * definition lists no pairs, is not checked.
609
+ */
610
+ function connectorSignalIssues(id, use) {
611
+ const type = parseComponentId(id)?.type ?? id;
612
+ const pairs = definitionOf("connector", type)?.connects;
613
+ if (!pairs || pairs.length === 0)
614
+ return [];
615
+ const supported = (from, to) => pairs.some((p) => p.from === from && p.to === to);
616
+ const inSignals = use.asExporter.map(pipelineSignal);
617
+ const outSignals = use.asReceiver.map(pipelineSignal);
618
+ const issues = [];
619
+ for (const pipeline of use.asExporter) {
620
+ const from = pipelineSignal(pipeline);
621
+ if (outSignals.some((to) => supported(from, to)))
622
+ continue;
623
+ issues.push({
624
+ code: "OTEL112",
625
+ severity: "error",
626
+ pipeline,
627
+ component: id,
628
+ message: `connector "${id}" is an exporter in ${from} pipeline "${pipeline}", but no pipeline it feeds carries a signal ${type} makes from ${from} (it supports ${describePairs(pairs)}); the collector refuses to start`,
629
+ });
630
+ }
631
+ for (const pipeline of use.asReceiver) {
632
+ const to = pipelineSignal(pipeline);
633
+ if (inSignals.some((from) => supported(from, to)))
634
+ continue;
635
+ issues.push({
636
+ code: "OTEL112",
637
+ severity: "error",
638
+ pipeline,
639
+ component: id,
640
+ message: `connector "${id}" is a receiver in ${to} pipeline "${pipeline}", but no pipeline feeding it carries a signal ${type} turns into ${to} (it supports ${describePairs(pairs)}); the collector refuses to start`,
641
+ });
642
+ }
643
+ return issues;
644
+ }
645
+ /**
646
+ * Check declared entities for what only the declaration knows (OTEL107-OTEL109):
647
+ * each component's own config rules, duplicate ids, and custom components'
648
+ * schema pins.
649
+ */
650
+ export function validateCollectorEntities(entities) {
651
+ const list = entities instanceof Map ? [...entities.values()] : [...entities];
652
+ const issues = [];
653
+ const seen = new Map();
654
+ const pipelineIds = new Map();
655
+ for (const e of list) {
656
+ if (isPipelineEntity(e)) {
657
+ pipelineIds.set(e.pipelineId, (pipelineIds.get(e.pipelineId) ?? 0) + 1);
658
+ continue;
659
+ }
660
+ if (!isOTelComponent(e))
661
+ continue;
662
+ const key = `${e.componentKind} ${e.componentId}`;
663
+ seen.set(key, (seen.get(key) ?? 0) + 1);
664
+ const def = definitionFor(e.entityType);
665
+ if (!def)
666
+ continue;
667
+ if (!def.builtin && !isUsablePin(def.pin)) {
668
+ issues.push({
669
+ code: "OTEL109",
670
+ severity: "error",
671
+ component: e.componentId,
672
+ message: `custom ${e.componentKind} "${e.componentId}" has no schema pin; give defineComponent a pin with a source and a version`,
673
+ });
674
+ }
675
+ for (const problem of runValidator(def.validate, componentConfig(e))) {
676
+ issues.push({
677
+ code: "OTEL107",
678
+ severity: "error",
679
+ component: e.componentId,
680
+ message: `${e.componentKind} "${e.componentId}": ${problem}`,
681
+ });
682
+ }
683
+ }
684
+ for (const [key, count] of seen) {
685
+ if (count > 1) {
686
+ const [kind, id] = key.split(" ");
687
+ issues.push({
688
+ code: "OTEL108",
689
+ severity: "error",
690
+ component: id,
691
+ message: `${count} ${kind}s declare the id "${id}"; give each a distinct name`,
692
+ });
693
+ }
694
+ }
695
+ for (const [id, count] of pipelineIds) {
696
+ if (count > 1) {
697
+ issues.push({
698
+ code: "OTEL108",
699
+ severity: "error",
700
+ pipeline: id,
701
+ message: `${count} pipelines declare the id "${id}"; give each a distinct name`,
702
+ });
703
+ }
704
+ }
705
+ return issues;
706
+ }