@intentius/chant-lexicon-otel 0.95.0 → 0.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +30 -9
  2. package/dist/catalog.d.ts +2 -1
  3. package/dist/catalog.d.ts.map +1 -1
  4. package/dist/codegen/docs.d.ts.map +1 -1
  5. package/dist/collector.d.ts +4 -1
  6. package/dist/collector.d.ts.map +1 -1
  7. package/dist/components/connectors.d.ts +156 -0
  8. package/dist/components/connectors.d.ts.map +1 -0
  9. package/dist/components/filtering.d.ts +95 -0
  10. package/dist/components/filtering.d.ts.map +1 -0
  11. package/dist/components/index.d.ts +4 -0
  12. package/dist/components/index.d.ts.map +1 -1
  13. package/dist/components/k8s-receivers.d.ts +74 -0
  14. package/dist/components/k8s-receivers.d.ts.map +1 -0
  15. package/dist/components/sampling.d.ts +260 -0
  16. package/dist/components/sampling.d.ts.map +1 -0
  17. package/dist/define.d.ts +31 -4
  18. package/dist/define.d.ts.map +1 -1
  19. package/dist/genai.d.ts +165 -0
  20. package/dist/genai.d.ts.map +1 -0
  21. package/dist/index.d.ts +5 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/integrity.json +8 -7
  24. package/dist/lint/audit-catalog.d.ts +2 -1
  25. package/dist/lint/audit-catalog.d.ts.map +1 -1
  26. package/dist/lint/post-synth/index.d.ts.map +1 -1
  27. package/dist/lint/post-synth/otel-helpers.d.ts +1 -1
  28. package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
  29. package/dist/lint/post-synth/otel101.d.ts +1 -1
  30. package/dist/lint/post-synth/otel103.d.ts +1 -1
  31. package/dist/lint/post-synth/otel112.d.ts +8 -0
  32. package/dist/lint/post-synth/otel112.d.ts.map +1 -0
  33. package/dist/manifest.json +1 -1
  34. package/dist/meta.json +70 -0
  35. package/dist/metric-names.d.ts +121 -0
  36. package/dist/metric-names.d.ts.map +1 -0
  37. package/dist/model.d.ts +23 -4
  38. package/dist/model.d.ts.map +1 -1
  39. package/dist/okf/index.md +15 -0
  40. package/dist/okf/rules/OTEL112.md +11 -0
  41. package/dist/okf/types/CountConnector.md +9 -0
  42. package/dist/okf/types/FilterProcessor.md +9 -0
  43. package/dist/okf/types/ForwardConnector.md +9 -0
  44. package/dist/okf/types/K8sClusterReceiver.md +9 -0
  45. package/dist/okf/types/KubeletStatsReceiver.md +9 -0
  46. package/dist/okf/types/LoadBalancingExporter.md +9 -0
  47. package/dist/okf/types/ProbabilisticSamplerProcessor.md +9 -0
  48. package/dist/okf/types/RedactionProcessor.md +9 -0
  49. package/dist/okf/types/RoutingConnector.md +9 -0
  50. package/dist/okf/types/ServiceGraphConnector.md +9 -0
  51. package/dist/okf/types/SpanMetricsConnector.md +9 -0
  52. package/dist/okf/types/SumConnector.md +9 -0
  53. package/dist/okf/types/TailSamplingProcessor.md +9 -0
  54. package/dist/okf/types/TransformProcessor.md +9 -0
  55. package/dist/pipeline.d.ts +9 -3
  56. package/dist/pipeline.d.ts.map +1 -1
  57. package/dist/plugin.d.ts +1 -1
  58. package/dist/rules/otel-helpers.ts +1 -1
  59. package/dist/rules/otel101.ts +1 -1
  60. package/dist/rules/otel103.ts +1 -1
  61. package/dist/rules/otel112.ts +17 -0
  62. package/dist/semconv.d.ts +33 -0
  63. package/dist/semconv.d.ts.map +1 -0
  64. package/dist/serializer.d.ts +1 -1
  65. package/dist/skills/chant-otel.md +28 -5
  66. package/dist/topology.d.ts +34 -5
  67. package/dist/topology.d.ts.map +1 -1
  68. package/dist/validate-config.d.ts +6 -2
  69. package/dist/validate-config.d.ts.map +1 -1
  70. package/dist/validate.d.ts.map +1 -1
  71. package/package.json +3 -3
  72. package/src/catalog.ts +2 -1
  73. package/src/codegen/docs.ts +14 -8
  74. package/src/collector.ts +9 -1
  75. package/src/components/connectors.ts +276 -0
  76. package/src/components/filtering.test.ts +192 -0
  77. package/src/components/filtering.ts +192 -0
  78. package/src/components/index.ts +4 -0
  79. package/src/components/k8s-receivers.test.ts +123 -0
  80. package/src/components/k8s-receivers.ts +107 -0
  81. package/src/components/sampling.test.ts +464 -0
  82. package/src/components/sampling.ts +521 -0
  83. package/src/connectors.test.ts +388 -0
  84. package/src/define.ts +36 -4
  85. package/src/genai.test.ts +526 -0
  86. package/src/genai.ts +466 -0
  87. package/src/generated/lexicon-otel.json +70 -0
  88. package/src/index.ts +39 -0
  89. package/src/lint/audit-catalog.ts +11 -2
  90. package/src/lint/post-synth/index.ts +2 -0
  91. package/src/lint/post-synth/otel-helpers.ts +1 -1
  92. package/src/lint/post-synth/otel101.ts +1 -1
  93. package/src/lint/post-synth/otel103.ts +1 -1
  94. package/src/lint/post-synth/otel112.ts +17 -0
  95. package/src/metric-names.test.ts +80 -0
  96. package/src/metric-names.ts +176 -0
  97. package/src/model.ts +26 -4
  98. package/src/otelcol-validate.test.ts +143 -0
  99. package/src/pipeline.ts +9 -3
  100. package/src/plugin.test.ts +1 -0
  101. package/src/plugin.ts +1 -1
  102. package/src/semconv.ts +63 -0
  103. package/src/serializer.ts +1 -1
  104. package/src/skills/chant-otel.md +28 -5
  105. package/src/topology.ts +54 -8
  106. package/src/validate-config.ts +92 -4
  107. package/src/validate.ts +8 -0
@@ -6,7 +6,7 @@ user-invocable: true
6
6
 
7
7
  # OpenTelemetry Collector config with chant
8
8
 
9
- The otel lexicon (`@intentius/chant-lexicon-otel`) types collector config. Each receiver, processor, exporter and extension is an entity, pipelines reference them, and `chant build` writes one collector config file.
9
+ The otel lexicon (`@intentius/chant-lexicon-otel`) types collector config. Each receiver, processor, exporter, connector and extension is an entity, pipelines reference them, and `chant build` writes one collector config file.
10
10
 
11
11
  ## Project setup
12
12
 
@@ -40,13 +40,36 @@ Emits `receivers.otlp`, `processors.memory_limiter` and `processors.batch`, `exp
40
40
 
41
41
  | Kind | Types |
42
42
  |---|---|
43
- | receivers | otlp, prometheus, hostmetrics, filelog |
44
- | processors | batch, memory_limiter, resource, attributes, k8sattributes, resourcedetection |
45
- | exporters | otlp, otlphttp, debug, prometheus, googlecloud |
43
+ | receivers | otlp, prometheus, hostmetrics, filelog, k8s_cluster, kubeletstats |
44
+ | processors | batch, memory_limiter, resource, attributes, k8sattributes, resourcedetection, filter, transform, redaction, tail_sampling, probabilistic_sampler |
45
+ | exporters | otlp, otlphttp, debug, prometheus, googlecloud, loadbalancing |
46
+ | connectors | spanmetrics, servicegraph, routing, forward, count, sum |
46
47
  | extensions | health_check, pprof, zpages |
47
48
 
48
49
  Anything else goes through `defineComponent`; see the `chant-otel-custom-components` skill.
49
50
 
51
+ ## Connectors
52
+
53
+ A connector joins two pipelines. List the same entity in `exporters` of the pipeline that feeds it and in `receivers` of the pipeline it feeds:
54
+
55
+ ```ts
56
+ export const spanmetrics = new SpanMetricsConnector({ dimensions: [{ name: "http.route" }] });
57
+ export const traces = new Pipeline({ signal: "traces", receivers: [otlp], exporters: [backend, spanmetrics] });
58
+ export const red = new Pipeline({ signal: "metrics", name: "red", receivers: [spanmetrics], exporters: [prom] });
59
+ ```
60
+
61
+ `spanmetrics` and `servicegraph` turn traces into metrics, `count` and `sum` turn any signal into metrics (`sum` adds up a numeric attribute), `routing` and `forward` keep the signal. A connector on one side only fails OTEL101; a pipeline whose signal the connector can't pair fails OTEL112.
62
+
63
+ ## GenAI workloads
64
+
65
+ For services that emit GenAI spans, start from the preset instead of writing the processors by hand:
66
+
67
+ ```ts
68
+ export const collector = genAiPipeline({ traceExporters: [tempo], metricExporters: [prom] });
69
+ ```
70
+
71
+ It deletes prompt, completion, system-instruction and tool-call content from spans, span events and logs, and derives call, error, duration and token metrics (`genai_calls_total`, `genai_duration_seconds`, `genai_tokens_input_total`, `genai_tokens_output_total`) from every GenAI span before sampling. Keep content only when asked, with `keepContent: true`. `genAiComponents()` returns the pieces for hand-built pipelines.
72
+
50
73
  ## Rules
51
74
 
52
75
  - Reference components by entity where you can. A string id (`"otlp/backend"`) is allowed for a component declared elsewhere, and OTEL101 fails the build if nothing declares it.
@@ -57,4 +80,4 @@ Anything else goes through `defineComponent`; see the `chant-otel-custom-compone
57
80
 
58
81
  ## Reading the result
59
82
 
60
- `collectorTopologyOf(entities)` returns the pipelines, each component's endpoints and schema pin, and for each exporter the signals it carries, as plain data.
83
+ `collectorTopologyOf(entities)` returns the pipelines, each component's endpoints and schema pin, for each exporter the signals it carries, the connector `edges` between pipelines, and under `semconv` the semantic-conventions version (`GENAI_SEMCONV_PIN`) the config's `gen_ai.` keys follow, as plain data.
package/src/topology.ts CHANGED
@@ -3,16 +3,21 @@
3
3
  *
4
4
  * `collectorTopology()` reads a collector config (declared, or parsed from a
5
5
  * YAML file) and returns its pipelines, its components with their endpoints
6
- * and schema pins, and for each exporter the pipelines and signals it
7
- * carries. It is the surface a reader such as `chant workspace graph` uses to
8
- * say where a member's telemetry is sent, and what a declared telemetry
9
- * endpoint link can point at.
6
+ * and schema pins, for each exporter the pipelines and signals it carries,
7
+ * the edges connectors make from one pipeline to another, and the
8
+ * semantic-convention versions its attribute keys follow. It is the
9
+ * surface a reader such as `chant workspace graph` uses to say where a
10
+ * member's telemetry is sent, and what a declared telemetry endpoint link can
11
+ * point at.
10
12
  */
11
13
 
12
14
  import type { Declarable } from "@intentius/chant/declarable";
13
15
  import { buildCollectorConfig } from "./collector";
14
16
  import { definitionOf, type SchemaPin } from "./define";
15
17
  import { parseComponentId, pipelineSignal, SECTION_OF, type CollectorConfig, type ComponentKind } from "./model";
18
+ import { semconvUsage, type SemconvUsage } from "./semconv";
19
+
20
+ export type { SemconvUsage } from "./semconv";
16
21
 
17
22
  export interface TopologyPipeline {
18
23
  /** The id under `service.pipelines`, e.g. `traces` or `traces/backend`. */
@@ -36,10 +41,24 @@ export interface TopologyComponent {
36
41
  schema?: SchemaPin;
37
42
  /** Addresses it listens on or sends to, as the config states them. Empty when the config names none. */
38
43
  endpoints: string[];
39
- /** The pipelines that use it. For an extension, empty. */
44
+ /** The pipelines that use it. For a connector, those on either side; for an extension, empty. */
40
45
  pipelines: string[];
41
46
  }
42
47
 
48
+ /** One hop from a pipeline to another through a connector. */
49
+ export interface TopologyEdge {
50
+ /** The connector's id. */
51
+ connector: string;
52
+ /** The pipeline that lists the connector as an exporter. */
53
+ from: string;
54
+ /** That pipeline's signal. */
55
+ fromSignal: string;
56
+ /** The pipeline that lists the connector as a receiver. */
57
+ to: string;
58
+ /** That pipeline's signal. */
59
+ toSignal: string;
60
+ }
61
+
43
62
  export interface TopologyExporter {
44
63
  id: string;
45
64
  type: string;
@@ -54,6 +73,18 @@ export interface CollectorTopology {
54
73
  components: TopologyComponent[];
55
74
  /** The exporters again, grouped for the question "where does this telemetry go". */
56
75
  exporters: TopologyExporter[];
76
+ /**
77
+ * Pipeline-to-pipeline edges through connectors, one per (from, to) pair the
78
+ * connector supports. For a connector whose definition this process lacks,
79
+ * every pair.
80
+ */
81
+ edges: TopologyEdge[];
82
+ /**
83
+ * The semantic-convention vocabularies the config's attribute keys come
84
+ * from, each with the pin this package follows for it (`GENAI_SEMCONV_PIN`
85
+ * for `gen_ai`) and the components that use it. Empty when none is used.
86
+ */
87
+ semconv: SemconvUsage[];
57
88
  }
58
89
 
59
90
  function endpointsOf(kind: ComponentKind, type: string, config: Record<string, unknown> | null | undefined): string[] {
@@ -86,9 +117,12 @@ export function collectorTopology(config: CollectorConfig): CollectorTopology {
86
117
  const parsed = parseComponentId(id);
87
118
  const type = parsed?.type ?? id;
88
119
  const def = definitionOf(kind, type);
89
- const field = section as "receivers" | "processors" | "exporters" | "extensions";
90
120
  const inPipelines =
91
- field === "extensions" ? [] : pipelines.filter((p) => p[field].includes(id)).map((p) => p.id);
121
+ section === "extensions"
122
+ ? []
123
+ : section === "connectors"
124
+ ? pipelines.filter((p) => p.receivers.includes(id) || p.exporters.includes(id)).map((p) => p.id)
125
+ : pipelines.filter((p) => p[section].includes(id)).map((p) => p.id);
92
126
  components.push({
93
127
  id,
94
128
  kind,
@@ -113,7 +147,19 @@ export function collectorTopology(config: CollectorConfig): CollectorTopology {
113
147
  return { id: c.id, type: c.type, endpoints: c.endpoints, pipelines: c.pipelines, signals };
114
148
  });
115
149
 
116
- return { pipelines, components, exporters };
150
+ const edges: TopologyEdge[] = [];
151
+ for (const c of components) {
152
+ if (c.kind !== "connector") continue;
153
+ const pairs = definitionOf("connector", c.type)?.connects;
154
+ for (const from of pipelines.filter((p) => p.exporters.includes(c.id))) {
155
+ for (const to of pipelines.filter((p) => p.receivers.includes(c.id))) {
156
+ if (pairs && pairs.length > 0 && !pairs.some((pair) => pair.from === from.signal && pair.to === to.signal)) continue;
157
+ edges.push({ connector: c.id, from: from.id, fromSignal: from.signal, to: to.id, toSignal: to.signal });
158
+ }
159
+ }
160
+ }
161
+
162
+ return { pipelines, components, exporters, edges, semconv: semconvUsage(config) };
117
163
  }
118
164
 
119
165
  /** The topology of declared entities, i.e. of the config the serializer would emit for them. */
@@ -9,10 +9,12 @@
9
9
  */
10
10
 
11
11
  import type { Declarable } from "@intentius/chant/declarable";
12
- import { definitionFor, isOTelComponent, isUsablePin, runValidator } from "./define";
12
+ import { definitionFor, definitionOf, isOTelComponent, isUsablePin, runValidator } from "./define";
13
13
  import { componentConfig } from "./collector";
14
- import { isComponentId, pipelineSignal, SIGNALS, type CollectorConfig } from "./model";
14
+ import { isComponentId, parseComponentId, pipelineSignal, SIGNALS, type CollectorConfig, type ConnectorSignalPair } from "./model";
15
15
  import { isPipelineEntity } from "./pipeline";
16
+ // OTEL112 reads the built-in connectors' signal pairs from the registry.
17
+ import "./components/connectors";
16
18
 
17
19
  export type CollectorIssueCode =
18
20
  | "OTEL101"
@@ -23,7 +25,8 @@ export type CollectorIssueCode =
23
25
  | "OTEL106"
24
26
  | "OTEL107"
25
27
  | "OTEL108"
26
- | "OTEL109";
28
+ | "OTEL109"
29
+ | "OTEL112";
27
30
 
28
31
  export interface CollectorIssue {
29
32
  code: CollectorIssueCode;
@@ -41,7 +44,20 @@ function ids(section: Record<string, unknown> | undefined): Set<string> {
41
44
  return new Set(Object.keys(section ?? {}));
42
45
  }
43
46
 
44
- /** Check a collector config's references and pipeline shape (OTEL101-OTEL106). */
47
+ /** Where a connector appears: the pipelines it is an exporter in, and those it is a receiver in. */
48
+ interface ConnectorUse {
49
+ asExporter: string[];
50
+ asReceiver: string[];
51
+ }
52
+
53
+ function describePairs(pairs: ReadonlyArray<ConnectorSignalPair>): string {
54
+ return pairs.map((p) => `${p.from} to ${p.to}`).join(", ");
55
+ }
56
+
57
+ /**
58
+ * Check a collector config's references and pipeline shape (OTEL101-OTEL106),
59
+ * and each connector's signals against its definition (OTEL112).
60
+ */
45
61
  export function validateCollectorConfig(config: CollectorConfig): CollectorIssue[] {
46
62
  const issues: CollectorIssue[] = [];
47
63
  const receivers = ids(config.receivers);
@@ -52,6 +68,7 @@ export function validateCollectorConfig(config: CollectorConfig): CollectorIssue
52
68
  const pipelines = config.service?.pipelines ?? {};
53
69
 
54
70
  const used = { receivers: new Set<string>(), processors: new Set<string>(), exporters: new Set<string>() };
71
+ const connectorUse = new Map<string, ConnectorUse>();
55
72
 
56
73
  for (const [pipelineId, pipeline] of Object.entries(pipelines)) {
57
74
  const signal = pipelineSignal(pipelineId);
@@ -84,6 +101,11 @@ export function validateCollectorConfig(config: CollectorConfig): CollectorIssue
84
101
  continue;
85
102
  }
86
103
  const viaConnector = field !== "processors" && connectors.has(id);
104
+ if (viaConnector) {
105
+ const use = connectorUse.get(id) ?? { asExporter: [], asReceiver: [] };
106
+ (field === "exporters" ? use.asExporter : use.asReceiver).push(pipelineId);
107
+ connectorUse.set(id, use);
108
+ }
87
109
  if (!declared.has(id) && !viaConnector) {
88
110
  issues.push({
89
111
  code: "OTEL101",
@@ -124,10 +146,35 @@ export function validateCollectorConfig(config: CollectorConfig): CollectorIssue
124
146
  }
125
147
  }
126
148
 
149
+ // A connector joins two pipelines, so the collector refuses one that is
150
+ // listed on only one side.
151
+ for (const [id, use] of connectorUse) {
152
+ if (use.asReceiver.length === 0) {
153
+ issues.push({
154
+ code: "OTEL101",
155
+ severity: "error",
156
+ pipeline: use.asExporter[0],
157
+ component: id,
158
+ 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`,
159
+ });
160
+ } else if (use.asExporter.length === 0) {
161
+ issues.push({
162
+ code: "OTEL101",
163
+ severity: "error",
164
+ pipeline: use.asReceiver[0],
165
+ component: id,
166
+ 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`,
167
+ });
168
+ } else {
169
+ issues.push(...connectorSignalIssues(id, use));
170
+ }
171
+ }
172
+
127
173
  const unused: Array<[Set<string>, Set<string>, string]> = [
128
174
  [receivers, used.receivers, "receiver"],
129
175
  [processors, used.processors, "processor"],
130
176
  [exporters, used.exporters, "exporter"],
177
+ [connectors, new Set(connectorUse.keys()), "connector"],
131
178
  ];
132
179
  for (const [declared, usedIds, noun] of unused) {
133
180
  for (const id of declared) {
@@ -167,6 +214,47 @@ export function validateCollectorConfig(config: CollectorConfig): CollectorIssue
167
214
  return issues;
168
215
  }
169
216
 
217
+ /**
218
+ * OTEL112: every pipeline a connector joins must pair with a pipeline on the
219
+ * other side through a signal pair the connector supports. This is the
220
+ * collector's own rule: `spanmetrics` fed by a traces pipeline needs a metrics
221
+ * pipeline to receive from it, and cannot be the receiver of a traces
222
+ * pipeline. A connector whose definition this process doesn't have, or whose
223
+ * definition lists no pairs, is not checked.
224
+ */
225
+ function connectorSignalIssues(id: string, use: ConnectorUse): CollectorIssue[] {
226
+ const type = parseComponentId(id)?.type ?? id;
227
+ const pairs = definitionOf("connector", type)?.connects;
228
+ if (!pairs || pairs.length === 0) return [];
229
+ const supported = (from: string, to: string) => pairs.some((p) => p.from === from && p.to === to);
230
+ const inSignals = use.asExporter.map(pipelineSignal);
231
+ const outSignals = use.asReceiver.map(pipelineSignal);
232
+ const issues: CollectorIssue[] = [];
233
+ for (const pipeline of use.asExporter) {
234
+ const from = pipelineSignal(pipeline);
235
+ if (outSignals.some((to) => supported(from, to))) continue;
236
+ issues.push({
237
+ code: "OTEL112",
238
+ severity: "error",
239
+ pipeline,
240
+ component: id,
241
+ 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`,
242
+ });
243
+ }
244
+ for (const pipeline of use.asReceiver) {
245
+ const to = pipelineSignal(pipeline);
246
+ if (inSignals.some((from) => supported(from, to))) continue;
247
+ issues.push({
248
+ code: "OTEL112",
249
+ severity: "error",
250
+ pipeline,
251
+ component: id,
252
+ 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`,
253
+ });
254
+ }
255
+ return issues;
256
+ }
257
+
170
258
  /**
171
259
  * Check declared entities for what only the declaration knows (OTEL107-OTEL109):
172
260
  * each component's own config rules, duplicate ids, and custom components'
package/src/validate.ts CHANGED
@@ -18,17 +18,25 @@ export const REQUIRED_NAMES = [
18
18
  "PrometheusReceiver",
19
19
  "HostMetricsReceiver",
20
20
  "FileLogReceiver",
21
+ "K8sClusterReceiver",
22
+ "KubeletStatsReceiver",
21
23
  "BatchProcessor",
22
24
  "MemoryLimiterProcessor",
23
25
  "ResourceProcessor",
24
26
  "AttributesProcessor",
25
27
  "K8sAttributesProcessor",
26
28
  "ResourceDetectionProcessor",
29
+ "FilterProcessor",
30
+ "TransformProcessor",
31
+ "RedactionProcessor",
32
+ "TailSamplingProcessor",
33
+ "ProbabilisticSamplerProcessor",
27
34
  "OtlpExporter",
28
35
  "OtlpHttpExporter",
29
36
  "DebugExporter",
30
37
  "PrometheusExporter",
31
38
  "GoogleCloudExporter",
39
+ "LoadBalancingExporter",
32
40
  "HealthCheckExtension",
33
41
  "PprofExtension",
34
42
  "ZPagesExtension",