@intentius/chant-lexicon-otel 0.100.0 → 0.101.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 +110 -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/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/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/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/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 +6 -2
- package/dist/validate-config.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- 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 +219 -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 +146 -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 +223 -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/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 +415 -2
- package/src/validate.ts +17 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { load } from "js-yaml";
|
|
3
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
4
|
+
import { collectorYaml } from "../collector";
|
|
5
|
+
import { validateCollectorEntities } from "../validate-config";
|
|
6
|
+
import { COLLECTOR_PIN } from "../define";
|
|
7
|
+
import { Pipeline, Service } from "../pipeline";
|
|
8
|
+
import { DebugExporter } from "./exporters";
|
|
9
|
+
import { K8sClusterReceiver } from "./k8s-receivers";
|
|
10
|
+
import { K8sLeaderElectorExtension, type K8sLeaderElectorExtensionConfig } from "./extensions";
|
|
11
|
+
|
|
12
|
+
function problems(component: Declarable): string[] {
|
|
13
|
+
return validateCollectorEntities([component])
|
|
14
|
+
.filter((i) => i.code === "OTEL107")
|
|
15
|
+
.map((i) => i.message.replace(/^\w+ "[^"]+": /, ""));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const lease = { lease_name: "otel-k8s-cluster", lease_namespace: "observability" };
|
|
19
|
+
|
|
20
|
+
describe("k8s_leader_elector extension", () => {
|
|
21
|
+
test("is a built-in pinned to the collector release", () => {
|
|
22
|
+
expect(K8sLeaderElectorExtension.definition.kind).toBe("extension");
|
|
23
|
+
expect(K8sLeaderElectorExtension.definition.type).toBe("k8s_leader_elector");
|
|
24
|
+
expect(K8sLeaderElectorExtension.definition.builtin).toBe(true);
|
|
25
|
+
expect(K8sLeaderElectorExtension.definition.pin).toBe(COLLECTOR_PIN);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("a k8s_cluster receiver names it by componentId", () => {
|
|
29
|
+
const elector = new K8sLeaderElectorExtension({ name: "cluster", auth_type: "serviceAccount", ...lease, lease_duration: "20s" });
|
|
30
|
+
const cluster = new K8sClusterReceiver({ k8s_leader_elector: elector.componentId });
|
|
31
|
+
const debug = new DebugExporter({});
|
|
32
|
+
const yaml = collectorYaml([
|
|
33
|
+
cluster,
|
|
34
|
+
elector,
|
|
35
|
+
debug,
|
|
36
|
+
new Pipeline({ signal: "metrics", receivers: [cluster], exporters: [debug] }),
|
|
37
|
+
new Service({ extensions: [elector] }),
|
|
38
|
+
]);
|
|
39
|
+
expect(load(yaml)).toMatchObject({
|
|
40
|
+
receivers: { k8s_cluster: { k8s_leader_elector: "k8s_leader_elector/cluster" } },
|
|
41
|
+
extensions: {
|
|
42
|
+
"k8s_leader_elector/cluster": { auth_type: "serviceAccount", lease_name: "otel-k8s-cluster", lease_namespace: "observability", lease_duration: "20s" },
|
|
43
|
+
},
|
|
44
|
+
service: { extensions: ["k8s_leader_elector/cluster"] },
|
|
45
|
+
});
|
|
46
|
+
expect(problems(elector)).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("needs lease_name and lease_namespace", () => {
|
|
50
|
+
expect(problems(new K8sLeaderElectorExtension({ lease_name: "", lease_namespace: "obs" }))).toEqual([
|
|
51
|
+
"lease_name and lease_namespace must be set",
|
|
52
|
+
]);
|
|
53
|
+
expect(problems(new K8sLeaderElectorExtension({} as K8sLeaderElectorExtensionConfig))).toEqual([
|
|
54
|
+
"lease_name and lease_namespace must be set",
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("checks the durations the way client-go's leader elector does", () => {
|
|
59
|
+
expect(problems(new K8sLeaderElectorExtension({ ...lease, lease_duration: "10s" }))).toEqual([
|
|
60
|
+
"lease_duration must be greater than renew_deadline",
|
|
61
|
+
]);
|
|
62
|
+
expect(problems(new K8sLeaderElectorExtension({ ...lease, renew_deadline: "2400ms" }))).toEqual([
|
|
63
|
+
"renew_deadline must be greater than 1.2 times retry_period",
|
|
64
|
+
]);
|
|
65
|
+
expect(problems(new K8sLeaderElectorExtension({ ...lease, retry_period: "0s" }))).toEqual(["retry_period must be positive"]);
|
|
66
|
+
expect(problems(new K8sLeaderElectorExtension({ ...lease, lease_duration: "1m30s", renew_deadline: "1m", retry_period: "5s" }))).toEqual([]);
|
|
67
|
+
// A value this can't read (an env reference) is left to the collector.
|
|
68
|
+
expect(problems(new K8sLeaderElectorExtension({ ...lease, lease_duration: "${env:LEASE}" }))).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in extensions: health_check, pprof, zpages.
|
|
2
|
+
* Built-in extensions: health_check, pprof, zpages, k8s_leader_elector.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { defineBuiltin } from "../define";
|
|
6
|
-
import type
|
|
6
|
+
import { goDurationMs, type Duration, type TLSServerSettings } from "./common";
|
|
7
|
+
import type { K8sAuthType } from "./k8s-receivers";
|
|
7
8
|
|
|
8
9
|
export interface HealthCheckExtensionConfig {
|
|
9
10
|
/** Default `localhost:13133`. */
|
|
@@ -49,3 +50,60 @@ export const ZPagesExtension = defineBuiltin<ZPagesExtensionConfig, "extension",
|
|
|
49
50
|
description: "Serves zPages for live debugging of receivers and exporters",
|
|
50
51
|
endpoints: (c) => [c.endpoint ?? "localhost:55679"],
|
|
51
52
|
});
|
|
53
|
+
|
|
54
|
+
// ── k8s_leader_elector ───────────────────────────────────────────────
|
|
55
|
+
//
|
|
56
|
+
// Typed against extension/k8sleaderelector at COLLECTOR_PIN
|
|
57
|
+
// (collector-contrib v0.130.0): config.go, factory.go, leader_elector.go and README.md at that tag,
|
|
58
|
+
// https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.130.0/extension/k8sleaderelector
|
|
59
|
+
// The config squashes internal/k8sconfig's APIConfig (`auth_type`, `context`).
|
|
60
|
+
// Upstream marks it alpha. It ships in the otelcol-contrib and otelcol-k8s
|
|
61
|
+
// distributions at v0.130.0.
|
|
62
|
+
|
|
63
|
+
export interface K8sLeaderElectorExtensionConfig {
|
|
64
|
+
/** How to reach the Kubernetes API. Default `serviceAccount`. */
|
|
65
|
+
auth_type?: K8sAuthType;
|
|
66
|
+
/** With `auth_type: kubeConfig`, the kubeconfig context to use instead of the current one. */
|
|
67
|
+
context?: string;
|
|
68
|
+
/** The Lease object the replicas compete for. Required. */
|
|
69
|
+
lease_name: string;
|
|
70
|
+
/** The namespace of the Lease. Required. */
|
|
71
|
+
lease_namespace: string;
|
|
72
|
+
/** How long a lease is held without renewal. Default `15s`. */
|
|
73
|
+
lease_duration?: Duration;
|
|
74
|
+
/** How long the leader keeps retrying a renewal before giving up the lease; less than `lease_duration`. Default `10s`. */
|
|
75
|
+
renew_deadline?: Duration;
|
|
76
|
+
/** How long to wait between attempts to acquire or renew. Default `2s`. */
|
|
77
|
+
retry_period?: Duration;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Elects one leader among collector replicas through a Kubernetes Lease, so a
|
|
82
|
+
* receiver that names it (`k8s_cluster`'s `k8s_leader_elector`) collects only
|
|
83
|
+
* in the replica holding the lease. The collector's ServiceAccount needs
|
|
84
|
+
* access to `leases` in `coordination.k8s.io` in `lease_namespace`.
|
|
85
|
+
*/
|
|
86
|
+
export const K8sLeaderElectorExtension = defineBuiltin<K8sLeaderElectorExtensionConfig, "extension", "k8s_leader_elector">({
|
|
87
|
+
kind: "extension",
|
|
88
|
+
type: "k8s_leader_elector",
|
|
89
|
+
description: "Elects one leader among collector replicas through a Kubernetes Lease",
|
|
90
|
+
validate: (c) => {
|
|
91
|
+
const problems: string[] = [];
|
|
92
|
+
// config.go Validate.
|
|
93
|
+
if (!c.lease_name || !c.lease_namespace) problems.push("lease_name and lease_namespace must be set");
|
|
94
|
+
// client-go's leaderelection.NewLeaderElector, which leader_elector.go calls when the extension starts.
|
|
95
|
+
const lease = goDurationMs(c.lease_duration, 15_000);
|
|
96
|
+
const renew = goDurationMs(c.renew_deadline, 10_000);
|
|
97
|
+
const retry = goDurationMs(c.retry_period, 2_000);
|
|
98
|
+
for (const [key, ms] of [["lease_duration", lease], ["renew_deadline", renew], ["retry_period", retry]] as const) {
|
|
99
|
+
if (ms !== undefined && ms <= 0) problems.push(`${key} must be positive`);
|
|
100
|
+
}
|
|
101
|
+
if (lease !== undefined && renew !== undefined && lease > 0 && renew > 0 && lease <= renew) {
|
|
102
|
+
problems.push("lease_duration must be greater than renew_deadline");
|
|
103
|
+
}
|
|
104
|
+
if (renew !== undefined && retry !== undefined && renew > 0 && retry > 0 && renew <= 1.2 * retry) {
|
|
105
|
+
problems.push("renew_deadline must be greater than 1.2 times retry_period");
|
|
106
|
+
}
|
|
107
|
+
return problems;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { load } from "js-yaml";
|
|
3
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
4
|
+
import { collectorYaml } from "../collector";
|
|
5
|
+
import { validateCollectorEntities } from "../validate-config";
|
|
6
|
+
import { COLLECTOR_PIN } from "../define";
|
|
7
|
+
import { Pipeline } from "../pipeline";
|
|
8
|
+
import { DebugExporter } from "./exporters";
|
|
9
|
+
import { SumConnector } from "./connectors";
|
|
10
|
+
import { OtlpReceiver } from "./receivers";
|
|
11
|
+
import { DeltaToCumulativeProcessor, type DeltaToCumulativeProcessorConfig } from "./processors";
|
|
12
|
+
|
|
13
|
+
function problems(component: Declarable): string[] {
|
|
14
|
+
return validateCollectorEntities([component])
|
|
15
|
+
.filter((i) => i.code === "OTEL107")
|
|
16
|
+
.map((i) => i.message.replace(/^\w+ "[^"]+": /, ""));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("deltatocumulative processor", () => {
|
|
20
|
+
test("is a built-in pinned to the collector release", () => {
|
|
21
|
+
expect(DeltaToCumulativeProcessor.definition.kind).toBe("processor");
|
|
22
|
+
expect(DeltaToCumulativeProcessor.definition.type).toBe("deltatocumulative");
|
|
23
|
+
expect(DeltaToCumulativeProcessor.definition.builtin).toBe(true);
|
|
24
|
+
expect(DeltaToCumulativeProcessor.definition.pin).toBe(COLLECTOR_PIN);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("sits in a metrics pipeline after a delta-emitting connector", () => {
|
|
28
|
+
const otlp = new OtlpReceiver({ protocols: { grpc: {} } });
|
|
29
|
+
const sum = new SumConnector({ spans: { "tokens": { source_attribute: "gen_ai.usage.input_tokens" } } });
|
|
30
|
+
const d2c = new DeltaToCumulativeProcessor({ max_stale: "10m", max_streams: 1000 });
|
|
31
|
+
const debug = new DebugExporter({});
|
|
32
|
+
const config = load(
|
|
33
|
+
collectorYaml([
|
|
34
|
+
otlp,
|
|
35
|
+
new Pipeline({ signal: "traces", receivers: [otlp], exporters: [sum] }),
|
|
36
|
+
new Pipeline({ signal: "metrics", receivers: [sum], processors: [d2c], exporters: [debug] }),
|
|
37
|
+
]),
|
|
38
|
+
) as any;
|
|
39
|
+
expect(config.processors.deltatocumulative).toEqual({ max_stale: "10m", max_streams: 1000 });
|
|
40
|
+
expect(config.service.pipelines.metrics.processors).toEqual(["deltatocumulative"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test.each<[string, DeltaToCumulativeProcessorConfig, string[]]>([
|
|
44
|
+
["defaults", {}, []],
|
|
45
|
+
["set", { max_stale: "1h", max_streams: 0 }, []],
|
|
46
|
+
["zero max_stale", { max_stale: "0s" }, ['max_stale must be a positive duration (got "0s")']],
|
|
47
|
+
["bare zero max_stale", { max_stale: "0" }, ['max_stale must be a positive duration (got "0")']],
|
|
48
|
+
["negative max_stale", { max_stale: "-5m" }, ['max_stale must be a positive duration (got "-5m")']],
|
|
49
|
+
["negative max_streams", { max_streams: -1 }, ["max_streams must be a whole number, 0 or more (got -1)"]],
|
|
50
|
+
["fractional max_streams", { max_streams: 1.5 }, ["max_streams must be a whole number, 0 or more (got 1.5)"]],
|
|
51
|
+
])("checks what config.go Validate checks: %s", (_label, config, expected) => {
|
|
52
|
+
expect(problems(new DeltaToCumulativeProcessor(config))).toEqual(expected);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in processors: batch, memory_limiter, resource, attributes,
|
|
3
|
-
* k8sattributes, resourcedetection.
|
|
3
|
+
* k8sattributes, resourcedetection, deltatocumulative.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { defineBuiltin } from "../define";
|
|
7
|
-
import type
|
|
7
|
+
import { goDurationMs, type AttributeAction, type Duration } from "./common";
|
|
8
8
|
|
|
9
9
|
// ── batch ────────────────────────────────────────────────────────────
|
|
10
10
|
|
|
@@ -210,3 +210,42 @@ export const ResourceDetectionProcessor = defineBuiltin<
|
|
|
210
210
|
description: "Detects cloud, container and host resource attributes",
|
|
211
211
|
validate: (c) => (c.detectors?.length ? [] : ["detectors is empty, so nothing is detected"]),
|
|
212
212
|
});
|
|
213
|
+
|
|
214
|
+
// ── deltatocumulative ────────────────────────────────────────────────
|
|
215
|
+
//
|
|
216
|
+
// Typed against processor/deltatocumulativeprocessor at COLLECTOR_PIN
|
|
217
|
+
// (collector-contrib v0.130.0): config.go, factory.go and README.md at that tag,
|
|
218
|
+
// https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.130.0/processor/deltatocumulativeprocessor
|
|
219
|
+
// Upstream marks it alpha for metrics. It ships in the otelcol-contrib and
|
|
220
|
+
// otelcol-k8s distributions at v0.130.0, and keeps every stream's running
|
|
221
|
+
// total in memory, so one collector instance must see all of a stream's
|
|
222
|
+
// deltas for its totals to be right.
|
|
223
|
+
|
|
224
|
+
export interface DeltaToCumulativeProcessorConfig {
|
|
225
|
+
/** How long a stream that receives no samples is kept before it is dropped. Positive. Default `5m`. */
|
|
226
|
+
max_stale?: Duration;
|
|
227
|
+
/** The most streams tracked at once; samples of new streams past it are dropped. Not negative. Default unlimited. */
|
|
228
|
+
max_streams?: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Turns delta metrics into cumulative ones by keeping a running total per
|
|
233
|
+
* stream. Exporters that read only cumulative data, such as
|
|
234
|
+
* `prometheusremotewrite`, need it after connectors that emit deltas (`sum`,
|
|
235
|
+
* `count`, `signaltometrics`). Cumulative metrics pass through unchanged.
|
|
236
|
+
*/
|
|
237
|
+
export const DeltaToCumulativeProcessor = defineBuiltin<DeltaToCumulativeProcessorConfig, "processor", "deltatocumulative">({
|
|
238
|
+
kind: "processor",
|
|
239
|
+
type: "deltatocumulative",
|
|
240
|
+
description: "Converts delta metrics to cumulative by keeping a running total per stream",
|
|
241
|
+
validate: (c) => {
|
|
242
|
+
// config.go Validate.
|
|
243
|
+
const problems: string[] = [];
|
|
244
|
+
const stale = goDurationMs(c.max_stale, 5 * 60_000);
|
|
245
|
+
if (stale !== undefined && stale <= 0) problems.push(`max_stale must be a positive duration (got ${JSON.stringify(c.max_stale)})`);
|
|
246
|
+
if (c.max_streams !== undefined && (!Number.isInteger(c.max_streams) || c.max_streams < 0)) {
|
|
247
|
+
problems.push(`max_streams must be a whole number, 0 or more (got ${c.max_streams})`);
|
|
248
|
+
}
|
|
249
|
+
return problems;
|
|
250
|
+
},
|
|
251
|
+
});
|
|
@@ -35,6 +35,11 @@ export const OtlpReceiver = defineBuiltin<OtlpReceiverConfig, "receiver", "otlp"
|
|
|
35
35
|
if (c.protocols && "http" in c.protocols) out.push(c.protocols.http?.endpoint ?? "localhost:4318");
|
|
36
36
|
return out;
|
|
37
37
|
},
|
|
38
|
+
// The HTTP receiver takes protobuf and JSON bodies on the same port.
|
|
39
|
+
protocols: (c) => [
|
|
40
|
+
...(c.protocols && "grpc" in c.protocols ? ["grpc"] : []),
|
|
41
|
+
...(c.protocols && "http" in c.protocols ? ["http/protobuf", "http/json"] : []),
|
|
42
|
+
],
|
|
38
43
|
});
|
|
39
44
|
|
|
40
45
|
// ── prometheus ───────────────────────────────────────────────────────
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composite catalog matches what this lexicon exports (#2662).
|
|
3
|
+
*
|
|
4
|
+
* `catalog.ts` is generated by `npm run generate:composite-catalogs` and is
|
|
5
|
+
* what `chant serve mcp` lists.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import { isCompositeDefinition } from "@intentius/chant";
|
|
10
|
+
import * as composites from "./index";
|
|
11
|
+
import { compositeCatalog } from "./catalog";
|
|
12
|
+
import { otelPlugin } from "../plugin";
|
|
13
|
+
|
|
14
|
+
const exported = Object.entries(composites)
|
|
15
|
+
.filter(([, value]) => isCompositeDefinition(value))
|
|
16
|
+
.map(([name]) => name)
|
|
17
|
+
.sort();
|
|
18
|
+
const catalogued = compositeCatalog.map((entry) => entry.name).sort();
|
|
19
|
+
|
|
20
|
+
describe("the otel composite catalog", () => {
|
|
21
|
+
test("every exported composite has an entry, and every entry names an exported composite", () => {
|
|
22
|
+
expect(exported).toEqual(["NodeAgent"]);
|
|
23
|
+
expect(catalogued, "regenerate with `npm run generate:composite-catalogs -- otel`").toEqual(exported);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("every entry says what it is and names the components it bundles", () => {
|
|
27
|
+
for (const entry of compositeCatalog) {
|
|
28
|
+
expect(entry.lexicon, entry.name).toBe("otel");
|
|
29
|
+
expect(entry.description.length, entry.name).toBeGreaterThan(0);
|
|
30
|
+
expect(entry.bundles, entry.name).toContain("Pipeline");
|
|
31
|
+
expect(entry.bundles, entry.name).toContain("OtlpReceiver");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("the plugin contributes it", () => {
|
|
36
|
+
expect(otelPlugin.composites?.()).toBe(compositeCatalog);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Generated by scripts/generate-composite-catalogs.ts (#2662) from this
|
|
2
|
+
// lexicon's composite exports. Do not edit by hand: run
|
|
3
|
+
// `npm run generate:composite-catalogs`. catalog.test.ts holds it to the exports.
|
|
4
|
+
import type { CompositeEntry } from "@intentius/chant/lexicon";
|
|
5
|
+
|
|
6
|
+
export const compositeCatalog: CompositeEntry[] = [
|
|
7
|
+
{
|
|
8
|
+
"name": "NodeAgent",
|
|
9
|
+
"lexicon": "otel",
|
|
10
|
+
"description": "The collector config of a per-node Kubernetes agent.",
|
|
11
|
+
"bundles": [
|
|
12
|
+
"BatchProcessor",
|
|
13
|
+
"FileLogReceiver",
|
|
14
|
+
"HealthCheckExtension",
|
|
15
|
+
"HostMetricsReceiver",
|
|
16
|
+
"K8sAttributesProcessor",
|
|
17
|
+
"KubeletStatsReceiver",
|
|
18
|
+
"MemoryLimiterProcessor",
|
|
19
|
+
"OtlpReceiver",
|
|
20
|
+
"Pipeline",
|
|
21
|
+
"ResourceDetectionProcessor",
|
|
22
|
+
"ResourceProcessor"
|
|
23
|
+
],
|
|
24
|
+
"params": [
|
|
25
|
+
{
|
|
26
|
+
"name": "exporters",
|
|
27
|
+
"type": "Exporter[]",
|
|
28
|
+
"required": true,
|
|
29
|
+
"description": "Where traces and logs go: usually the gateway (the k8s lexicon's `gatewayExporter()`), or an OTLP backend."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "metricExporters",
|
|
33
|
+
"type": "Exporter[]",
|
|
34
|
+
"required": false,
|
|
35
|
+
"description": "Where metrics go, e.g. a `prometheus` exporter for a scrape on each node."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "clusterName",
|
|
39
|
+
"type": "string",
|
|
40
|
+
"required": false,
|
|
41
|
+
"description": "Sets `k8s.cluster.name` on everything the agent sends."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "nodeNameEnv",
|
|
45
|
+
"type": "string",
|
|
46
|
+
"required": false,
|
|
47
|
+
"description": "The environment variable holding this node's name, set from `spec.nodeName` (default `K8S_NODE_NAME`)."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "hostMetrics",
|
|
51
|
+
"type": "boolean | { interval?: Duration; rootPath?: string }",
|
|
52
|
+
"required": false,
|
|
53
|
+
"description": "Node CPU, memory, load, filesystem and network from the host root at `/hostfs` (default: on, every 30s)."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "containerLogs",
|
|
57
|
+
"type": "boolean | { selfContainer?: string }",
|
|
58
|
+
"required": false,
|
|
59
|
+
"description": "Container logs from `/var/log/pods` (default: on)."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "kubeletStats",
|
|
63
|
+
"type": "boolean | { interval?: Duration }",
|
|
64
|
+
"required": false,
|
|
65
|
+
"description": "Pod and container resource metrics from this node's kubelet (default: off)."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "memoryLimitMib",
|
|
69
|
+
"type": "number",
|
|
70
|
+
"required": false,
|
|
71
|
+
"description": "`memory_limiter`'s hard limit in MiB (default 400; the spike limit is a quarter of it)."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "healthCheck",
|
|
75
|
+
"type": "boolean",
|
|
76
|
+
"required": false,
|
|
77
|
+
"description": "Serve `health_check` on 0.0.0.0:13133 (default: on)."
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
];
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `NodeAgent` composite: validation, defaults, the switches, and that the
|
|
3
|
+
* config it declares passes the lexicon's own config checks.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import { load } from "js-yaml";
|
|
7
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
8
|
+
import { NodeAgent, nodeAgentPropsProblem, type NodeAgentProps } from "./index";
|
|
9
|
+
import { OtlpExporter, PrometheusExporter } from "../components/exporters";
|
|
10
|
+
import { collectorYaml } from "../collector";
|
|
11
|
+
import { validateCollectorConfig } from "../validate-config";
|
|
12
|
+
import type { CollectorConfig } from "../model";
|
|
13
|
+
|
|
14
|
+
const gateway = new OtlpExporter({ name: "gateway", endpoint: "otel-gateway.observability.svc:4317", tls: { insecure: true } });
|
|
15
|
+
const base: NodeAgentProps = { exporters: [gateway] };
|
|
16
|
+
|
|
17
|
+
/** A section of the built config, read loosely: the assertions name the keys they expect. */
|
|
18
|
+
type Loose = Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
19
|
+
|
|
20
|
+
function configOf(props: NodeAgentProps): CollectorConfig {
|
|
21
|
+
const agent = NodeAgent(props);
|
|
22
|
+
return load(collectorYaml(Object.values(agent.members) as Declarable[])) as CollectorConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("NodeAgent validation", () => {
|
|
26
|
+
test("needs at least one exporter", () => {
|
|
27
|
+
expect(() => NodeAgent({ exporters: [] })).toThrow(/NodeAgent: exporters must name at least one exporter/);
|
|
28
|
+
expect(nodeAgentPropsProblem({ ...base, metricExporters: [] })).toMatch(/metricExporters/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("rejects a memory limit that is not a positive whole number, and a node env var that is not a name", () => {
|
|
32
|
+
expect(() => NodeAgent({ ...base, memoryLimitMib: 0 })).toThrow(/memoryLimitMib/);
|
|
33
|
+
expect(() => NodeAgent({ ...base, memoryLimitMib: 1.5 })).toThrow(/memoryLimitMib/);
|
|
34
|
+
expect(() => NodeAgent({ ...base, nodeNameEnv: "NODE-NAME" })).toThrow(/not an environment variable name/);
|
|
35
|
+
expect(nodeAgentPropsProblem(base)).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("NodeAgent defaults", () => {
|
|
40
|
+
const config = configOf(base);
|
|
41
|
+
|
|
42
|
+
test("the config passes every config check", () => {
|
|
43
|
+
expect(validateCollectorConfig(config)).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("OTLP, host metrics and container logs in; memory_limiter first and batch last", () => {
|
|
47
|
+
expect(Object.keys(config.receivers ?? {}).sort()).toEqual(["filelog", "hostmetrics", "otlp"]);
|
|
48
|
+
const pipelines = config.service?.pipelines ?? {};
|
|
49
|
+
expect(Object.keys(pipelines).sort()).toEqual(["logs", "metrics", "traces"]);
|
|
50
|
+
for (const p of Object.values(pipelines)) {
|
|
51
|
+
expect(p.processors).toEqual(["memory_limiter", "k8sattributes", "resourcedetection", "batch"]);
|
|
52
|
+
expect(p.exporters).toEqual(["otlp/gateway"]);
|
|
53
|
+
}
|
|
54
|
+
expect(pipelines.metrics.receivers).toEqual(["otlp", "hostmetrics"]);
|
|
55
|
+
expect(pipelines.logs.receivers).toEqual(["filelog", "otlp"]);
|
|
56
|
+
expect(pipelines.traces.receivers).toEqual(["otlp"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("k8sattributes keeps to this node's pods and matches log records by pod uid", () => {
|
|
60
|
+
const k8s = config.processors?.k8sattributes as Loose;
|
|
61
|
+
expect(k8s.filter).toEqual({ node_from_env_var: "K8S_NODE_NAME" });
|
|
62
|
+
expect(k8s.extract.metadata).toContain("k8s.pod.uid");
|
|
63
|
+
expect(k8s.pod_association.map((a: Loose) => a.sources[0].name ?? a.sources[0].from)).toEqual(["k8s.pod.ip", "k8s.pod.uid", "connection"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the agent's own container logs are left out", () => {
|
|
67
|
+
const filelog = config.receivers?.filelog as Loose;
|
|
68
|
+
expect(filelog.include).toEqual(["/var/log/pods/*/*/*.log"]);
|
|
69
|
+
expect(filelog.exclude).toEqual(["/var/log/pods/*/otel-collector/*.log"]);
|
|
70
|
+
expect(filelog.operators).toEqual([{ type: "container", id: "container-parser" }]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("memory_limiter defaults to 400 MiB with a quarter of it as spike limit, and health_check is served", () => {
|
|
74
|
+
expect(config.processors?.memory_limiter).toMatchObject({ limit_mib: 400, spike_limit_mib: 100 });
|
|
75
|
+
expect(config.service?.extensions).toEqual(["health_check"]);
|
|
76
|
+
expect(config.extensions?.health_check).toEqual({ endpoint: "0.0.0.0:13133" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("no tail sampling and no kubeletstats", () => {
|
|
80
|
+
expect(config.processors?.tail_sampling).toBeUndefined();
|
|
81
|
+
expect(config.receivers?.kubeletstats).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("NodeAgent options", () => {
|
|
86
|
+
test("clusterName adds a resource processor before batch", () => {
|
|
87
|
+
const config = configOf({ ...base, clusterName: "prod-eu-1" });
|
|
88
|
+
expect(config.service?.pipelines?.traces.processors).toEqual(["memory_limiter", "k8sattributes", "resourcedetection", "resource", "batch"]);
|
|
89
|
+
expect(config.processors?.resource).toEqual({ attributes: [{ key: "k8s.cluster.name", value: "prod-eu-1", action: "upsert" }] });
|
|
90
|
+
expect(validateCollectorConfig(config)).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("metricExporters sends metrics elsewhere", () => {
|
|
94
|
+
const scrape = new PrometheusExporter({ endpoint: "0.0.0.0:8889" });
|
|
95
|
+
const config = configOf({ ...base, metricExporters: [scrape] });
|
|
96
|
+
expect(config.service?.pipelines?.metrics.exporters).toEqual(["prometheus"]);
|
|
97
|
+
expect(config.service?.pipelines?.traces.exporters).toEqual(["otlp/gateway"]);
|
|
98
|
+
expect(validateCollectorConfig(config)).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("kubeletStats reads this node's kubelet with the service account, named by the node env var", () => {
|
|
102
|
+
const config = configOf({ ...base, kubeletStats: { interval: "1m" }, nodeNameEnv: "NODE_NAME" });
|
|
103
|
+
expect(config.receivers?.kubeletstats).toEqual({
|
|
104
|
+
collection_interval: "1m",
|
|
105
|
+
auth_type: "serviceAccount",
|
|
106
|
+
endpoint: "https://${env:NODE_NAME}:10250",
|
|
107
|
+
insecure_skip_verify: true,
|
|
108
|
+
node: "${env:NODE_NAME}",
|
|
109
|
+
});
|
|
110
|
+
expect(config.service?.pipelines?.metrics.receivers).toEqual(["otlp", "hostmetrics", "kubeletstats"]);
|
|
111
|
+
expect((config.processors?.k8sattributes as Loose).filter).toEqual({ node_from_env_var: "NODE_NAME" });
|
|
112
|
+
expect(validateCollectorConfig(config)).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("host metrics, container logs and health_check can be turned off; OTLP stays", () => {
|
|
116
|
+
const agent = NodeAgent({ ...base, hostMetrics: false, containerLogs: false, healthCheck: false });
|
|
117
|
+
expect(Object.keys(agent.members).sort()).toEqual([
|
|
118
|
+
"batch",
|
|
119
|
+
"k8sAttributes",
|
|
120
|
+
"logs",
|
|
121
|
+
"memoryLimiter",
|
|
122
|
+
"metrics",
|
|
123
|
+
"otlp",
|
|
124
|
+
"resourceDetection",
|
|
125
|
+
"traces",
|
|
126
|
+
]);
|
|
127
|
+
const config = configOf({ ...base, hostMetrics: false, containerLogs: false, healthCheck: false });
|
|
128
|
+
expect(Object.keys(config.receivers ?? {})).toEqual(["otlp"]);
|
|
129
|
+
expect(config.service?.extensions).toBeUndefined();
|
|
130
|
+
expect(validateCollectorConfig(config)).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("host metrics and container logs take their own settings", () => {
|
|
134
|
+
const config = configOf({ ...base, hostMetrics: { interval: "1m", rootPath: "/host" }, containerLogs: { selfContainer: "agent" } });
|
|
135
|
+
expect(config.receivers?.hostmetrics).toMatchObject({ collection_interval: "1m", root_path: "/host" });
|
|
136
|
+
expect((config.receivers?.filelog as Loose).exclude).toEqual(["/var/log/pods/*/agent/*.log"]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* otel lexicon composites: declarations that expand to a collector config's components and pipelines.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { NodeAgent, nodeAgentPropsProblem } from "./node-agent";
|
|
6
|
+
export type { NodeAgentProps, NodeAgentMembers, NodeAgentInstance } from "./node-agent";
|