@intentius/chant-lexicon-otel 0.100.0 → 0.102.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 +8 -6
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/common.d.ts +2 -0
- package/dist/components/common.d.ts.map +1 -1
- package/dist/components/connectors.d.ts +109 -1
- package/dist/components/connectors.d.ts.map +1 -1
- package/dist/components/exporters.d.ts.map +1 -1
- package/dist/components/extensions.d.ts +26 -2
- package/dist/components/extensions.d.ts.map +1 -1
- package/dist/components/processors.d.ts +15 -2
- package/dist/components/processors.d.ts.map +1 -1
- package/dist/components/receivers.d.ts.map +1 -1
- package/dist/composites/catalog.d.ts +3 -0
- package/dist/composites/catalog.d.ts.map +1 -0
- package/dist/composites/index.d.ts +6 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/node-agent.d.ts +111 -0
- package/dist/composites/node-agent.d.ts.map +1 -0
- package/dist/configmap.d.ts +35 -0
- package/dist/configmap.d.ts.map +1 -0
- package/dist/define.d.ts +9 -0
- package/dist/define.d.ts.map +1 -1
- package/dist/genai.d.ts +145 -2
- package/dist/genai.d.ts.map +1 -1
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/init-templates.d.ts +24 -0
- package/dist/init-templates.d.ts.map +1 -0
- package/dist/integrity.json +10 -5
- package/dist/lint/audit-catalog.d.ts +1 -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 +32 -9
- package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/otel113.d.ts +8 -0
- package/dist/lint/post-synth/otel113.d.ts.map +1 -0
- package/dist/lint/post-synth/otel114.d.ts +8 -0
- package/dist/lint/post-synth/otel114.d.ts.map +1 -0
- package/dist/lint/post-synth/otel115.d.ts +8 -0
- package/dist/lint/post-synth/otel115.d.ts.map +1 -0
- package/dist/lint/post-synth/otel116.d.ts +8 -0
- package/dist/lint/post-synth/otel116.d.ts.map +1 -0
- package/dist/lint/post-synth/otel117.d.ts +8 -0
- package/dist/lint/post-synth/otel117.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +15 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/okf/index.md +8 -0
- package/dist/okf/rules/OTEL113.md +11 -0
- package/dist/okf/rules/OTEL114.md +11 -0
- package/dist/okf/rules/OTEL115.md +11 -0
- package/dist/okf/rules/OTEL116.md +11 -0
- package/dist/okf/rules/OTEL117.md +11 -0
- package/dist/okf/types/DeltaToCumulativeProcessor.md +9 -0
- package/dist/okf/types/K8sLeaderElectorExtension.md +9 -0
- package/dist/okf/types/SignalToMetricsConnector.md +9 -0
- package/dist/platform.d.ts +6 -2
- package/dist/platform.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/otel-helpers.ts +43 -11
- package/dist/rules/otel113.ts +17 -0
- package/dist/rules/otel114.ts +17 -0
- package/dist/rules/otel115.ts +17 -0
- package/dist/rules/otel116.ts +17 -0
- package/dist/rules/otel117.ts +17 -0
- package/dist/skill-defs.d.ts +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-otel.md +11 -5
- package/dist/topology.d.ts +5 -0
- package/dist/topology.d.ts.map +1 -1
- package/dist/validate-config.d.ts +14 -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/codegen/docs.ts +9 -4
- package/src/components/common.ts +17 -0
- package/src/components/connectors.ts +209 -1
- package/src/components/exporters.ts +2 -0
- package/src/components/extensions.test.ts +70 -0
- package/src/components/extensions.ts +60 -2
- package/src/components/processors.test.ts +54 -0
- package/src/components/processors.ts +41 -2
- package/src/components/receivers.ts +5 -0
- package/src/composites/catalog.test.ts +38 -0
- package/src/composites/catalog.ts +81 -0
- package/src/composites/composites.test.ts +138 -0
- package/src/composites/index.ts +6 -0
- package/src/composites/node-agent.ts +220 -0
- package/src/configmap.ts +67 -0
- package/src/connectors.test.ts +197 -1
- package/src/define.ts +9 -0
- package/src/genai.test.ts +301 -6
- package/src/genai.ts +323 -8
- package/src/generated/lexicon-otel.json +15 -0
- package/src/import/roundtrip.test.ts +21 -1
- package/src/import/testdata/fixtures.ts +37 -5
- package/src/import/testdata/k8s-leader-elector.yaml +35 -0
- package/src/import/testdata/signaltometrics.yaml +83 -0
- package/src/index.ts +16 -0
- package/src/init-templates.test.ts +72 -0
- package/src/init-templates.ts +148 -0
- package/src/lint/audit-catalog.ts +41 -1
- package/src/lint/post-synth/connector-graph.test.ts +207 -0
- package/src/lint/post-synth/index.ts +10 -0
- package/src/lint/post-synth/otel-helpers.ts +43 -11
- package/src/lint/post-synth/otel113.ts +17 -0
- package/src/lint/post-synth/otel114.ts +17 -0
- package/src/lint/post-synth/otel115.ts +17 -0
- package/src/lint/post-synth/otel116.ts +17 -0
- package/src/lint/post-synth/otel117.ts +17 -0
- package/src/lint/post-synth/post-synth.test.ts +236 -1
- package/src/lint/post-synth/testdata/otel113-fail.yaml +23 -0
- package/src/lint/post-synth/testdata/otel113-pass.yaml +21 -0
- package/src/lint/post-synth/testdata/otel114-fail.yaml +25 -0
- package/src/lint/post-synth/testdata/otel114-pass.yaml +23 -0
- package/src/lint/post-synth/testdata/otel115-fail.yaml +29 -0
- package/src/lint/post-synth/testdata/otel115-pass.yaml +29 -0
- package/src/platform.test.ts +26 -0
- package/src/platform.ts +17 -13
- package/src/plugin.test.ts +18 -0
- package/src/plugin.ts +22 -0
- package/src/skills/chant-otel.md +11 -5
- package/src/testdata/genai-configured.yaml +108 -0
- package/src/testdata/genai-default.yaml +92 -0
- package/src/topology.test.ts +12 -0
- package/src/topology.ts +16 -0
- package/src/typecheck.test.ts +112 -0
- package/src/validate-config.ts +426 -2
- package/src/validate.ts +17 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# A signaltometrics connector (collector-contrib v0.130.0) building metrics
|
|
2
|
+
# from spans, logs and data points: explicit and exponential histograms, a
|
|
3
|
+
# sum and a gauge, with attributes, resource attributes and conditions.
|
|
4
|
+
|
|
5
|
+
receivers:
|
|
6
|
+
otlp:
|
|
7
|
+
protocols:
|
|
8
|
+
grpc:
|
|
9
|
+
endpoint: 0.0.0.0:4317
|
|
10
|
+
processors:
|
|
11
|
+
batch: {}
|
|
12
|
+
exporters:
|
|
13
|
+
debug:
|
|
14
|
+
verbosity: basic
|
|
15
|
+
prometheus:
|
|
16
|
+
endpoint: 0.0.0.0:8889
|
|
17
|
+
connectors:
|
|
18
|
+
signaltometrics/genai:
|
|
19
|
+
spans:
|
|
20
|
+
- name: gen_ai.client.operation.duration
|
|
21
|
+
description: GenAI operation duration
|
|
22
|
+
unit: s
|
|
23
|
+
conditions:
|
|
24
|
+
- attributes["gen_ai.operation.name"] != nil
|
|
25
|
+
attributes:
|
|
26
|
+
- key: gen_ai.operation.name
|
|
27
|
+
- key: gen_ai.request.model
|
|
28
|
+
default_value: unknown
|
|
29
|
+
- key: error.type
|
|
30
|
+
optional: true
|
|
31
|
+
include_resource_attributes:
|
|
32
|
+
- key: service.name
|
|
33
|
+
histogram:
|
|
34
|
+
buckets: [0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92]
|
|
35
|
+
value: Double(Microseconds(end_time - start_time)) / 1000000.0
|
|
36
|
+
- name: gen_ai.client.token.usage
|
|
37
|
+
description: Input tokens used
|
|
38
|
+
unit: "{token}"
|
|
39
|
+
conditions:
|
|
40
|
+
- attributes["gen_ai.usage.input_tokens"] != nil
|
|
41
|
+
attributes:
|
|
42
|
+
- key: gen_ai.request.model
|
|
43
|
+
default_value: unknown
|
|
44
|
+
- key: gen_ai.token.type
|
|
45
|
+
default_value: input
|
|
46
|
+
histogram:
|
|
47
|
+
buckets: [1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864]
|
|
48
|
+
value: attributes["gen_ai.usage.input_tokens"]
|
|
49
|
+
- name: span.count
|
|
50
|
+
description: Spans, adjusted for sampling
|
|
51
|
+
sum:
|
|
52
|
+
value: Int(AdjustedCount())
|
|
53
|
+
- name: span.duration.exponential
|
|
54
|
+
unit: us
|
|
55
|
+
exponential_histogram:
|
|
56
|
+
max_size: 80
|
|
57
|
+
value: Microseconds(end_time - start_time)
|
|
58
|
+
logs:
|
|
59
|
+
- name: logrecord.count
|
|
60
|
+
sum:
|
|
61
|
+
value: "1"
|
|
62
|
+
datapoints:
|
|
63
|
+
- name: datapoint.last
|
|
64
|
+
conditions:
|
|
65
|
+
- metric.type == 1
|
|
66
|
+
gauge:
|
|
67
|
+
value: value_double
|
|
68
|
+
service:
|
|
69
|
+
pipelines:
|
|
70
|
+
traces:
|
|
71
|
+
receivers: [otlp]
|
|
72
|
+
processors: [batch]
|
|
73
|
+
exporters: [debug, signaltometrics/genai]
|
|
74
|
+
logs:
|
|
75
|
+
receivers: [otlp]
|
|
76
|
+
exporters: [signaltometrics/genai]
|
|
77
|
+
metrics:
|
|
78
|
+
receivers: [otlp]
|
|
79
|
+
exporters: [signaltometrics/genai]
|
|
80
|
+
metrics/genai:
|
|
81
|
+
receivers: [signaltometrics/genai]
|
|
82
|
+
processors: [batch]
|
|
83
|
+
exporters: [prometheus]
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export {
|
|
|
47
47
|
export * from "./model";
|
|
48
48
|
export { buildCollectorConfig, collectorYaml, componentConfig, type BuiltCollector } from "./collector";
|
|
49
49
|
export { emitCollectorYaml, type EmitOptions } from "./yaml";
|
|
50
|
+
export { configMapCollectorConfigs, describeConfigMapConfig, parseCollectorConfig, type ConfigMapCollectorConfig } from "./configmap";
|
|
50
51
|
export {
|
|
51
52
|
validateCollectorConfig,
|
|
52
53
|
validateCollectorEntities,
|
|
@@ -65,6 +66,9 @@ export {
|
|
|
65
66
|
} from "./topology";
|
|
66
67
|
export { semconvUsage, SEMCONV_VOCABULARIES, type SemconvVocabulary } from "./semconv";
|
|
67
68
|
|
|
69
|
+
// Composites
|
|
70
|
+
export { NodeAgent, nodeAgentPropsProblem, type NodeAgentProps, type NodeAgentMembers, type NodeAgentInstance } from "./composites";
|
|
71
|
+
|
|
68
72
|
// What the platform collector composites (docker, k8s, fly) share
|
|
69
73
|
export {
|
|
70
74
|
otlpCollector,
|
|
@@ -85,16 +89,28 @@ export {
|
|
|
85
89
|
GENAI_CONTENT_ATTRIBUTES,
|
|
86
90
|
GENAI_CONTENT_EVENTS,
|
|
87
91
|
GENAI_INDEXED_CONTENT_PATTERN,
|
|
92
|
+
GENAI_HIGH_CARDINALITY_ATTRIBUTES,
|
|
93
|
+
genAiCardinalityRisk,
|
|
88
94
|
GENAI_SPAN_METRIC_DIMENSIONS,
|
|
89
95
|
GENAI_TOKEN_DIMENSIONS,
|
|
90
96
|
GENAI_DURATION_BUCKETS,
|
|
91
97
|
GENAI_UNKNOWN_MODEL,
|
|
98
|
+
GENAI_PROVIDER_DIMENSIONS,
|
|
99
|
+
GENAI_CLIENT_METRIC_NAMES,
|
|
100
|
+
GENAI_CLIENT_METRIC_ATTRIBUTES,
|
|
101
|
+
GENAI_CLIENT_DURATION_BUCKETS,
|
|
102
|
+
GENAI_CLIENT_TOKEN_BUCKETS,
|
|
103
|
+
GENAI_TOKEN_TYPES,
|
|
104
|
+
DELTA_READY_EXPORTERS,
|
|
105
|
+
genAiNeedsDeltaToCumulative,
|
|
92
106
|
type GenAiPipelineOptions,
|
|
93
107
|
type GenAiComponentsOptions,
|
|
94
108
|
type GenAiComponents,
|
|
95
109
|
type GenAiMetricsOptions,
|
|
96
110
|
type GenAiMetrics,
|
|
97
111
|
type GenAiMetric,
|
|
112
|
+
type GenAiClientMetrics,
|
|
113
|
+
type GenAiClientMetricsSource,
|
|
98
114
|
} from "./genai";
|
|
99
115
|
|
|
100
116
|
// Metric names as Prometheus serves them, for dashboards and SLOs built from a declaration
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { load } from "js-yaml";
|
|
5
|
+
import { build } from "@intentius/chant/build";
|
|
6
|
+
import { lintCommand } from "@intentius/chant/cli/commands/lint";
|
|
7
|
+
import { runPostSynthChecks } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { otelPlugin } from "./plugin";
|
|
9
|
+
import { postSynthChecks } from "./lint/post-synth";
|
|
10
|
+
import { TEMPLATE_NAMES } from "./init-templates";
|
|
11
|
+
import type { CollectorConfig } from "./model";
|
|
12
|
+
|
|
13
|
+
async function buildTemplate(name: string | undefined): Promise<{ dir: string; result: Awaited<ReturnType<typeof build>> }> {
|
|
14
|
+
const set = otelPlugin.initTemplates!(name);
|
|
15
|
+
const dir = mkdtempSync(join(import.meta.dirname, "..", ".init-template-"));
|
|
16
|
+
mkdirSync(join(dir, "src"));
|
|
17
|
+
for (const [file, text] of Object.entries(set.src)) writeFileSync(join(dir, "src", file), text);
|
|
18
|
+
for (const [file, text] of Object.entries(set.root ?? {})) writeFileSync(join(dir, file), text);
|
|
19
|
+
return { dir, result: await build(join(dir, "src"), [otelPlugin.serializer]) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function configOf(result: Awaited<ReturnType<typeof build>>): CollectorConfig {
|
|
23
|
+
return load(result.outputs.get("otel") as string) as CollectorConfig;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Every template builds with the otel lexicon alone, and its output passes
|
|
27
|
+
// every OTEL check and lints with nothing to report.
|
|
28
|
+
describe("init templates", () => {
|
|
29
|
+
test.each([undefined, ...TEMPLATE_NAMES])("%s builds, lints clean and passes every check", async (name) => {
|
|
30
|
+
const { dir, result } = await buildTemplate(name);
|
|
31
|
+
try {
|
|
32
|
+
expect(result.errors).toEqual([]);
|
|
33
|
+
expect(result.outputs.get("otel")).toBeTruthy();
|
|
34
|
+
expect(runPostSynthChecks(postSynthChecks, result)).toEqual([]);
|
|
35
|
+
const lint = await lintCommand({ path: join(dir, "src"), format: "stylish", fix: false });
|
|
36
|
+
expect(lint.errorCount + lint.warningCount, lint.output).toBe(0);
|
|
37
|
+
} finally {
|
|
38
|
+
rmSync(dir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("the k8s-agent template sends traces and logs to the gateway and serves metrics on the node", async () => {
|
|
43
|
+
const { dir, result } = await buildTemplate("k8s-agent");
|
|
44
|
+
try {
|
|
45
|
+
const config = configOf(result);
|
|
46
|
+
const pipelines = config.service?.pipelines ?? {};
|
|
47
|
+
expect(pipelines.traces.exporters).toEqual(["otlp/gateway"]);
|
|
48
|
+
expect(pipelines.logs.exporters).toEqual(["otlp/gateway"]);
|
|
49
|
+
expect(pipelines.metrics.exporters).toEqual(["prometheus"]);
|
|
50
|
+
expect(pipelines.metrics.receivers).toEqual(["otlp", "hostmetrics", "kubeletstats"]);
|
|
51
|
+
expect(config.processors?.resource).toEqual({ attributes: [{ key: "k8s.cluster.name", value: "my-cluster", action: "upsert" }] });
|
|
52
|
+
} finally {
|
|
53
|
+
rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the genai template derives the conventions' client metrics and removes content", async () => {
|
|
58
|
+
const { dir, result } = await buildTemplate("genai");
|
|
59
|
+
try {
|
|
60
|
+
const config = configOf(result);
|
|
61
|
+
expect(Object.keys(config.connectors ?? {})).toContain("signaltometrics/genai_client");
|
|
62
|
+
expect(config.service?.pipelines?.traces?.processors).toContain("transform/genai_content");
|
|
63
|
+
expect(config.service?.pipelines?.["metrics/genai"]?.exporters).toEqual(["prometheus"]);
|
|
64
|
+
} finally {
|
|
65
|
+
rmSync(dir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("an unknown template name falls back to the default", () => {
|
|
70
|
+
expect(otelPlugin.initTemplates!("no-such-template")).toBe(otelPlugin.initTemplates!());
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant init --lexicon otel` scaffolds.
|
|
3
|
+
*
|
|
4
|
+
* - default: OTLP in, `memory_limiter` and `batch`, traces to an OTLP
|
|
5
|
+
* backend and logs to `debug`, with a `health_check`.
|
|
6
|
+
* - `k8s-agent`: the config a per-node agent runs as a Kubernetes DaemonSet
|
|
7
|
+
* (`NodeAgent`): OTLP from the node's pods, host metrics and container
|
|
8
|
+
* logs, Kubernetes metadata on all of it, traces and logs on to a gateway
|
|
9
|
+
* and metrics served for Prometheus to scrape on each node.
|
|
10
|
+
* - `genai`: a collector for an agent service instrumented with the
|
|
11
|
+
* OpenTelemetry GenAI conventions (`genAiPipeline`): prompt and completion
|
|
12
|
+
* content removed, RED and token metrics and the conventions' client
|
|
13
|
+
* metrics derived from every GenAI span, traces to Tempo.
|
|
14
|
+
*
|
|
15
|
+
* Each template builds with the otel lexicon alone, which is all
|
|
16
|
+
* `chant init --lexicon otel` installs.
|
|
17
|
+
*/
|
|
18
|
+
import type { InitTemplateSet } from "@intentius/chant/lexicon";
|
|
19
|
+
|
|
20
|
+
// ── default ────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const DEFAULT_COLLECTOR = `/**
|
|
23
|
+
* A collector: OTLP in, batched, traces out to an OTLP backend and logs to
|
|
24
|
+
* the collector's own output. \`chant build\` writes the config
|
|
25
|
+
* \`otelcol --config\` reads.
|
|
26
|
+
*
|
|
27
|
+
* Nested settings are named consts typed with the lexicon's config types, so
|
|
28
|
+
* each constructor stays flat.
|
|
29
|
+
*/
|
|
30
|
+
import {
|
|
31
|
+
OtlpReceiver,
|
|
32
|
+
MemoryLimiterProcessor,
|
|
33
|
+
BatchProcessor,
|
|
34
|
+
OtlpExporter,
|
|
35
|
+
DebugExporter,
|
|
36
|
+
HealthCheckExtension,
|
|
37
|
+
Pipeline,
|
|
38
|
+
type OtlpReceiverConfig,
|
|
39
|
+
type TLSClientSettings,
|
|
40
|
+
type QueueSettings,
|
|
41
|
+
} from "@intentius/chant-lexicon-otel";
|
|
42
|
+
|
|
43
|
+
const protocols: OtlpReceiverConfig["protocols"] = {
|
|
44
|
+
grpc: { endpoint: "0.0.0.0:4317" },
|
|
45
|
+
http: { endpoint: "0.0.0.0:4318" },
|
|
46
|
+
};
|
|
47
|
+
const otlp = new OtlpReceiver({ protocols });
|
|
48
|
+
|
|
49
|
+
const memoryLimiter = new MemoryLimiterProcessor({ check_interval: "1s", limit_percentage: 80, spike_limit_percentage: 20 });
|
|
50
|
+
const batch = new BatchProcessor({ timeout: "5s" });
|
|
51
|
+
|
|
52
|
+
// Point this at your tracing backend. \`otlp/backend\`: the instance name leaves room for a second otlp exporter.
|
|
53
|
+
const plaintext: TLSClientSettings = { insecure: true };
|
|
54
|
+
const queue: QueueSettings = { enabled: true, queue_size: 5000 };
|
|
55
|
+
const backend = new OtlpExporter({ name: "backend", endpoint: "tempo:4317", tls: plaintext, sending_queue: queue });
|
|
56
|
+
|
|
57
|
+
const debug = new DebugExporter({ verbosity: "basic" });
|
|
58
|
+
|
|
59
|
+
const health = new HealthCheckExtension({ endpoint: "0.0.0.0:13133" });
|
|
60
|
+
|
|
61
|
+
const traces = new Pipeline({ signal: "traces", receivers: [otlp], processors: [memoryLimiter, batch], exporters: [backend] });
|
|
62
|
+
const logs = new Pipeline({ signal: "logs", receivers: [otlp], processors: [memoryLimiter, batch], exporters: [debug] });
|
|
63
|
+
|
|
64
|
+
export { otlp, memoryLimiter, batch, backend, debug, health, traces, logs };
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
// ── k8s-agent ──────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const AGENT_EXPORTERS = `/**
|
|
70
|
+
* Where the agent sends what it collects: traces and logs to the gateway
|
|
71
|
+
* collectors, metrics served on each node for Prometheus to scrape.
|
|
72
|
+
*/
|
|
73
|
+
import { OtlpExporter, PrometheusExporter, type RetrySettings, type TLSClientSettings } from "@intentius/chant-lexicon-otel";
|
|
74
|
+
|
|
75
|
+
// The gateway Service. With the k8s lexicon's OtelCollectorGateway, use its gatewayExporter() instead.
|
|
76
|
+
const inCluster: TLSClientSettings = { insecure: true };
|
|
77
|
+
const retry: RetrySettings = { enabled: true, max_elapsed_time: "300s" };
|
|
78
|
+
const gateway = new OtlpExporter({ name: "gateway", endpoint: "otel-gateway.observability.svc:4317", tls: inCluster, retry_on_failure: retry });
|
|
79
|
+
|
|
80
|
+
// Resource attributes (k8s.namespace.name, k8s.pod.name, ...) become metric labels.
|
|
81
|
+
const asLabels = { enabled: true };
|
|
82
|
+
const scrape = new PrometheusExporter({ endpoint: "0.0.0.0:8889", resource_to_telemetry_conversion: asLabels });
|
|
83
|
+
|
|
84
|
+
export { gateway, scrape };
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const AGENT = `/**
|
|
88
|
+
* The config each node's collector runs. The pod needs the node name in
|
|
89
|
+
* K8S_NODE_NAME (from spec.nodeName), the host root mounted read-only at
|
|
90
|
+
* /hostfs, /var/log/pods mounted read-only, and a service account that can
|
|
91
|
+
* read pods, namespaces, nodes and replicasets (and nodes/stats, for kubelet
|
|
92
|
+
* stats). Pass \`Object.values(agent.members)\` as the k8s lexicon's
|
|
93
|
+
* OtelCollector \`config\` to run it as a DaemonSet: it adds all of these,
|
|
94
|
+
* worked out from the config.
|
|
95
|
+
*/
|
|
96
|
+
import { NodeAgent } from "@intentius/chant-lexicon-otel";
|
|
97
|
+
import { gateway, scrape } from "./exporters";
|
|
98
|
+
|
|
99
|
+
export const agent = NodeAgent({
|
|
100
|
+
exporters: [gateway],
|
|
101
|
+
metricExporters: [scrape],
|
|
102
|
+
clusterName: "my-cluster",
|
|
103
|
+
kubeletStats: true,
|
|
104
|
+
});
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
// ── genai ──────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
const GENAI_COLLECTOR = `/**
|
|
110
|
+
* A collector for an agent service instrumented with the OpenTelemetry GenAI
|
|
111
|
+
* conventions. Prompts, completions and tool payloads are removed before
|
|
112
|
+
* anything leaves the collector. Every GenAI span also becomes call, error,
|
|
113
|
+
* duration and token metrics, and the conventions' own client metrics
|
|
114
|
+
* (gen_ai.client.operation.duration, gen_ai.client.token.usage), served for
|
|
115
|
+
* Prometheus to scrape. Traces go to Tempo.
|
|
116
|
+
*/
|
|
117
|
+
import { genAiPipeline, OtlpExporter, PrometheusExporter, type TLSClientSettings } from "@intentius/chant-lexicon-otel";
|
|
118
|
+
|
|
119
|
+
const plaintext: TLSClientSettings = { insecure: true };
|
|
120
|
+
const tempo = new OtlpExporter({ name: "tempo", endpoint: "tempo:4317", tls: plaintext });
|
|
121
|
+
|
|
122
|
+
const prometheus = new PrometheusExporter({ endpoint: "0.0.0.0:8889" });
|
|
123
|
+
|
|
124
|
+
// Content stays out unless keepContent: true is set here. Card-number-like values are masked in whatever is left.
|
|
125
|
+
const cardNumbers = ["\\\\b[0-9]{13,16}\\\\b"];
|
|
126
|
+
|
|
127
|
+
export const collector = genAiPipeline({
|
|
128
|
+
traceExporters: [tempo],
|
|
129
|
+
metricExporters: [prometheus],
|
|
130
|
+
clientMetrics: "derive",
|
|
131
|
+
maskValues: cardNumbers,
|
|
132
|
+
});
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
export const DEFAULT_TEMPLATE: InitTemplateSet = {
|
|
136
|
+
src: { "collector.ts": DEFAULT_COLLECTOR },
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const K8S_AGENT_TEMPLATE: InitTemplateSet = {
|
|
140
|
+
src: { "exporters.ts": AGENT_EXPORTERS, "agent.ts": AGENT },
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const GENAI_TEMPLATE: InitTemplateSet = {
|
|
144
|
+
src: { "collector.ts": GENAI_COLLECTOR },
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** The template names `chant init --lexicon otel --template <name>` takes, besides the default. */
|
|
148
|
+
export const TEMPLATE_NAMES = ["k8s-agent", "genai"] as const;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The otel lexicon's chant audit catalog, contributed via
|
|
3
3
|
* `otelPlugin.auditCatalog()` (#687, #1346).
|
|
4
4
|
*
|
|
5
|
-
* OTEL101-OTEL106 and OTEL112 read the emitted collector YAML, so they are
|
|
5
|
+
* OTEL101-OTEL106 and OTEL112-OTEL117 read the emitted collector YAML, so they are
|
|
6
6
|
* `yamlBased`.
|
|
7
7
|
* OTEL107-OTEL109 read the declared entities (a component's definition, its
|
|
8
8
|
* schema pin), which a standalone YAML file does not carry, so they are
|
|
@@ -108,4 +108,44 @@ export const otelAuditCatalog: Record<string, RuleMeta> = {
|
|
|
108
108
|
"Feed the connector from, and receive from it into, pipelines of the signals it supports (spanmetrics: traces in, metrics out).",
|
|
109
109
|
{ category: "correctness" },
|
|
110
110
|
),
|
|
111
|
+
OTEL113: auditRule(
|
|
112
|
+
"OTEL113",
|
|
113
|
+
"merge-worthy",
|
|
114
|
+
"guidance",
|
|
115
|
+
"Pipelines form a cycle through connectors",
|
|
116
|
+
"Break the cycle the message names: drop one connector hop, or send that pipeline's data to an exporter instead of back upstream.",
|
|
117
|
+
{ category: "correctness" },
|
|
118
|
+
),
|
|
119
|
+
OTEL114: auditRule(
|
|
120
|
+
"OTEL114",
|
|
121
|
+
"merge-worthy",
|
|
122
|
+
"guidance",
|
|
123
|
+
"Connector id also declared as a receiver or exporter",
|
|
124
|
+
'Give the connector, or the receiver or exporter, its own name (e.g. "datadog/connector") and update the pipelines that list it.',
|
|
125
|
+
{ category: "correctness" },
|
|
126
|
+
),
|
|
127
|
+
OTEL115: auditRule(
|
|
128
|
+
"OTEL115",
|
|
129
|
+
"merge-worthy",
|
|
130
|
+
"guidance",
|
|
131
|
+
"Routing connector routes to a pipeline that does not receive from it",
|
|
132
|
+
"List the routing connector in the receivers of every pipeline its table and default_pipelines name, or remove the pipeline from the route.",
|
|
133
|
+
{ category: "correctness" },
|
|
134
|
+
),
|
|
135
|
+
OTEL116: auditRule(
|
|
136
|
+
"OTEL116",
|
|
137
|
+
"merge-worthy",
|
|
138
|
+
"guidance",
|
|
139
|
+
"Connector splits metrics by a high-cardinality GenAI attribute",
|
|
140
|
+
"Remove the conversation, response, tool call, session or user id, or the content key, from the connector's dimensions or attributes. Keep it on spans and logs, where a per-request value costs nothing extra.",
|
|
141
|
+
{ category: "efficiency" },
|
|
142
|
+
),
|
|
143
|
+
OTEL117: auditRule(
|
|
144
|
+
"OTEL117",
|
|
145
|
+
"merge-worthy",
|
|
146
|
+
"guidance",
|
|
147
|
+
"Two started components listen on the same address",
|
|
148
|
+
"Give one of them another port, or a specific host that doesn't overlap the other's. The collector's own metrics listen on localhost:8888 unless service.telemetry.metrics sets a reader or level none.",
|
|
149
|
+
{ category: "correctness" },
|
|
150
|
+
),
|
|
111
151
|
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL113-OTEL115 against the fixtures in testdata/: each failing fixture
|
|
3
|
+
* trips its own rule and nothing else, each passing fixture trips nothing.
|
|
4
|
+
*
|
|
5
|
+
* Where `otelcol-contrib` is on PATH (or `OTELCOL_BIN` names a contrib build),
|
|
6
|
+
* `otelcol validate` confirms the collector refuses each failing fixture and
|
|
7
|
+
* accepts each passing one. Without it those tests skip; CI does not install it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { load } from "js-yaml";
|
|
15
|
+
import { makePostSynthCtx } from "@intentius/chant-test-utils";
|
|
16
|
+
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
17
|
+
import { validateCollectorConfig } from "../../validate-config";
|
|
18
|
+
import type { CollectorConfig } from "../../model";
|
|
19
|
+
import { otel113 } from "./otel113";
|
|
20
|
+
import { otel114 } from "./otel114";
|
|
21
|
+
import { otel115 } from "./otel115";
|
|
22
|
+
|
|
23
|
+
const fixture = (name: string) => readFileSync(join(import.meta.dirname, "testdata", name), "utf-8");
|
|
24
|
+
|
|
25
|
+
const RULES: Array<[string, PostSynthCheck]> = [
|
|
26
|
+
["OTEL113", otel113],
|
|
27
|
+
["OTEL114", otel114],
|
|
28
|
+
["OTEL115", otel115],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function findOtelcol(): string | undefined {
|
|
32
|
+
const candidates = [process.env.OTELCOL_BIN, "otelcol-contrib"].filter((c): c is string => !!c);
|
|
33
|
+
for (const bin of candidates) {
|
|
34
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf-8" });
|
|
35
|
+
if (r.status === 0) return bin;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const OTELCOL = findOtelcol();
|
|
41
|
+
|
|
42
|
+
function otelcolValidate(file: string): { ok: boolean; output: string } {
|
|
43
|
+
const r = spawnSync(OTELCOL!, ["validate", `--config=${join(import.meta.dirname, "testdata", file)}`], {
|
|
44
|
+
encoding: "utf-8",
|
|
45
|
+
timeout: 60_000,
|
|
46
|
+
});
|
|
47
|
+
return { ok: r.status === 0, output: `${r.stdout ?? ""}${r.stderr ?? ""}` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe.each(RULES)("%s fixtures", (id, check) => {
|
|
51
|
+
const lower = id.toLowerCase();
|
|
52
|
+
|
|
53
|
+
test("the failing fixture trips this rule and no other", () => {
|
|
54
|
+
const yaml = fixture(`${lower}-fail.yaml`);
|
|
55
|
+
const diags = check.check(makePostSynthCtx("otel", yaml));
|
|
56
|
+
expect(diags).toHaveLength(1);
|
|
57
|
+
expect(diags[0]).toMatchObject({ checkId: id, severity: "error", lexicon: "otel" });
|
|
58
|
+
expect(validateCollectorConfig(load(yaml) as CollectorConfig).map((i) => i.code)).toEqual([id]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the passing fixture trips nothing", () => {
|
|
62
|
+
const yaml = fixture(`${lower}-pass.yaml`);
|
|
63
|
+
expect(check.check(makePostSynthCtx("otel", yaml))).toEqual([]);
|
|
64
|
+
expect(validateCollectorConfig(load(yaml) as CollectorConfig)).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test.skipIf(!OTELCOL)("otelcol validate refuses the failing fixture", () => {
|
|
68
|
+
const { ok, output } = otelcolValidate(`${lower}-fail.yaml`);
|
|
69
|
+
expect(ok, output).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test.skipIf(!OTELCOL)("otelcol validate accepts the passing fixture", () => {
|
|
73
|
+
const { ok, output } = otelcolValidate(`${lower}-pass.yaml`);
|
|
74
|
+
expect(ok, output).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("OTEL113 connector cycles", () => {
|
|
79
|
+
const config = (pipelines: string): CollectorConfig =>
|
|
80
|
+
load(`receivers: {otlp: {protocols: {grpc: {}}}}
|
|
81
|
+
exporters: {debug: {}}
|
|
82
|
+
connectors: {forward: {}, forward/2: {}, spanmetrics: {}}
|
|
83
|
+
service:
|
|
84
|
+
pipelines:
|
|
85
|
+
${pipelines}`) as CollectorConfig;
|
|
86
|
+
const cycles = (c: CollectorConfig) => validateCollectorConfig(c).filter((i) => i.code === "OTEL113");
|
|
87
|
+
|
|
88
|
+
test("names the cycle hop by hop", () => {
|
|
89
|
+
const [issue] = cycles(load(fixture("otel113-fail.yaml")) as CollectorConfig);
|
|
90
|
+
expect(issue.pipeline).toBe("traces/a");
|
|
91
|
+
expect(issue.message).toContain("traces/a -> forward/ab -> traces/b -> forward/ba -> traces/a");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("flags a pipeline that feeds itself through one connector", () => {
|
|
95
|
+
const issues = cycles(
|
|
96
|
+
config(` traces:
|
|
97
|
+
receivers: [otlp, forward]
|
|
98
|
+
exporters: [forward, debug]`),
|
|
99
|
+
);
|
|
100
|
+
expect(issues).toHaveLength(1);
|
|
101
|
+
expect(issues[0].message).toContain("traces -> forward -> traces");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("reports one cycle per group of pipelines, however many paths it holds", () => {
|
|
105
|
+
const issues = cycles(
|
|
106
|
+
config(` traces/a:
|
|
107
|
+
receivers: [otlp, forward/2]
|
|
108
|
+
exporters: [forward]
|
|
109
|
+
traces/b:
|
|
110
|
+
receivers: [forward]
|
|
111
|
+
exporters: [forward/2]
|
|
112
|
+
traces/c:
|
|
113
|
+
receivers: [forward]
|
|
114
|
+
exporters: [forward/2]`),
|
|
115
|
+
);
|
|
116
|
+
expect(issues.map((i) => i.pipeline)).toEqual(["traces/a"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a fan-out with no way back is not a cycle", () => {
|
|
120
|
+
expect(
|
|
121
|
+
cycles(
|
|
122
|
+
config(` traces:
|
|
123
|
+
receivers: [otlp]
|
|
124
|
+
exporters: [forward, spanmetrics]
|
|
125
|
+
traces/2:
|
|
126
|
+
receivers: [forward]
|
|
127
|
+
exporters: [debug]
|
|
128
|
+
metrics:
|
|
129
|
+
receivers: [spanmetrics]
|
|
130
|
+
exporters: [debug]`),
|
|
131
|
+
),
|
|
132
|
+
).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("an edge the connector cannot make does not close a cycle", () => {
|
|
136
|
+
// spanmetrics only goes traces to metrics, so metrics -> spanmetrics -> traces is no edge (OTEL112 reports it).
|
|
137
|
+
expect(
|
|
138
|
+
cycles(
|
|
139
|
+
config(` traces:
|
|
140
|
+
receivers: [otlp, spanmetrics]
|
|
141
|
+
exporters: [spanmetrics]
|
|
142
|
+
metrics:
|
|
143
|
+
receivers: [spanmetrics]
|
|
144
|
+
exporters: [debug]`),
|
|
145
|
+
),
|
|
146
|
+
).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("OTEL114 connector id collisions", () => {
|
|
151
|
+
test("flags a connector that shares its id with an exporter", () => {
|
|
152
|
+
const issues = validateCollectorConfig({
|
|
153
|
+
receivers: { otlp: {} },
|
|
154
|
+
exporters: { "forward/x": {}, debug: {} },
|
|
155
|
+
connectors: { "forward/x": {} },
|
|
156
|
+
service: { pipelines: { traces: { receivers: ["otlp"], exporters: ["debug"] } } },
|
|
157
|
+
}).filter((i) => i.code === "OTEL114");
|
|
158
|
+
expect(issues).toHaveLength(1);
|
|
159
|
+
expect(issues[0].component).toBe("forward/x");
|
|
160
|
+
expect(issues[0].message).toContain("declared exporter");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("names the receiver in the message", () => {
|
|
164
|
+
const [issue] = validateCollectorConfig(load(fixture("otel114-fail.yaml")) as CollectorConfig);
|
|
165
|
+
expect(issue.message).toContain('connector "datadog" has the same id as a declared receiver');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("OTEL115 routing targets", () => {
|
|
170
|
+
const routes = (c: CollectorConfig) => validateCollectorConfig(c).filter((i) => i.code === "OTEL115");
|
|
171
|
+
|
|
172
|
+
test("names where the target came from", () => {
|
|
173
|
+
const [issue] = routes(load(fixture("otel115-fail.yaml")) as CollectorConfig);
|
|
174
|
+
expect(issue).toMatchObject({ pipeline: "traces/other", component: "routing" });
|
|
175
|
+
expect(issue.message).toContain("(default_pipelines)");
|
|
176
|
+
expect(issue.message).toContain("does not list");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("flags a target pipeline that does not exist, once however often it is named", () => {
|
|
180
|
+
const config = load(fixture("otel115-pass.yaml")) as any;
|
|
181
|
+
config.connectors.routing.table.push(
|
|
182
|
+
{ statement: 'route() where attributes["tenant"] == "b"', pipelines: ["traces/missing"] },
|
|
183
|
+
{ statement: 'route() where attributes["tenant"] == "c"', pipelines: ["traces/missing"] },
|
|
184
|
+
);
|
|
185
|
+
const issues = routes(config);
|
|
186
|
+
expect(issues).toHaveLength(1);
|
|
187
|
+
expect(issues[0].message).toContain('"traces/missing" (table[1].pipelines), which is not declared');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("checks named routing connectors and leaves other connectors alone", () => {
|
|
191
|
+
const issues = routes({
|
|
192
|
+
receivers: { otlp: {} },
|
|
193
|
+
exporters: { debug: {} },
|
|
194
|
+
connectors: {
|
|
195
|
+
"routing/tenants": { table: [{ pipelines: ["traces/nowhere"] }] },
|
|
196
|
+
"forward": { table: [{ pipelines: ["traces/nowhere"] }] },
|
|
197
|
+
},
|
|
198
|
+
service: {
|
|
199
|
+
pipelines: {
|
|
200
|
+
traces: { receivers: ["otlp"], exporters: ["routing/tenants", "forward"] },
|
|
201
|
+
"traces/out": { receivers: ["routing/tenants", "forward"], exporters: ["debug"] },
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
expect(issues.map((i) => i.component)).toEqual(["routing/tenants"]);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -10,6 +10,11 @@ import { otel107 } from "./otel107";
|
|
|
10
10
|
import { otel108 } from "./otel108";
|
|
11
11
|
import { otel109 } from "./otel109";
|
|
12
12
|
import { otel112 } from "./otel112";
|
|
13
|
+
import { otel113 } from "./otel113";
|
|
14
|
+
import { otel114 } from "./otel114";
|
|
15
|
+
import { otel115 } from "./otel115";
|
|
16
|
+
import { otel116 } from "./otel116";
|
|
17
|
+
import { otel117 } from "./otel117";
|
|
13
18
|
|
|
14
19
|
export const postSynthChecks: PostSynthCheck[] = [
|
|
15
20
|
otel101,
|
|
@@ -22,4 +27,9 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
22
27
|
otel108,
|
|
23
28
|
otel109,
|
|
24
29
|
otel112,
|
|
30
|
+
otel113,
|
|
31
|
+
otel114,
|
|
32
|
+
otel115,
|
|
33
|
+
otel116,
|
|
34
|
+
otel117,
|
|
25
35
|
];
|