@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,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";
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NodeAgent`: the collector config a per-node agent runs on Kubernetes.
|
|
3
|
+
*
|
|
4
|
+
* One collector per node (a DaemonSet) takes OTLP from the pods on its node,
|
|
5
|
+
* reads the node's own telemetry, puts Kubernetes metadata on all of it, and
|
|
6
|
+
* hands it on, usually to a gateway tier. This composite declares that
|
|
7
|
+
* config:
|
|
8
|
+
*
|
|
9
|
+
* - receivers: `otlp` (gRPC on 4317, HTTP on 4318), and by default
|
|
10
|
+
* `hostmetrics` (CPU, memory, load, filesystem, network of the node, read
|
|
11
|
+
* under `/hostfs`) and `filelog` (container logs under `/var/log/pods`,
|
|
12
|
+
* parsed by the `container` operator). `kubeletstats` (pod and container
|
|
13
|
+
* resource metrics from the local kubelet) is opt-in.
|
|
14
|
+
* - processors, in order: `memory_limiter`; `k8sattributes`, filtered to
|
|
15
|
+
* pods on this node and matching by pod IP, pod UID or the connection;
|
|
16
|
+
* `resourcedetection` (`env`, `system`); a `resource` processor setting
|
|
17
|
+
* `k8s.cluster.name` when `clusterName` is given; `batch`.
|
|
18
|
+
* - a `traces`, a `metrics` and a `logs` pipeline, each through those
|
|
19
|
+
* processors to the given exporters.
|
|
20
|
+
* - a `health_check` extension on 0.0.0.0:13133.
|
|
21
|
+
*
|
|
22
|
+
* Tail sampling is left out on purpose: an agent sees only its node's
|
|
23
|
+
* spans of a trace, so a sampling decision there is made on partial traces.
|
|
24
|
+
* Sample on the gateway.
|
|
25
|
+
*
|
|
26
|
+
* What the pod has to provide: the node name in an environment variable
|
|
27
|
+
* (`K8S_NODE_NAME` by default, from `spec.nodeName`), the host root mounted
|
|
28
|
+
* read-only at `/hostfs` for `hostmetrics`, `/var/log/pods` mounted
|
|
29
|
+
* read-only for `filelog`, and a service account that may get, list and
|
|
30
|
+
* watch pods, namespaces and nodes (and replicasets, for
|
|
31
|
+
* `k8s.deployment.name`). The k8s lexicon's `OtelCollector` takes this
|
|
32
|
+
* composite's entities as its `config` and derives all of that from them:
|
|
33
|
+
* the variable, the read-only host mounts, group 0 for the root-owned log
|
|
34
|
+
* files, the RBAC and the ports.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { Composite, type CompositeInstance } from "@intentius/chant/composite";
|
|
38
|
+
import type { Duration } from "../components/common";
|
|
39
|
+
import { OtlpReceiver, HostMetricsReceiver, FileLogReceiver, type HostMetricsReceiverConfig } from "../components/receivers";
|
|
40
|
+
import { KubeletStatsReceiver } from "../components/k8s-receivers";
|
|
41
|
+
import {
|
|
42
|
+
BatchProcessor,
|
|
43
|
+
K8sAttributesProcessor,
|
|
44
|
+
MemoryLimiterProcessor,
|
|
45
|
+
ResourceDetectionProcessor,
|
|
46
|
+
ResourceProcessor,
|
|
47
|
+
type K8sAttributesProcessorConfig,
|
|
48
|
+
} from "../components/processors";
|
|
49
|
+
import { HealthCheckExtension } from "../components/extensions";
|
|
50
|
+
import type { OTelComponent } from "../define";
|
|
51
|
+
import { Pipeline, type PipelineEntity } from "../pipeline";
|
|
52
|
+
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
type Exporter = OTelComponent<"exporter", string, any>;
|
|
55
|
+
type Component<K extends "receiver" | "processor" | "extension"> = OTelComponent<K, string, object>;
|
|
56
|
+
|
|
57
|
+
export interface NodeAgentProps {
|
|
58
|
+
/**
|
|
59
|
+
* Where traces and logs go: usually the gateway (the k8s lexicon's
|
|
60
|
+
* `gatewayExporter()`), or an OTLP backend. Also where metrics go, unless
|
|
61
|
+
* `metricExporters` is set.
|
|
62
|
+
*/
|
|
63
|
+
exporters: Exporter[];
|
|
64
|
+
/** Where metrics go, e.g. a `prometheus` exporter for a scrape on each node. Default: `exporters`. */
|
|
65
|
+
metricExporters?: Exporter[];
|
|
66
|
+
/** Sets `k8s.cluster.name` on everything the agent sends. */
|
|
67
|
+
clusterName?: string;
|
|
68
|
+
/** The environment variable holding this node's name, set from `spec.nodeName` (default `K8S_NODE_NAME`). */
|
|
69
|
+
nodeNameEnv?: string;
|
|
70
|
+
/** Node CPU, memory, load, filesystem and network from the host root at `/hostfs` (default: on, every 30s). */
|
|
71
|
+
hostMetrics?: boolean | { interval?: Duration; rootPath?: string };
|
|
72
|
+
/**
|
|
73
|
+
* Container logs from `/var/log/pods` (default: on). The agent's own
|
|
74
|
+
* container is left out so the agent doesn't read its own log lines; name
|
|
75
|
+
* it with `selfContainer` (default `otel-collector`, the k8s
|
|
76
|
+
* `OtelCollector`'s default).
|
|
77
|
+
*/
|
|
78
|
+
containerLogs?: boolean | { selfContainer?: string };
|
|
79
|
+
/** Pod and container resource metrics from this node's kubelet (default: off). The service account then needs get on `nodes/stats`. */
|
|
80
|
+
kubeletStats?: boolean | { interval?: Duration };
|
|
81
|
+
/** `memory_limiter`'s hard limit in MiB (default 400; the spike limit is a quarter of it). */
|
|
82
|
+
memoryLimitMib?: number;
|
|
83
|
+
/** Serve `health_check` on 0.0.0.0:13133 (default: on). */
|
|
84
|
+
healthCheck?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A type alias, not an interface: a composite's members type needs the implicit index signature.
|
|
88
|
+
export type NodeAgentMembers = {
|
|
89
|
+
otlp: Component<"receiver">;
|
|
90
|
+
hostMetrics?: Component<"receiver">;
|
|
91
|
+
containerLogs?: Component<"receiver">;
|
|
92
|
+
kubeletStats?: Component<"receiver">;
|
|
93
|
+
memoryLimiter: Component<"processor">;
|
|
94
|
+
k8sAttributes: Component<"processor">;
|
|
95
|
+
resourceDetection: Component<"processor">;
|
|
96
|
+
cluster?: Component<"processor">;
|
|
97
|
+
batch: Component<"processor">;
|
|
98
|
+
health?: Component<"extension">;
|
|
99
|
+
traces: PipelineEntity;
|
|
100
|
+
metrics: PipelineEntity;
|
|
101
|
+
logs: PipelineEntity;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type NodeAgentInstance = CompositeInstance<NodeAgentMembers> & NodeAgentMembers;
|
|
105
|
+
|
|
106
|
+
const HOST_SCRAPERS: HostMetricsReceiverConfig["scrapers"] = { cpu: {}, memory: {}, load: {}, filesystem: {}, network: {} };
|
|
107
|
+
|
|
108
|
+
/** Pod metadata for every signal. `k8s.pod.uid` is what the `container` log parser sets on log records. */
|
|
109
|
+
const POD_METADATA: K8sAttributesProcessorConfig["extract"] = {
|
|
110
|
+
metadata: ["k8s.namespace.name", "k8s.pod.name", "k8s.pod.uid", "k8s.deployment.name", "k8s.node.name"],
|
|
111
|
+
};
|
|
112
|
+
const POD_ASSOCIATION: K8sAttributesProcessorConfig["pod_association"] = [
|
|
113
|
+
{ sources: [{ from: "resource_attribute", name: "k8s.pod.ip" }] },
|
|
114
|
+
{ sources: [{ from: "resource_attribute", name: "k8s.pod.uid" }] },
|
|
115
|
+
{ sources: [{ from: "connection" }] },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
function option<T extends object>(value: boolean | T | undefined, onByDefault: boolean): T | undefined {
|
|
119
|
+
if (value === undefined) return onByDefault ? ({} as T) : undefined;
|
|
120
|
+
if (value === false) return undefined;
|
|
121
|
+
return value === true ? ({} as T) : value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Why a set of `NodeAgent` props can't build, or undefined when they can. */
|
|
125
|
+
export function nodeAgentPropsProblem(props: NodeAgentProps): string | undefined {
|
|
126
|
+
if (!Array.isArray(props.exporters) || props.exporters.length === 0) return "exporters must name at least one exporter";
|
|
127
|
+
if (props.metricExporters !== undefined && props.metricExporters.length === 0) return "metricExporters must name at least one exporter when set";
|
|
128
|
+
if (props.memoryLimitMib !== undefined && !(Number.isInteger(props.memoryLimitMib) && props.memoryLimitMib > 0)) {
|
|
129
|
+
return "memoryLimitMib must be a positive whole number";
|
|
130
|
+
}
|
|
131
|
+
if (props.nodeNameEnv !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(props.nodeNameEnv)) {
|
|
132
|
+
return `nodeNameEnv "${props.nodeNameEnv}" is not an environment variable name`;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The collector config of a per-node Kubernetes agent: OTLP from the node's
|
|
139
|
+
* pods, host metrics and container logs from the node itself, Kubernetes
|
|
140
|
+
* metadata on everything, sent on to the given exporters.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* import { NodeAgent, OtlpExporter } from "@intentius/chant-lexicon-otel";
|
|
145
|
+
*
|
|
146
|
+
* const gateway = new OtlpExporter({ name: "gateway", endpoint: "otel-gateway.observability.svc:4317", tls: { insecure: true } });
|
|
147
|
+
* export const agent = NodeAgent({ exporters: [gateway], clusterName: "prod-eu-1" });
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export const NodeAgent = Composite<NodeAgentProps, NodeAgentMembers>((props) => {
|
|
151
|
+
const problem = nodeAgentPropsProblem(props);
|
|
152
|
+
if (problem) throw new Error(`NodeAgent: ${problem}`);
|
|
153
|
+
|
|
154
|
+
const nodeNameEnv = props.nodeNameEnv ?? "K8S_NODE_NAME";
|
|
155
|
+
const limit = props.memoryLimitMib ?? 400;
|
|
156
|
+
const host = option(props.hostMetrics, true);
|
|
157
|
+
const logFiles = option(props.containerLogs, true);
|
|
158
|
+
const kubelet = option(props.kubeletStats, false);
|
|
159
|
+
|
|
160
|
+
const otlp = new OtlpReceiver({
|
|
161
|
+
protocols: { grpc: { endpoint: "0.0.0.0:4317" }, http: { endpoint: "0.0.0.0:4318" } },
|
|
162
|
+
});
|
|
163
|
+
const hostMetrics = host
|
|
164
|
+
? new HostMetricsReceiver({ collection_interval: host.interval ?? "30s", root_path: host.rootPath ?? "/hostfs", scrapers: HOST_SCRAPERS })
|
|
165
|
+
: undefined;
|
|
166
|
+
const containerLogs = logFiles
|
|
167
|
+
? new FileLogReceiver({
|
|
168
|
+
include: ["/var/log/pods/*/*/*.log"],
|
|
169
|
+
exclude: [`/var/log/pods/*/${logFiles.selfContainer ?? "otel-collector"}/*.log`],
|
|
170
|
+
start_at: "end",
|
|
171
|
+
include_file_path: true,
|
|
172
|
+
operators: [{ type: "container", id: "container-parser" }],
|
|
173
|
+
})
|
|
174
|
+
: undefined;
|
|
175
|
+
const kubeletStats = kubelet
|
|
176
|
+
? new KubeletStatsReceiver({
|
|
177
|
+
collection_interval: kubelet.interval ?? "30s",
|
|
178
|
+
auth_type: "serviceAccount",
|
|
179
|
+
endpoint: `https://\${env:${nodeNameEnv}}:10250`,
|
|
180
|
+
insecure_skip_verify: true,
|
|
181
|
+
node: `\${env:${nodeNameEnv}}`,
|
|
182
|
+
})
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
const memoryLimiter = new MemoryLimiterProcessor({ check_interval: "1s", limit_mib: limit, spike_limit_mib: Math.ceil(limit / 4) });
|
|
186
|
+
const k8sAttributes = new K8sAttributesProcessor({
|
|
187
|
+
auth_type: "serviceAccount",
|
|
188
|
+
filter: { node_from_env_var: nodeNameEnv },
|
|
189
|
+
extract: POD_METADATA,
|
|
190
|
+
pod_association: POD_ASSOCIATION,
|
|
191
|
+
});
|
|
192
|
+
const resourceDetection = new ResourceDetectionProcessor({ detectors: ["env", "system"], timeout: "5s", override: false });
|
|
193
|
+
const cluster = props.clusterName
|
|
194
|
+
? new ResourceProcessor({ attributes: [{ key: "k8s.cluster.name", value: props.clusterName, action: "upsert" }] })
|
|
195
|
+
: undefined;
|
|
196
|
+
const batch = new BatchProcessor({ timeout: "10s" });
|
|
197
|
+
const processors = [memoryLimiter, k8sAttributes, resourceDetection, ...(cluster ? [cluster] : []), batch];
|
|
198
|
+
|
|
199
|
+
const exporters = props.exporters;
|
|
200
|
+
const metricExporters = props.metricExporters ?? exporters;
|
|
201
|
+
const traces = new Pipeline({ signal: "traces", receivers: [otlp], processors, exporters });
|
|
202
|
+
const metrics = new Pipeline({
|
|
203
|
+
signal: "metrics",
|
|
204
|
+
receivers: [otlp, ...(hostMetrics ? [hostMetrics] : []), ...(kubeletStats ? [kubeletStats] : [])],
|
|
205
|
+
processors,
|
|
206
|
+
exporters: metricExporters,
|
|
207
|
+
});
|
|
208
|
+
const logs = new Pipeline({ signal: "logs", receivers: [...(containerLogs ? [containerLogs] : []), otlp], processors, exporters });
|
|
209
|
+
|
|
210
|
+
const health = props.healthCheck === false ? undefined : new HealthCheckExtension({ endpoint: "0.0.0.0:13133" });
|
|
211
|
+
|
|
212
|
+
// A composite member must be a declarable, so the components that are off are left out rather than undefined.
|
|
213
|
+
const members: NodeAgentMembers = { otlp, memoryLimiter, k8sAttributes, resourceDetection, batch, traces, metrics, logs };
|
|
214
|
+
if (hostMetrics) members.hostMetrics = hostMetrics;
|
|
215
|
+
if (containerLogs) members.containerLogs = containerLogs;
|
|
216
|
+
if (kubeletStats) members.kubeletStats = kubeletStats;
|
|
217
|
+
if (cluster) members.cluster = cluster;
|
|
218
|
+
if (health) members.health = health;
|
|
219
|
+
return members;
|
|
220
|
+
}, "NodeAgent");
|
package/src/configmap.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collector configs held in Kubernetes ConfigMaps (chant #2930).
|
|
3
|
+
*
|
|
4
|
+
* `OtelCollector`, `OtelCollectorGateway`, `GkeOtelCollector` and most
|
|
5
|
+
* hand-written manifests carry the collector config as a YAML string under a
|
|
6
|
+
* ConfigMap data key, usually `config.yaml`. The otel config checks
|
|
7
|
+
* (OTEL101-OTEL106, OTEL112) and the k8s placement checks (WK8601-WK8603)
|
|
8
|
+
* both read configs from there, through these two functions.
|
|
9
|
+
*
|
|
10
|
+
* Values are parsed with js-yaml rather than core's YAML reader, which keeps
|
|
11
|
+
* flow lists (`[otlp, batch]`) as strings (chant #2925, #3006).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { load } from "js-yaml";
|
|
15
|
+
import type { CollectorConfig } from "./model";
|
|
16
|
+
|
|
17
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
18
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a ConfigMap value as a collector config: YAML with a
|
|
23
|
+
* `service.pipelines` map. Undefined for anything else.
|
|
24
|
+
*/
|
|
25
|
+
export function parseCollectorConfig(text: unknown): CollectorConfig | undefined {
|
|
26
|
+
if (typeof text !== "string") return undefined;
|
|
27
|
+
let value: unknown;
|
|
28
|
+
try {
|
|
29
|
+
value = load(text);
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!isRecord(value) || !isRecord(value.service) || !isRecord(value.service.pipelines)) return undefined;
|
|
34
|
+
return value as unknown as CollectorConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One collector config found in a ConfigMap. */
|
|
38
|
+
export interface ConfigMapCollectorConfig {
|
|
39
|
+
/** `metadata.namespace`, or `default` when unset. */
|
|
40
|
+
namespace: string;
|
|
41
|
+
name: string;
|
|
42
|
+
/** The data key that holds the config, e.g. `config.yaml`. */
|
|
43
|
+
key: string;
|
|
44
|
+
config: CollectorConfig;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The collector configs in one parsed manifest: every `data` value of a
|
|
49
|
+
* ConfigMap that parses as a collector config. Empty for any other document.
|
|
50
|
+
*/
|
|
51
|
+
export function configMapCollectorConfigs(doc: unknown): ConfigMapCollectorConfig[] {
|
|
52
|
+
if (!isRecord(doc) || doc.kind !== "ConfigMap" || !isRecord(doc.data)) return [];
|
|
53
|
+
const metadata = isRecord(doc.metadata) ? doc.metadata : {};
|
|
54
|
+
if (typeof metadata.name !== "string") return [];
|
|
55
|
+
const namespace = typeof metadata.namespace === "string" && metadata.namespace.length > 0 ? metadata.namespace : "default";
|
|
56
|
+
const out: ConfigMapCollectorConfig[] = [];
|
|
57
|
+
for (const [key, text] of Object.entries(doc.data)) {
|
|
58
|
+
const config = parseCollectorConfig(text);
|
|
59
|
+
if (config) out.push({ namespace, name: metadata.name, key, config });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** How a ConfigMap config is named in a message: `ConfigMap observability/otel-agent-config, key config.yaml`. */
|
|
65
|
+
export function describeConfigMapConfig(c: Pick<ConfigMapCollectorConfig, "namespace" | "name" | "key">): string {
|
|
66
|
+
return `ConfigMap ${c.namespace}/${c.name}, key ${c.key}`;
|
|
67
|
+
}
|
package/src/connectors.test.ts
CHANGED
|
@@ -15,8 +15,11 @@ import {
|
|
|
15
15
|
PrometheusExporter,
|
|
16
16
|
RoutingConnector,
|
|
17
17
|
ServiceGraphConnector,
|
|
18
|
+
SignalToMetricsConnector,
|
|
18
19
|
SpanMetricsConnector,
|
|
19
20
|
SumConnector,
|
|
21
|
+
signalToMetricsEntries,
|
|
22
|
+
type SignalToMetricsConnectorConfig,
|
|
20
23
|
} from "./components";
|
|
21
24
|
import { collectorYaml } from "./collector";
|
|
22
25
|
import { COLLECTOR_PIN, componentEntityType, defineComponent } from "./define";
|
|
@@ -62,7 +65,15 @@ describe("connector as a component kind", () => {
|
|
|
62
65
|
});
|
|
63
66
|
|
|
64
67
|
test("the built-ins are connectors pinned to the collector release", () => {
|
|
65
|
-
for (const Cls of [
|
|
68
|
+
for (const Cls of [
|
|
69
|
+
SpanMetricsConnector,
|
|
70
|
+
ServiceGraphConnector,
|
|
71
|
+
RoutingConnector,
|
|
72
|
+
ForwardConnector,
|
|
73
|
+
CountConnector,
|
|
74
|
+
SumConnector,
|
|
75
|
+
SignalToMetricsConnector,
|
|
76
|
+
]) {
|
|
66
77
|
expect(Cls.definition.kind).toBe("connector");
|
|
67
78
|
expect(Cls.definition.builtin).toBe(true);
|
|
68
79
|
expect(Cls.definition.pin).toEqual(COLLECTOR_PIN);
|
|
@@ -332,6 +343,180 @@ describe("connector config rules (OTEL107)", () => {
|
|
|
332
343
|
});
|
|
333
344
|
});
|
|
334
345
|
|
|
346
|
+
/** Spans into a duration histogram and logs into a count, both feeding one metrics pipeline through signaltometrics. */
|
|
347
|
+
function signalToMetricsCollector(): Declarable[] {
|
|
348
|
+
const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
|
|
349
|
+
const s2m = new SignalToMetricsConnector({
|
|
350
|
+
name: "genai",
|
|
351
|
+
spans: [
|
|
352
|
+
{
|
|
353
|
+
name: "gen_ai.client.operation.duration",
|
|
354
|
+
description: "GenAI operation duration",
|
|
355
|
+
unit: "s",
|
|
356
|
+
conditions: ['attributes["gen_ai.operation.name"] != nil'],
|
|
357
|
+
attributes: [{ key: "gen_ai.operation.name" }, { key: "gen_ai.request.model", default_value: "unknown" }, { key: "error.type", optional: true }],
|
|
358
|
+
histogram: { buckets: [0.01, 0.1, 1, 10], value: "Double(Microseconds(end_time - start_time)) / 1000000.0" },
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
logs: [{ name: "logrecord.count", sum: { value: "1" } }],
|
|
362
|
+
});
|
|
363
|
+
const debug = new DebugExporter({ verbosity: "basic" });
|
|
364
|
+
const prom = new PrometheusExporter({ endpoint: "0.0.0.0:8889" });
|
|
365
|
+
return [
|
|
366
|
+
otlp,
|
|
367
|
+
s2m,
|
|
368
|
+
debug,
|
|
369
|
+
prom,
|
|
370
|
+
new Pipeline({ signal: "traces", receivers: [otlp], exporters: [debug, s2m] }),
|
|
371
|
+
new Pipeline({ signal: "logs", receivers: [otlp], exporters: [s2m] }),
|
|
372
|
+
new Pipeline({ signal: "metrics", name: "genai", receivers: [s2m], exporters: [prom] }),
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
describe("signaltometrics", () => {
|
|
377
|
+
const problems = (config: SignalToMetricsConnectorConfig) =>
|
|
378
|
+
validateCollectorEntities([new SignalToMetricsConnector(config)]).map((i) => i.message);
|
|
379
|
+
|
|
380
|
+
test("is typed against the pinned release and reads every signal into metrics", () => {
|
|
381
|
+
expect(SignalToMetricsConnector.definition.type).toBe("signaltometrics");
|
|
382
|
+
expect(SignalToMetricsConnector.definition.pin).toBe(COLLECTOR_PIN);
|
|
383
|
+
expect(SignalToMetricsConnector.definition.connects).toEqual([
|
|
384
|
+
{ from: "traces", to: "metrics" },
|
|
385
|
+
{ from: "metrics", to: "metrics" },
|
|
386
|
+
{ from: "logs", to: "metrics" },
|
|
387
|
+
{ from: "profiles", to: "metrics" },
|
|
388
|
+
]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("serializes each signal's metric list, and the pipelines it joins pass every check", () => {
|
|
392
|
+
const entities = signalToMetricsCollector();
|
|
393
|
+
const config = load(collectorYaml(entities)) as CollectorConfig;
|
|
394
|
+
expect(config.connectors?.["signaltometrics/genai"]).toEqual({
|
|
395
|
+
spans: [
|
|
396
|
+
{
|
|
397
|
+
name: "gen_ai.client.operation.duration",
|
|
398
|
+
description: "GenAI operation duration",
|
|
399
|
+
unit: "s",
|
|
400
|
+
conditions: ['attributes["gen_ai.operation.name"] != nil'],
|
|
401
|
+
attributes: [
|
|
402
|
+
{ key: "gen_ai.operation.name" },
|
|
403
|
+
{ key: "gen_ai.request.model", default_value: "unknown" },
|
|
404
|
+
{ key: "error.type", optional: true },
|
|
405
|
+
],
|
|
406
|
+
histogram: { buckets: [0.01, 0.1, 1, 10], value: "Double(Microseconds(end_time - start_time)) / 1000000.0" },
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
logs: [{ name: "logrecord.count", sum: { value: "1" } }],
|
|
410
|
+
});
|
|
411
|
+
expect(validateCollectorConfig(config)).toEqual([]);
|
|
412
|
+
expect(validateCollectorEntities(entities)).toEqual([]);
|
|
413
|
+
expect(collectorTopologyOf(entities).edges.map((e) => [e.from, e.to])).toEqual([
|
|
414
|
+
["traces", "metrics/genai"],
|
|
415
|
+
["logs", "metrics/genai"],
|
|
416
|
+
]);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("OTEL112: it cannot feed a traces pipeline", () => {
|
|
420
|
+
const yaml = `receivers:
|
|
421
|
+
otlp: {protocols: {grpc: {}}}
|
|
422
|
+
exporters:
|
|
423
|
+
debug: {}
|
|
424
|
+
connectors:
|
|
425
|
+
signaltometrics:
|
|
426
|
+
spans: [{name: n, sum: {value: "1"}}]
|
|
427
|
+
service:
|
|
428
|
+
pipelines:
|
|
429
|
+
traces:
|
|
430
|
+
receivers: [otlp]
|
|
431
|
+
exporters: [signaltometrics]
|
|
432
|
+
traces/out:
|
|
433
|
+
receivers: [signaltometrics]
|
|
434
|
+
exporters: [debug]
|
|
435
|
+
`;
|
|
436
|
+
expect(otel112.check(ctxFor(yaml)).map((d) => d.entity)).toEqual(["signaltometrics", "signaltometrics"]);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("entries list each metric with the types it names", () => {
|
|
440
|
+
const entries = signalToMetricsEntries({
|
|
441
|
+
spans: [{ name: "a", sum: { value: "1" } }],
|
|
442
|
+
datapoints: [{ name: "b", gauge: { value: "value_double" } }],
|
|
443
|
+
});
|
|
444
|
+
expect(entries.map((e) => [e.signal, e.index, e.metric.name, e.types])).toEqual([
|
|
445
|
+
["spans", 0, "a", ["sum"]],
|
|
446
|
+
["datapoints", 0, "b", ["gauge"]],
|
|
447
|
+
]);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("OTEL107 passes valid entries of every metric type", () => {
|
|
451
|
+
expect(
|
|
452
|
+
problems({
|
|
453
|
+
spans: [
|
|
454
|
+
{ name: "s.sum", sum: { value: "Int(AdjustedCount())" } },
|
|
455
|
+
{ name: "s.hist", histogram: { value: "Milliseconds(end_time - start_time)", count: "1" } },
|
|
456
|
+
{ name: "s.exp", exponential_histogram: { max_size: 160, value: "Microseconds(end_time - start_time)" } },
|
|
457
|
+
],
|
|
458
|
+
logs: [{ name: "l.gauge", gauge: { value: 'ExtractGrokPatterns(body, "Memory usage %{NUMBER:memory_mb:int}MB")["memory_mb"]' } }],
|
|
459
|
+
profiles: [{ name: "p.count", sum: { value: "1" } }],
|
|
460
|
+
}),
|
|
461
|
+
).toEqual([]);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("OTEL107: each entry names exactly one metric type, with a value", () => {
|
|
465
|
+
const bad = {
|
|
466
|
+
spans: [
|
|
467
|
+
{ name: "none" },
|
|
468
|
+
{ name: "two", sum: { value: "1" }, histogram: { value: "1" } },
|
|
469
|
+
{ name: "nohistvalue", histogram: { count: "1" } },
|
|
470
|
+
{ name: "nosumvalue", sum: {} },
|
|
471
|
+
],
|
|
472
|
+
} as unknown as SignalToMetricsConnectorConfig;
|
|
473
|
+
expect(problems(bad)).toEqual([
|
|
474
|
+
'connector "signaltometrics": spans[0] (none): name exactly one metric type (sum, gauge, histogram or exponential_histogram); found none',
|
|
475
|
+
'connector "signaltometrics": spans[1] (two): name exactly one metric type (sum, gauge, histogram or exponential_histogram); found sum and histogram',
|
|
476
|
+
'connector "signaltometrics": spans[2] (nohistvalue): histogram.value is missing',
|
|
477
|
+
'connector "signaltometrics": spans[3] (nosumvalue): sum.value is missing',
|
|
478
|
+
]);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("OTEL107: names, attributes, buckets, max_size and grok selectors", () => {
|
|
482
|
+
const bad = {
|
|
483
|
+
logs: [
|
|
484
|
+
{ name: "", sum: { value: "1" } },
|
|
485
|
+
{
|
|
486
|
+
name: "attrs",
|
|
487
|
+
attributes: [{ key: "a", default_value: "x", optional: true }, { key: "b" }, { key: "b" }, { key: "" }],
|
|
488
|
+
histogram: { buckets: [1, 5, 5], value: "1" },
|
|
489
|
+
},
|
|
490
|
+
{ name: "exp", exponential_histogram: { max_size: 1, value: "1" } },
|
|
491
|
+
{ name: "grok", gauge: { value: 'ExtractGrokPatterns(body, "%{NUMBER:n:int}")' } },
|
|
492
|
+
],
|
|
493
|
+
} as unknown as SignalToMetricsConnectorConfig;
|
|
494
|
+
expect(problems(bad)).toEqual([
|
|
495
|
+
'connector "signaltometrics": logs[0]: name is missing',
|
|
496
|
+
'connector "signaltometrics": logs[1] (attrs): histogram.buckets must increase',
|
|
497
|
+
'connector "signaltometrics": logs[1] (attrs): attributes "a": set default_value or optional, not both',
|
|
498
|
+
'connector "signaltometrics": logs[1] (attrs): attributes "b" is listed twice',
|
|
499
|
+
'connector "signaltometrics": logs[1] (attrs): attributes[3]: key is missing',
|
|
500
|
+
'connector "signaltometrics": logs[2] (exp): exponential_histogram.max_size must be between 2 and 16384',
|
|
501
|
+
'connector "signaltometrics": logs[3] (grok): gauge.value with ExtractGrokPatterns needs one key selector, ExtractGrokPatterns(...)["key"]',
|
|
502
|
+
]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("OTEL107: a connector with no metrics", () => {
|
|
506
|
+
expect(problems({})).toEqual([
|
|
507
|
+
'connector "signaltometrics": no metric is configured under spans, datapoints, logs or profiles; the collector refuses the connector',
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("the type allows exactly one metric type per entry", () => {
|
|
512
|
+
// @ts-expect-error: two metric types
|
|
513
|
+
const two: SignalToMetricsConnectorConfig = { spans: [{ name: "x", sum: { value: "1" }, gauge: { value: "1" } }] };
|
|
514
|
+
// @ts-expect-error: no metric type
|
|
515
|
+
const none: SignalToMetricsConnectorConfig = { spans: [{ name: "x" }] };
|
|
516
|
+
expect([two, none]).toHaveLength(2);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
335
520
|
// `otelcol validate` is the acceptance test for the emitted YAML. CI has no
|
|
336
521
|
// collector binary, so this runs only when one is on PATH (the contrib build,
|
|
337
522
|
// which ships the connectors) and is skipped, with the reason shown, when not.
|
|
@@ -365,6 +550,17 @@ describe.skipIf(!otelcol)(
|
|
|
365
550
|
}
|
|
366
551
|
});
|
|
367
552
|
|
|
553
|
+
test("the signaltometrics example validates", () => {
|
|
554
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-otel-connectors-"));
|
|
555
|
+
try {
|
|
556
|
+
const file = join(dir, "config.yaml");
|
|
557
|
+
writeFileSync(file, collectorYaml(signalToMetricsCollector()));
|
|
558
|
+
expect(() => execFileSync(otelcol!, ["validate", `--config=${file}`], { stdio: "pipe" })).not.toThrow();
|
|
559
|
+
} finally {
|
|
560
|
+
rmSync(dir, { recursive: true, force: true });
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
368
564
|
test("a spanmetrics signal mismatch is refused by the collector too", () => {
|
|
369
565
|
const dir = mkdtempSync(join(tmpdir(), "chant-otel-connectors-"));
|
|
370
566
|
try {
|