@intentius/chant-lexicon-otel 0.95.1 → 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.
- package/README.md +30 -9
- package/dist/catalog.d.ts +2 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/collector.d.ts +4 -1
- package/dist/collector.d.ts.map +1 -1
- package/dist/components/connectors.d.ts +156 -0
- package/dist/components/connectors.d.ts.map +1 -0
- package/dist/components/filtering.d.ts +95 -0
- package/dist/components/filtering.d.ts.map +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/k8s-receivers.d.ts +74 -0
- package/dist/components/k8s-receivers.d.ts.map +1 -0
- package/dist/components/sampling.d.ts +260 -0
- package/dist/components/sampling.d.ts.map +1 -0
- package/dist/define.d.ts +31 -4
- package/dist/define.d.ts.map +1 -1
- package/dist/genai.d.ts +165 -0
- package/dist/genai.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +8 -7
- package/dist/lint/audit-catalog.d.ts +2 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/otel-helpers.d.ts +1 -1
- package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/otel101.d.ts +1 -1
- package/dist/lint/post-synth/otel103.d.ts +1 -1
- package/dist/lint/post-synth/otel112.d.ts +8 -0
- package/dist/lint/post-synth/otel112.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +70 -0
- package/dist/metric-names.d.ts +121 -0
- package/dist/metric-names.d.ts.map +1 -0
- package/dist/model.d.ts +23 -4
- package/dist/model.d.ts.map +1 -1
- package/dist/okf/index.md +15 -0
- package/dist/okf/rules/OTEL112.md +11 -0
- package/dist/okf/types/CountConnector.md +9 -0
- package/dist/okf/types/FilterProcessor.md +9 -0
- package/dist/okf/types/ForwardConnector.md +9 -0
- package/dist/okf/types/K8sClusterReceiver.md +9 -0
- package/dist/okf/types/KubeletStatsReceiver.md +9 -0
- package/dist/okf/types/LoadBalancingExporter.md +9 -0
- package/dist/okf/types/ProbabilisticSamplerProcessor.md +9 -0
- package/dist/okf/types/RedactionProcessor.md +9 -0
- package/dist/okf/types/RoutingConnector.md +9 -0
- package/dist/okf/types/ServiceGraphConnector.md +9 -0
- package/dist/okf/types/SpanMetricsConnector.md +9 -0
- package/dist/okf/types/SumConnector.md +9 -0
- package/dist/okf/types/TailSamplingProcessor.md +9 -0
- package/dist/okf/types/TransformProcessor.md +9 -0
- package/dist/pipeline.d.ts +9 -3
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/rules/otel-helpers.ts +1 -1
- package/dist/rules/otel101.ts +1 -1
- package/dist/rules/otel103.ts +1 -1
- package/dist/rules/otel112.ts +17 -0
- package/dist/semconv.d.ts +33 -0
- package/dist/semconv.d.ts.map +1 -0
- package/dist/serializer.d.ts +1 -1
- package/dist/skills/chant-otel.md +28 -5
- package/dist/topology.d.ts +34 -5
- package/dist/topology.d.ts.map +1 -1
- package/dist/validate-config.d.ts +6 -2
- package/dist/validate-config.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/catalog.ts +2 -1
- package/src/codegen/docs.ts +14 -8
- package/src/collector.ts +9 -1
- package/src/components/connectors.ts +276 -0
- package/src/components/filtering.test.ts +192 -0
- package/src/components/filtering.ts +192 -0
- package/src/components/index.ts +4 -0
- package/src/components/k8s-receivers.test.ts +123 -0
- package/src/components/k8s-receivers.ts +107 -0
- package/src/components/sampling.test.ts +464 -0
- package/src/components/sampling.ts +521 -0
- package/src/connectors.test.ts +388 -0
- package/src/define.ts +36 -4
- package/src/genai.test.ts +526 -0
- package/src/genai.ts +466 -0
- package/src/generated/lexicon-otel.json +70 -0
- package/src/index.ts +39 -0
- package/src/lint/audit-catalog.ts +11 -2
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/otel-helpers.ts +1 -1
- package/src/lint/post-synth/otel101.ts +1 -1
- package/src/lint/post-synth/otel103.ts +1 -1
- package/src/lint/post-synth/otel112.ts +17 -0
- package/src/metric-names.test.ts +80 -0
- package/src/metric-names.ts +176 -0
- package/src/model.ts +26 -4
- package/src/otelcol-validate.test.ts +143 -0
- package/src/pipeline.ts +9 -3
- package/src/plugin.test.ts +1 -0
- package/src/plugin.ts +1 -1
- package/src/semconv.ts +63 -0
- package/src/serializer.ts +1 -1
- package/src/skills/chant-otel.md +28 -5
- package/src/topology.ts +54 -8
- package/src/validate-config.ts +92 -4
- package/src/validate.ts +8 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import { load } from "js-yaml";
|
|
7
|
+
import { makePostSynthCtx } from "@intentius/chant-test-utils";
|
|
8
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
9
|
+
import {
|
|
10
|
+
BatchProcessor,
|
|
11
|
+
CountConnector,
|
|
12
|
+
DebugExporter,
|
|
13
|
+
ForwardConnector,
|
|
14
|
+
OtlpReceiver,
|
|
15
|
+
PrometheusExporter,
|
|
16
|
+
RoutingConnector,
|
|
17
|
+
ServiceGraphConnector,
|
|
18
|
+
SpanMetricsConnector,
|
|
19
|
+
SumConnector,
|
|
20
|
+
} from "./components";
|
|
21
|
+
import { collectorYaml } from "./collector";
|
|
22
|
+
import { COLLECTOR_PIN, componentEntityType, defineComponent } from "./define";
|
|
23
|
+
import { COMPONENT_KINDS, SECTION_OF, type CollectorConfig } from "./model";
|
|
24
|
+
import { Pipeline } from "./pipeline";
|
|
25
|
+
import { collectorTopology, collectorTopologyOf } from "./topology";
|
|
26
|
+
import { validateCollectorConfig, validateCollectorEntities } from "./validate-config";
|
|
27
|
+
import { otel101 } from "./lint/post-synth/otel101";
|
|
28
|
+
import { otel103 } from "./lint/post-synth/otel103";
|
|
29
|
+
import { otel112 } from "./lint/post-synth/otel112";
|
|
30
|
+
|
|
31
|
+
/** A traces pipeline feeding a metrics pipeline through spanmetrics: the issue's acceptance case. */
|
|
32
|
+
function spanMetricsCollector(): Declarable[] {
|
|
33
|
+
const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
|
|
34
|
+
const batch = new BatchProcessor({});
|
|
35
|
+
const spanmetrics = new SpanMetricsConnector({
|
|
36
|
+
dimensions: [{ name: "http.route" }],
|
|
37
|
+
histogram: { unit: "ms", explicit: { buckets: ["5ms", "50ms", "500ms", "5s"] } },
|
|
38
|
+
metrics_flush_interval: "15s",
|
|
39
|
+
});
|
|
40
|
+
const tempo = new DebugExporter({ name: "traces", verbosity: "basic" });
|
|
41
|
+
const prom = new PrometheusExporter({ endpoint: "0.0.0.0:8889" });
|
|
42
|
+
return [
|
|
43
|
+
otlp,
|
|
44
|
+
batch,
|
|
45
|
+
spanmetrics,
|
|
46
|
+
tempo,
|
|
47
|
+
prom,
|
|
48
|
+
new Pipeline({ signal: "traces", receivers: [otlp], processors: [batch], exporters: [tempo, spanmetrics] }),
|
|
49
|
+
new Pipeline({ signal: "metrics", name: "spanmetrics", receivers: [spanmetrics], processors: [batch], exporters: [prom] }),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ctxFor(yaml: string) {
|
|
54
|
+
return makePostSynthCtx("otel", yaml);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("connector as a component kind", () => {
|
|
58
|
+
test("connector is a kind with its own section", () => {
|
|
59
|
+
expect(COMPONENT_KINDS).toContain("connector");
|
|
60
|
+
expect(SECTION_OF.connector).toBe("connectors");
|
|
61
|
+
expect(componentEntityType("connector", "spanmetrics")).toBe("OTel::Connector::spanmetrics");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("the built-ins are connectors pinned to the collector release", () => {
|
|
65
|
+
for (const Cls of [SpanMetricsConnector, ServiceGraphConnector, RoutingConnector, ForwardConnector, CountConnector, SumConnector]) {
|
|
66
|
+
expect(Cls.definition.kind).toBe("connector");
|
|
67
|
+
expect(Cls.definition.builtin).toBe(true);
|
|
68
|
+
expect(Cls.definition.pin).toEqual(COLLECTOR_PIN);
|
|
69
|
+
expect(Cls.definition.connects?.length).toBeGreaterThan(0);
|
|
70
|
+
}
|
|
71
|
+
expect(SpanMetricsConnector.definition.connects).toEqual([{ from: "traces", to: "metrics" }]);
|
|
72
|
+
expect(new ForwardConnector().componentId).toBe("forward");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("serializes under connectors and on both sides of the pipelines it joins", () => {
|
|
76
|
+
const yaml = collectorYaml(spanMetricsCollector());
|
|
77
|
+
const config = load(yaml) as CollectorConfig;
|
|
78
|
+
expect(Object.keys(config)).toEqual(["receivers", "processors", "exporters", "connectors", "service"]);
|
|
79
|
+
expect(config.connectors).toEqual({
|
|
80
|
+
spanmetrics: {
|
|
81
|
+
dimensions: [{ name: "http.route" }],
|
|
82
|
+
histogram: { unit: "ms", explicit: { buckets: ["5ms", "50ms", "500ms", "5s"] } },
|
|
83
|
+
metrics_flush_interval: "15s",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
expect(config.service?.pipelines).toEqual({
|
|
87
|
+
traces: { receivers: ["otlp"], processors: ["batch"], exporters: ["debug/traces", "spanmetrics"] },
|
|
88
|
+
"metrics/spanmetrics": { receivers: ["spanmetrics"], processors: ["batch"], exporters: ["prometheus"] },
|
|
89
|
+
});
|
|
90
|
+
expect(validateCollectorConfig(config)).toEqual([]);
|
|
91
|
+
expect(validateCollectorEntities(spanMetricsCollector())).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("collectorTopology returns the connector edge with the signal on each side", () => {
|
|
95
|
+
const topo = collectorTopologyOf(spanMetricsCollector());
|
|
96
|
+
expect(topo.edges).toEqual([
|
|
97
|
+
{ connector: "spanmetrics", from: "traces", fromSignal: "traces", to: "metrics/spanmetrics", toSignal: "metrics" },
|
|
98
|
+
]);
|
|
99
|
+
const c = topo.components.find((x) => x.id === "spanmetrics")!;
|
|
100
|
+
expect(c.kind).toBe("connector");
|
|
101
|
+
expect(c.pipelines).toEqual(["traces", "metrics/spanmetrics"]);
|
|
102
|
+
// A connector is not an exporter in the "where does telemetry go" list.
|
|
103
|
+
expect(topo.exporters.map((e) => e.id)).toEqual(["debug/traces", "prometheus"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("topology keeps only the signal pairs a connector supports", () => {
|
|
107
|
+
const topo = collectorTopology({
|
|
108
|
+
receivers: { otlp: {} },
|
|
109
|
+
exporters: { debug: {} },
|
|
110
|
+
connectors: { count: {}, "someconnector/x": {} },
|
|
111
|
+
service: {
|
|
112
|
+
pipelines: {
|
|
113
|
+
traces: { receivers: ["otlp"], exporters: ["count", "someconnector/x"] },
|
|
114
|
+
metrics: { receivers: ["count", "someconnector/x"], exporters: ["debug"] },
|
|
115
|
+
logs: { receivers: ["count", "someconnector/x"], exporters: ["debug"] },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
expect(topo.edges.map((e) => [e.connector, e.from, e.to])).toEqual([
|
|
120
|
+
["count", "traces", "metrics"],
|
|
121
|
+
// No definition in this process: every pair is reported.
|
|
122
|
+
["someconnector/x", "traces", "metrics"],
|
|
123
|
+
["someconnector/x", "traces", "logs"],
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a custom connector comes in through defineComponent", () => {
|
|
128
|
+
const Tee = defineComponent<{ copies?: number }>()({
|
|
129
|
+
kind: "connector",
|
|
130
|
+
type: "tee2895",
|
|
131
|
+
pin: { source: "github.com/acme/teeconnector", version: "v1.2.0" },
|
|
132
|
+
connects: [{ from: "logs", to: "logs" }],
|
|
133
|
+
});
|
|
134
|
+
const tee = new Tee({ copies: 2 });
|
|
135
|
+
expect(tee.componentKind).toBe("connector");
|
|
136
|
+
const otlp = new OtlpReceiver({ protocols: { grpc: {} } });
|
|
137
|
+
const debug = new DebugExporter({});
|
|
138
|
+
const yaml = collectorYaml([
|
|
139
|
+
new Pipeline({ signal: "logs", receivers: [otlp], exporters: [tee] }),
|
|
140
|
+
new Pipeline({ signal: "logs", name: "copy", receivers: [tee], exporters: [debug] }),
|
|
141
|
+
]);
|
|
142
|
+
expect(yaml).toContain("# chant: connector tee2895 schema github.com/acme/teeconnector@v1.2.0");
|
|
143
|
+
expect(validateCollectorConfig(load(yaml) as CollectorConfig)).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("OTEL101 with connectors", () => {
|
|
148
|
+
const base = `receivers:
|
|
149
|
+
otlp: {protocols: {grpc: {}}}
|
|
150
|
+
exporters:
|
|
151
|
+
debug: {}
|
|
152
|
+
connectors:
|
|
153
|
+
spanmetrics: {}
|
|
154
|
+
service:
|
|
155
|
+
pipelines:
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
test("a connector used on both sides is used", () => {
|
|
159
|
+
const yaml = `${base} traces:
|
|
160
|
+
receivers: [otlp]
|
|
161
|
+
exporters: [spanmetrics]
|
|
162
|
+
metrics:
|
|
163
|
+
receivers: [spanmetrics]
|
|
164
|
+
exporters: [debug]
|
|
165
|
+
`;
|
|
166
|
+
expect(otel101.check(ctxFor(yaml))).toEqual([]);
|
|
167
|
+
expect(otel103.check(ctxFor(yaml))).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("a connector used only as an exporter fails", () => {
|
|
171
|
+
const yaml = `${base} traces:
|
|
172
|
+
receivers: [otlp]
|
|
173
|
+
exporters: [spanmetrics, debug]
|
|
174
|
+
`;
|
|
175
|
+
const diags = otel101.check(ctxFor(yaml));
|
|
176
|
+
expect(diags).toHaveLength(1);
|
|
177
|
+
expect(diags[0].entity).toBe("spanmetrics");
|
|
178
|
+
expect(diags[0].severity).toBe("error");
|
|
179
|
+
expect(diags[0].message).toContain("no pipeline lists it as a receiver");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("a connector used only as a receiver fails", () => {
|
|
183
|
+
const yaml = `${base} metrics:
|
|
184
|
+
receivers: [otlp, spanmetrics]
|
|
185
|
+
exporters: [debug]
|
|
186
|
+
`;
|
|
187
|
+
const diags = otel101.check(ctxFor(yaml));
|
|
188
|
+
expect(diags.map((d) => d.entity)).toEqual(["spanmetrics"]);
|
|
189
|
+
expect(diags[0].message).toContain("no pipeline lists it as an exporter");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("a connector no pipeline lists is an OTEL103 warning", () => {
|
|
193
|
+
const yaml = `${base} traces:
|
|
194
|
+
receivers: [otlp]
|
|
195
|
+
exporters: [debug]
|
|
196
|
+
`;
|
|
197
|
+
expect(otel101.check(ctxFor(yaml))).toEqual([]);
|
|
198
|
+
expect(otel103.check(ctxFor(yaml)).map((d) => [d.entity, d.severity])).toEqual([["spanmetrics", "warning"]]);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("OTEL112 connector signals", () => {
|
|
203
|
+
test("spanmetrics cannot be the receiver of a traces pipeline", () => {
|
|
204
|
+
const yaml = `receivers:
|
|
205
|
+
otlp: {protocols: {grpc: {}}}
|
|
206
|
+
exporters:
|
|
207
|
+
debug: {}
|
|
208
|
+
connectors:
|
|
209
|
+
spanmetrics: {}
|
|
210
|
+
service:
|
|
211
|
+
pipelines:
|
|
212
|
+
traces:
|
|
213
|
+
receivers: [otlp]
|
|
214
|
+
exporters: [spanmetrics]
|
|
215
|
+
traces/out:
|
|
216
|
+
receivers: [spanmetrics]
|
|
217
|
+
exporters: [debug]
|
|
218
|
+
`;
|
|
219
|
+
const diags = otel112.check(ctxFor(yaml));
|
|
220
|
+
expect(diags.map((d) => d.message)).toEqual([
|
|
221
|
+
'connector "spanmetrics" is an exporter in traces pipeline "traces", but no pipeline it feeds carries a signal spanmetrics makes from traces (it supports traces to metrics); the collector refuses to start',
|
|
222
|
+
'connector "spanmetrics" is a receiver in traces pipeline "traces/out", but no pipeline feeding it carries a signal spanmetrics turns into traces (it supports traces to metrics); the collector refuses to start',
|
|
223
|
+
]);
|
|
224
|
+
expect(diags.every((d) => d.severity === "error" && d.checkId === "OTEL112")).toBe(true);
|
|
225
|
+
// One-sided use is OTEL101's, not this rule's.
|
|
226
|
+
expect(otel101.check(ctxFor(yaml))).toEqual([]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("flags only the pipeline that pairs with nothing", () => {
|
|
230
|
+
const yaml = `receivers:
|
|
231
|
+
otlp: {protocols: {grpc: {}}}
|
|
232
|
+
exporters:
|
|
233
|
+
debug: {}
|
|
234
|
+
connectors:
|
|
235
|
+
servicegraph: {}
|
|
236
|
+
service:
|
|
237
|
+
pipelines:
|
|
238
|
+
traces:
|
|
239
|
+
receivers: [otlp]
|
|
240
|
+
exporters: [servicegraph]
|
|
241
|
+
logs:
|
|
242
|
+
receivers: [otlp]
|
|
243
|
+
exporters: [servicegraph]
|
|
244
|
+
metrics:
|
|
245
|
+
receivers: [servicegraph]
|
|
246
|
+
exporters: [debug]
|
|
247
|
+
`;
|
|
248
|
+
expect(otel112.check(ctxFor(yaml)).map((d) => [d.entity, d.message.split(",")[0]])).toEqual([
|
|
249
|
+
["servicegraph", 'connector "servicegraph" is an exporter in logs pipeline "logs"'],
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("routing and forward keep the signal; count turns any signal into metrics", () => {
|
|
254
|
+
const yaml = `receivers:
|
|
255
|
+
otlp: {protocols: {grpc: {}}}
|
|
256
|
+
exporters:
|
|
257
|
+
debug: {}
|
|
258
|
+
connectors:
|
|
259
|
+
forward: {}
|
|
260
|
+
count: {}
|
|
261
|
+
routing:
|
|
262
|
+
table:
|
|
263
|
+
- condition: 'attributes["tenant"] == "a"'
|
|
264
|
+
pipelines: [logs/a]
|
|
265
|
+
service:
|
|
266
|
+
pipelines:
|
|
267
|
+
logs:
|
|
268
|
+
receivers: [otlp]
|
|
269
|
+
exporters: [routing, count]
|
|
270
|
+
logs/a:
|
|
271
|
+
receivers: [routing]
|
|
272
|
+
exporters: [forward]
|
|
273
|
+
logs/b:
|
|
274
|
+
receivers: [forward]
|
|
275
|
+
exporters: [debug]
|
|
276
|
+
metrics:
|
|
277
|
+
receivers: [count]
|
|
278
|
+
exporters: [debug]
|
|
279
|
+
`;
|
|
280
|
+
expect(otel112.check(ctxFor(yaml))).toEqual([]);
|
|
281
|
+
expect(otel101.check(ctxFor(yaml))).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("a connector with no definition in this process is not checked", () => {
|
|
285
|
+
const config: CollectorConfig = {
|
|
286
|
+
receivers: { otlp: {} },
|
|
287
|
+
exporters: { debug: {} },
|
|
288
|
+
connectors: { unknownconnector: {} },
|
|
289
|
+
service: {
|
|
290
|
+
pipelines: {
|
|
291
|
+
traces: { receivers: ["otlp"], exporters: ["unknownconnector"] },
|
|
292
|
+
logs: { receivers: ["unknownconnector"], exporters: ["debug"] },
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
expect(validateCollectorConfig(config)).toEqual([]);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("connector config rules (OTEL107)", () => {
|
|
301
|
+
test("routing needs a table whose routes each have a condition or statement and pipelines", () => {
|
|
302
|
+
const routing = new RoutingConnector({
|
|
303
|
+
table: [{ pipelines: [] }, { condition: "true", statement: "route()", pipelines: ["logs/a"] }],
|
|
304
|
+
});
|
|
305
|
+
const messages = validateCollectorEntities([routing]).map((i) => i.message);
|
|
306
|
+
expect(messages).toEqual([
|
|
307
|
+
'connector "routing": table[0]: set a condition or a statement',
|
|
308
|
+
'connector "routing": table[0]: no pipelines',
|
|
309
|
+
'connector "routing": table[1]: set a condition or a statement, not both',
|
|
310
|
+
]);
|
|
311
|
+
expect(validateCollectorEntities([new RoutingConnector({ table: [] })]).map((i) => i.message)).toEqual([
|
|
312
|
+
'connector "routing": table is empty',
|
|
313
|
+
]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("spanmetrics rejects both histogram kinds and a duplicate default dimension", () => {
|
|
317
|
+
const sm = new SpanMetricsConnector({
|
|
318
|
+
dimensions: [{ name: "span.kind" }],
|
|
319
|
+
histogram: { explicit: {}, exponential: { max_size: 160 } },
|
|
320
|
+
});
|
|
321
|
+
expect(validateCollectorEntities([sm]).map((i) => i.message)).toEqual([
|
|
322
|
+
'connector "spanmetrics": histogram: set explicit or exponential buckets, not both',
|
|
323
|
+
'connector "spanmetrics": dimensions: "span.kind" is already a default dimension',
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("count does not split metric counts by attribute", () => {
|
|
328
|
+
const count = new CountConnector({ metrics: { "metric.count": { attributes: [{ key: "env" }] } } });
|
|
329
|
+
expect(validateCollectorEntities([count]).map((i) => i.message)).toEqual([
|
|
330
|
+
'connector "count": metrics.metric.count: attributes are not supported when counting metrics',
|
|
331
|
+
]);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// `otelcol validate` is the acceptance test for the emitted YAML. CI has no
|
|
336
|
+
// collector binary, so this runs only when one is on PATH (the contrib build,
|
|
337
|
+
// which ships the connectors) and is skipped, with the reason shown, when not.
|
|
338
|
+
function collectorBinary(): string | undefined {
|
|
339
|
+
const fromEnv = process.env.OTELCOL_BIN;
|
|
340
|
+
const candidates = fromEnv ? [fromEnv] : ["otelcol-contrib", "otelcol"];
|
|
341
|
+
for (const bin of candidates) {
|
|
342
|
+
try {
|
|
343
|
+
execSync(`${bin} --version`, { stdio: "ignore" });
|
|
344
|
+
return bin;
|
|
345
|
+
} catch {
|
|
346
|
+
// not installed under this name
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const otelcol = collectorBinary();
|
|
353
|
+
|
|
354
|
+
describe.skipIf(!otelcol)(
|
|
355
|
+
`otelcol validate accepts a connector config${otelcol ? "" : " (skipped: no otelcol-contrib or otelcol on PATH)"}`,
|
|
356
|
+
() => {
|
|
357
|
+
test(`traces into metrics through spanmetrics validates (typed against ${COLLECTOR_PIN.version})`, () => {
|
|
358
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-otel-connectors-"));
|
|
359
|
+
try {
|
|
360
|
+
const file = join(dir, "config.yaml");
|
|
361
|
+
writeFileSync(file, collectorYaml(spanMetricsCollector()));
|
|
362
|
+
expect(() => execFileSync(otelcol!, ["validate", `--config=${file}`], { stdio: "pipe" })).not.toThrow();
|
|
363
|
+
} finally {
|
|
364
|
+
rmSync(dir, { recursive: true, force: true });
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("a spanmetrics signal mismatch is refused by the collector too", () => {
|
|
369
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-otel-connectors-"));
|
|
370
|
+
try {
|
|
371
|
+
const file = join(dir, "config.yaml");
|
|
372
|
+
const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
|
|
373
|
+
const sm = new SpanMetricsConnector({});
|
|
374
|
+
const debug = new DebugExporter({});
|
|
375
|
+
writeFileSync(
|
|
376
|
+
file,
|
|
377
|
+
collectorYaml([
|
|
378
|
+
new Pipeline({ signal: "traces", receivers: [otlp], exporters: [sm] }),
|
|
379
|
+
new Pipeline({ signal: "traces", name: "out", receivers: [sm], exporters: [debug] }),
|
|
380
|
+
]),
|
|
381
|
+
);
|
|
382
|
+
expect(() => execFileSync(otelcol!, ["validate", `--config=${file}`], { stdio: "pipe" })).toThrow();
|
|
383
|
+
} finally {
|
|
384
|
+
rmSync(dir, { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
);
|
package/src/define.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* `defineComponent()`, the one way a collector component type enters this
|
|
3
3
|
* lexicon.
|
|
4
4
|
*
|
|
5
|
-
* The built-in receivers, processors, exporters and extensions are
|
|
6
|
-
* through it, and so is a component a team or plugin adds for something
|
|
7
|
-
* doesn't ship (a vendor exporter, an in-house processor). Both produce the
|
|
5
|
+
* The built-in receivers, processors, exporters, connectors and extensions are
|
|
6
|
+
* defined through it, and so is a component a team or plugin adds for something
|
|
7
|
+
* chant doesn't ship (a vendor exporter, an in-house processor). Both produce the
|
|
8
8
|
* same kind of class, register in the same table, serialize through the same
|
|
9
9
|
* code and are checked by the same post-synth checks, so there is no second
|
|
10
10
|
* path for a custom component to fall off.
|
|
@@ -24,11 +24,17 @@
|
|
|
24
24
|
* the top of the emitted YAML, so the file says which schema each
|
|
25
25
|
* non-built-in component was checked against. The collector ignores it.
|
|
26
26
|
* - OTEL109 fails a build whose custom component has no usable pin.
|
|
27
|
+
*
|
|
28
|
+
* Semantic-convention pins (`GENAI_SEMCONV_PIN`) record which version of an
|
|
29
|
+
* attribute vocabulary a preset's keys follow. They are not tied to a
|
|
30
|
+
* component type, so `collectorTopology()` reports them under `semconv` for
|
|
31
|
+
* each component whose config uses that vocabulary, and the serializer writes
|
|
32
|
+
* one `# chant: semconv` line for them.
|
|
27
33
|
*/
|
|
28
34
|
|
|
29
35
|
import { createResource } from "@intentius/chant/runtime";
|
|
30
36
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
31
|
-
import { componentId, type ComponentKind } from "./model";
|
|
37
|
+
import { componentId, type ComponentKind, type ConnectorSignalPair } from "./model";
|
|
32
38
|
|
|
33
39
|
/** Where a component's config schema comes from, and which version of it the type follows. */
|
|
34
40
|
export interface SchemaPin {
|
|
@@ -49,6 +55,24 @@ export const COLLECTOR_PIN: SchemaPin = Object.freeze({
|
|
|
49
55
|
version: "v0.130.0",
|
|
50
56
|
});
|
|
51
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The OpenTelemetry GenAI semantic conventions the GenAI preset
|
|
60
|
+
* (`genAiPipeline()`) follows: which attributes carry prompt and completion
|
|
61
|
+
* content, and which name the operation, model, tool and token usage.
|
|
62
|
+
*
|
|
63
|
+
* v1.41.1 is the last semantic-conventions release that defines `gen_ai.*`
|
|
64
|
+
* itself. From v1.42.0 the GenAI conventions live in
|
|
65
|
+
* github.com/open-telemetry/semantic-conventions-genai, which had published no
|
|
66
|
+
* release when this pin was set; move the pin there once it does. Like
|
|
67
|
+
* `COLLECTOR_PIN`, it moves only when this package does, and
|
|
68
|
+
* `collectorTopology()` reports it for every component whose config names a
|
|
69
|
+
* `gen_ai.` attribute.
|
|
70
|
+
*/
|
|
71
|
+
export const GENAI_SEMCONV_PIN: SchemaPin = Object.freeze({
|
|
72
|
+
source: "github.com/open-telemetry/semantic-conventions",
|
|
73
|
+
version: "v1.41.1",
|
|
74
|
+
});
|
|
75
|
+
|
|
52
76
|
/** A zod-compatible schema: anything with `safeParse`. Keeps zod optional. */
|
|
53
77
|
export interface SafeParseSchema {
|
|
54
78
|
safeParse(value: unknown): {
|
|
@@ -74,6 +98,13 @@ export interface ComponentDefinition<K extends ComponentKind = ComponentKind, T
|
|
|
74
98
|
validate?: ConfigValidator<C>;
|
|
75
99
|
/** Where this component sends or listens, for `collectorTopology()`. */
|
|
76
100
|
endpoints?: (config: C) => string[];
|
|
101
|
+
/**
|
|
102
|
+
* Connectors only: the signal pairs the connector supports, as its factory
|
|
103
|
+
* registers them. OTEL112 checks each pipeline a connector joins against
|
|
104
|
+
* these, and `collectorTopology()` reports an edge only for a supported
|
|
105
|
+
* pair. A connector without them is not checked.
|
|
106
|
+
*/
|
|
107
|
+
connects?: ReadonlyArray<ConnectorSignalPair>;
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
/** What a custom component supplies. `builtin` is always false for these. */
|
|
@@ -110,6 +141,7 @@ const KIND_SEGMENT: Record<ComponentKind, string> = {
|
|
|
110
141
|
receiver: "Receiver",
|
|
111
142
|
processor: "Processor",
|
|
112
143
|
exporter: "Exporter",
|
|
144
|
+
connector: "Connector",
|
|
113
145
|
extension: "Extension",
|
|
114
146
|
};
|
|
115
147
|
|