@intentius/chant-lexicon-otel 0.81.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 +49 -0
- package/dist/catalog.d.ts +18 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +8 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +19 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +7 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/collector.d.ts +29 -0
- package/dist/collector.d.ts.map +1 -0
- package/dist/components/common.d.ts +144 -0
- package/dist/components/common.d.ts.map +1 -0
- package/dist/components/exporters.d.ts +86 -0
- package/dist/components/exporters.d.ts.map +1 -0
- package/dist/components/extensions.d.ts +32 -0
- package/dist/components/extensions.d.ts.map +1 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/processors.d.ts +118 -0
- package/dist/components/processors.d.ts.map +1 -0
- package/dist/components/receivers.d.ts +118 -0
- package/dist/components/receivers.d.ts.map +1 -0
- package/dist/define.d.ts +136 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/detect.d.ts +2 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +25 -0
- package/dist/lint/audit-catalog.d.ts +13 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/otel-helpers.d.ts +22 -0
- package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/otel101.d.ts +8 -0
- package/dist/lint/post-synth/otel101.d.ts.map +1 -0
- package/dist/lint/post-synth/otel102.d.ts +8 -0
- package/dist/lint/post-synth/otel102.d.ts.map +1 -0
- package/dist/lint/post-synth/otel103.d.ts +8 -0
- package/dist/lint/post-synth/otel103.d.ts.map +1 -0
- package/dist/lint/post-synth/otel104.d.ts +8 -0
- package/dist/lint/post-synth/otel104.d.ts.map +1 -0
- package/dist/lint/post-synth/otel105.d.ts +8 -0
- package/dist/lint/post-synth/otel105.d.ts.map +1 -0
- package/dist/lint/post-synth/otel106.d.ts +8 -0
- package/dist/lint/post-synth/otel106.d.ts.map +1 -0
- package/dist/lint/post-synth/otel107.d.ts +8 -0
- package/dist/lint/post-synth/otel107.d.ts.map +1 -0
- package/dist/lint/post-synth/otel108.d.ts +8 -0
- package/dist/lint/post-synth/otel108.d.ts.map +1 -0
- package/dist/lint/post-synth/otel109.d.ts +8 -0
- package/dist/lint/post-synth/otel109.d.ts.map +1 -0
- package/dist/lint/rules/component-id-syntax.d.ts +14 -0
- package/dist/lint/rules/component-id-syntax.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +6 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/literal-credential.d.ts +13 -0
- package/dist/lint/rules/literal-credential.d.ts.map +1 -0
- package/dist/lint/rules/otel-ast.d.ts +14 -0
- package/dist/lint/rules/otel-ast.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +4 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +4 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +8 -0
- package/dist/meta.json +102 -0
- package/dist/model.d.ts +53 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/okf/index.md +40 -0
- package/dist/okf/rules/OTEL001.md +15 -0
- package/dist/okf/rules/OTEL002.md +11 -0
- package/dist/okf/rules/OTEL101.md +11 -0
- package/dist/okf/rules/OTEL102.md +11 -0
- package/dist/okf/rules/OTEL103.md +11 -0
- package/dist/okf/rules/OTEL104.md +11 -0
- package/dist/okf/rules/OTEL105.md +11 -0
- package/dist/okf/rules/OTEL106.md +11 -0
- package/dist/okf/rules/OTEL107.md +11 -0
- package/dist/okf/rules/OTEL108.md +11 -0
- package/dist/okf/rules/OTEL109.md +11 -0
- package/dist/okf/types/AttributesProcessor.md +9 -0
- package/dist/okf/types/BatchProcessor.md +9 -0
- package/dist/okf/types/DebugExporter.md +9 -0
- package/dist/okf/types/FileLogReceiver.md +9 -0
- package/dist/okf/types/GoogleCloudExporter.md +9 -0
- package/dist/okf/types/HealthCheckExtension.md +9 -0
- package/dist/okf/types/HostMetricsReceiver.md +9 -0
- package/dist/okf/types/K8sAttributesProcessor.md +9 -0
- package/dist/okf/types/MemoryLimiterProcessor.md +9 -0
- package/dist/okf/types/OtlpExporter.md +9 -0
- package/dist/okf/types/OtlpHttpExporter.md +9 -0
- package/dist/okf/types/OtlpReceiver.md +9 -0
- package/dist/okf/types/Pipeline.md +13 -0
- package/dist/okf/types/PprofExtension.md +9 -0
- package/dist/okf/types/PrometheusExporter.md +9 -0
- package/dist/okf/types/PrometheusReceiver.md +9 -0
- package/dist/okf/types/ResourceDetectionProcessor.md +9 -0
- package/dist/okf/types/ResourceProcessor.md +9 -0
- package/dist/okf/types/Service.md +9 -0
- package/dist/okf/types/ZPagesExtension.md +9 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/pipeline.d.ts +66 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/platform.d.ts +57 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/rules/component-id-syntax.ts +68 -0
- package/dist/rules/literal-credential.ts +62 -0
- package/dist/rules/otel-ast.ts +30 -0
- package/dist/rules/otel-helpers.ts +70 -0
- package/dist/rules/otel101.ts +17 -0
- package/dist/rules/otel102.ts +17 -0
- package/dist/rules/otel103.ts +17 -0
- package/dist/rules/otel104.ts +17 -0
- package/dist/rules/otel105.ts +17 -0
- package/dist/rules/otel106.ts +17 -0
- package/dist/rules/otel107.ts +17 -0
- package/dist/rules/otel108.ts +17 -0
- package/dist/rules/otel109.ts +17 -0
- package/dist/serializer.d.ts +16 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skill-defs.d.ts +3 -0
- package/dist/skill-defs.d.ts.map +1 -0
- package/dist/skills/chant-otel-custom-components.md +51 -0
- package/dist/skills/chant-otel-platforms.md +34 -0
- package/dist/skills/chant-otel.md +60 -0
- package/dist/topology.d.ts +56 -0
- package/dist/topology.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate-config.d.ts +30 -0
- package/dist/validate-config.d.ts.map +1 -0
- package/dist/validate.d.ts +11 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/yaml.d.ts +27 -0
- package/dist/yaml.d.ts.map +1 -0
- package/package.json +74 -0
- package/src/catalog.ts +56 -0
- package/src/codegen/docs-cli.ts +4 -0
- package/src/codegen/docs.ts +90 -0
- package/src/codegen/generate-cli.ts +8 -0
- package/src/codegen/generate.ts +50 -0
- package/src/codegen/package.ts +41 -0
- package/src/collector.ts +147 -0
- package/src/components/common.ts +151 -0
- package/src/components/exporters.ts +141 -0
- package/src/components/extensions.ts +51 -0
- package/src/components/index.ts +5 -0
- package/src/components/processors.ts +208 -0
- package/src/components/receivers.ts +177 -0
- package/src/define.test.ts +131 -0
- package/src/define.ts +249 -0
- package/src/detect.ts +11 -0
- package/src/generated/lexicon-otel.json +102 -0
- package/src/index.ts +73 -0
- package/src/lint/audit-catalog.ts +102 -0
- package/src/lint/post-synth/index.ts +23 -0
- package/src/lint/post-synth/otel-helpers.ts +70 -0
- package/src/lint/post-synth/otel101.ts +17 -0
- package/src/lint/post-synth/otel102.ts +17 -0
- package/src/lint/post-synth/otel103.ts +17 -0
- package/src/lint/post-synth/otel104.ts +17 -0
- package/src/lint/post-synth/otel105.ts +17 -0
- package/src/lint/post-synth/otel106.ts +17 -0
- package/src/lint/post-synth/otel107.ts +17 -0
- package/src/lint/post-synth/otel108.ts +17 -0
- package/src/lint/post-synth/otel109.ts +17 -0
- package/src/lint/post-synth/post-synth.test.ts +184 -0
- package/src/lint/rules/component-id-syntax.ts +68 -0
- package/src/lint/rules/index.ts +9 -0
- package/src/lint/rules/literal-credential.ts +62 -0
- package/src/lint/rules/otel-ast.ts +30 -0
- package/src/lint/rules/rules.test.ts +70 -0
- package/src/lsp/completions.test.ts +23 -0
- package/src/lsp/completions.ts +15 -0
- package/src/lsp/hover.test.ts +20 -0
- package/src/lsp/hover.ts +30 -0
- package/src/model.ts +88 -0
- package/src/package-cli.ts +17 -0
- package/src/pipeline.ts +89 -0
- package/src/platform.test.ts +79 -0
- package/src/platform.ts +143 -0
- package/src/plugin.test.ts +55 -0
- package/src/plugin.ts +111 -0
- package/src/serializer.test.ts +187 -0
- package/src/serializer.ts +31 -0
- package/src/skill-defs.ts +36 -0
- package/src/skills/chant-otel-custom-components.md +51 -0
- package/src/skills/chant-otel-platforms.md +34 -0
- package/src/skills/chant-otel.md +60 -0
- package/src/topology.test.ts +80 -0
- package/src/topology.ts +122 -0
- package/src/validate-cli.ts +7 -0
- package/src/validate-config.ts +232 -0
- package/src/validate.ts +82 -0
- package/src/yaml.test.ts +56 -0
- package/src/yaml.ts +145 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-otel
|
|
3
|
+
description: Declare OpenTelemetry Collector config (receivers, processors, exporters, pipelines) as typed chant entities and build collector YAML
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# OpenTelemetry Collector config with chant
|
|
8
|
+
|
|
9
|
+
The otel lexicon (`@intentius/chant-lexicon-otel`) types collector config. Each receiver, processor, exporter and extension is an entity, pipelines reference them, and `chant build` writes one collector config file.
|
|
10
|
+
|
|
11
|
+
## Project setup
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// chant.config.ts
|
|
15
|
+
export default { lexicons: ["otel"] };
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Declaring components
|
|
19
|
+
|
|
20
|
+
Every component class takes the collector's own config keys (snake_case, as in the collector docs) plus an optional `name`. The id is `type`, or `type/name` when `name` is set.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import {
|
|
24
|
+
OtlpReceiver, MemoryLimiterProcessor, BatchProcessor, OtlpExporter, DebugExporter, Pipeline,
|
|
25
|
+
} from "@intentius/chant-lexicon-otel";
|
|
26
|
+
|
|
27
|
+
export const otlp = new OtlpReceiver({
|
|
28
|
+
protocols: { grpc: { endpoint: "0.0.0.0:4317" }, http: { endpoint: "0.0.0.0:4318" } },
|
|
29
|
+
});
|
|
30
|
+
export const limiter = new MemoryLimiterProcessor({ check_interval: "1s", limit_percentage: 80, spike_limit_percentage: 20 });
|
|
31
|
+
export const batch = new BatchProcessor({ timeout: "5s" });
|
|
32
|
+
export const backend = new OtlpExporter({ name: "backend", endpoint: "tempo.observability:4317", tls: { insecure: true } });
|
|
33
|
+
|
|
34
|
+
export const traces = new Pipeline({ signal: "traces", receivers: [otlp], processors: [limiter, batch], exporters: [backend] });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Emits `receivers.otlp`, `processors.memory_limiter` and `processors.batch`, `exporters.otlp/backend`, and `service.pipelines.traces`.
|
|
38
|
+
|
|
39
|
+
## Built-in set
|
|
40
|
+
|
|
41
|
+
| Kind | Types |
|
|
42
|
+
|---|---|
|
|
43
|
+
| receivers | otlp, prometheus, hostmetrics, filelog |
|
|
44
|
+
| processors | batch, memory_limiter, resource, attributes, k8sattributes, resourcedetection |
|
|
45
|
+
| exporters | otlp, otlphttp, debug, prometheus, googlecloud |
|
|
46
|
+
| extensions | health_check, pprof, zpages |
|
|
47
|
+
|
|
48
|
+
Anything else goes through `defineComponent`; see the `chant-otel-custom-components` skill.
|
|
49
|
+
|
|
50
|
+
## Rules
|
|
51
|
+
|
|
52
|
+
- Reference components by entity where you can. A string id (`"otlp/backend"`) is allowed for a component declared elsewhere, and OTEL101 fails the build if nothing declares it.
|
|
53
|
+
- Put `memory_limiter` first in `processors` (OTEL105).
|
|
54
|
+
- Never write a credential literally. Use `"${env:NAME}"` and the collector reads it at start-up (OTEL002).
|
|
55
|
+
- Every pipeline needs at least one receiver and one exporter (OTEL102). A declared component no pipeline uses is a warning (OTEL103).
|
|
56
|
+
- Declared extensions are enabled in declaration order unless a `Service` lists `extensions` itself.
|
|
57
|
+
|
|
58
|
+
## Reading the result
|
|
59
|
+
|
|
60
|
+
`collectorTopologyOf(entities)` returns the pipelines, each component's endpoints and schema pin, and for each exporter the signals it carries, as plain data.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { load } from "js-yaml";
|
|
3
|
+
import { collectorTopology, collectorTopologyOf } from "./topology";
|
|
4
|
+
import { collectorYaml } from "./collector";
|
|
5
|
+
import {
|
|
6
|
+
BatchProcessor,
|
|
7
|
+
DebugExporter,
|
|
8
|
+
GoogleCloudExporter,
|
|
9
|
+
HealthCheckExtension,
|
|
10
|
+
OtlpHttpExporter,
|
|
11
|
+
OtlpReceiver,
|
|
12
|
+
PrometheusExporter,
|
|
13
|
+
} from "./components";
|
|
14
|
+
import { Pipeline } from "./pipeline";
|
|
15
|
+
import type { CollectorConfig } from "./model";
|
|
16
|
+
|
|
17
|
+
describe("collectorTopology", () => {
|
|
18
|
+
const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" }, http: {} } });
|
|
19
|
+
const batch = new BatchProcessor({});
|
|
20
|
+
const honeycomb = new OtlpHttpExporter({ name: "honeycomb", endpoint: "https://api.honeycomb.io" });
|
|
21
|
+
const prom = new PrometheusExporter({ endpoint: "0.0.0.0:8889" });
|
|
22
|
+
const gc = new GoogleCloudExporter({ project: "p1" });
|
|
23
|
+
const debug = new DebugExporter({});
|
|
24
|
+
const health = new HealthCheckExtension({ endpoint: "0.0.0.0:13133" });
|
|
25
|
+
const entities = [
|
|
26
|
+
otlp,
|
|
27
|
+
batch,
|
|
28
|
+
honeycomb,
|
|
29
|
+
prom,
|
|
30
|
+
gc,
|
|
31
|
+
debug,
|
|
32
|
+
health,
|
|
33
|
+
new Pipeline({ signal: "traces", receivers: [otlp], processors: [batch], exporters: [honeycomb, gc] }),
|
|
34
|
+
new Pipeline({ signal: "metrics", receivers: [otlp], processors: [batch], exporters: [prom, gc] }),
|
|
35
|
+
new Pipeline({ signal: "logs", name: "debug", receivers: [otlp], exporters: [debug] }),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
test("names every pipeline and where each exporter sends which signals", () => {
|
|
39
|
+
const topo = collectorTopologyOf(entities);
|
|
40
|
+
expect(topo.pipelines.map((p) => [p.id, p.signal])).toEqual([
|
|
41
|
+
["traces", "traces"],
|
|
42
|
+
["metrics", "metrics"],
|
|
43
|
+
["logs/debug", "logs"],
|
|
44
|
+
]);
|
|
45
|
+
expect(topo.exporters).toEqual([
|
|
46
|
+
{ id: "otlphttp/honeycomb", type: "otlphttp", endpoints: ["https://api.honeycomb.io"], pipelines: ["traces"], signals: ["traces"] },
|
|
47
|
+
{ id: "prometheus", type: "prometheus", endpoints: ["0.0.0.0:8889"], pipelines: ["metrics"], signals: ["metrics"] },
|
|
48
|
+
{
|
|
49
|
+
id: "googlecloud",
|
|
50
|
+
type: "googlecloud",
|
|
51
|
+
endpoints: ["googlecloud://projects/p1"],
|
|
52
|
+
pipelines: ["traces", "metrics"],
|
|
53
|
+
signals: ["traces", "metrics"],
|
|
54
|
+
},
|
|
55
|
+
{ id: "debug", type: "debug", endpoints: ["console"], pipelines: ["logs/debug"], signals: ["logs"] },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("reports receiver listen addresses, defaults included, and extension endpoints", () => {
|
|
60
|
+
const topo = collectorTopologyOf(entities);
|
|
61
|
+
expect(topo.components.find((c) => c.id === "otlp")!.endpoints).toEqual(["0.0.0.0:4317", "localhost:4318"]);
|
|
62
|
+
expect(topo.components.find((c) => c.id === "health_check")!.endpoints).toEqual(["0.0.0.0:13133/"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("reads a parsed YAML file the same as the declaration it came from", () => {
|
|
66
|
+
const parsed = load(collectorYaml(entities)) as CollectorConfig;
|
|
67
|
+
expect(collectorTopology(parsed)).toEqual(collectorTopologyOf(entities));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a component this process has no definition for falls back to its endpoint key", () => {
|
|
71
|
+
const topo = collectorTopology({
|
|
72
|
+
exporters: { "kafka/out": { brokers: ["k:9092"] }, "somevendor": { endpoint: "https://v.example" } },
|
|
73
|
+
service: { pipelines: { traces: { receivers: [], exporters: ["kafka/out", "somevendor"] } } },
|
|
74
|
+
});
|
|
75
|
+
expect(topo.components.map((c) => [c.id, c.type, c.name, c.builtin, c.endpoints])).toEqual([
|
|
76
|
+
["kafka/out", "kafka", "out", false, []],
|
|
77
|
+
["somevendor", "somevendor", undefined, false, ["https://v.example"]],
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/topology.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where telemetry goes, as plain data.
|
|
3
|
+
*
|
|
4
|
+
* `collectorTopology()` reads a collector config (declared, or parsed from a
|
|
5
|
+
* YAML file) and returns its pipelines, its components with their endpoints
|
|
6
|
+
* and schema pins, and for each exporter the pipelines and signals it
|
|
7
|
+
* carries. It is the surface a reader such as `chant workspace graph` uses to
|
|
8
|
+
* say where a member's telemetry is sent, and what a declared telemetry
|
|
9
|
+
* endpoint link can point at.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
13
|
+
import { buildCollectorConfig } from "./collector";
|
|
14
|
+
import { definitionOf, type SchemaPin } from "./define";
|
|
15
|
+
import { parseComponentId, pipelineSignal, SECTION_OF, type CollectorConfig, type ComponentKind } from "./model";
|
|
16
|
+
|
|
17
|
+
export interface TopologyPipeline {
|
|
18
|
+
/** The id under `service.pipelines`, e.g. `traces` or `traces/backend`. */
|
|
19
|
+
id: string;
|
|
20
|
+
/** `traces`, `metrics` or `logs`. */
|
|
21
|
+
signal: string;
|
|
22
|
+
receivers: string[];
|
|
23
|
+
processors: string[];
|
|
24
|
+
exporters: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TopologyComponent {
|
|
28
|
+
/** The collector id, `type` or `type/name`. */
|
|
29
|
+
id: string;
|
|
30
|
+
kind: ComponentKind;
|
|
31
|
+
type: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
/** True for a component this package ships; false for a custom one, or one this process has no definition for. */
|
|
34
|
+
builtin: boolean;
|
|
35
|
+
/** The schema pin of its definition, when this process has the definition. */
|
|
36
|
+
schema?: SchemaPin;
|
|
37
|
+
/** Addresses it listens on or sends to, as the config states them. Empty when the config names none. */
|
|
38
|
+
endpoints: string[];
|
|
39
|
+
/** The pipelines that use it. For an extension, empty. */
|
|
40
|
+
pipelines: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TopologyExporter {
|
|
44
|
+
id: string;
|
|
45
|
+
type: string;
|
|
46
|
+
endpoints: string[];
|
|
47
|
+
pipelines: string[];
|
|
48
|
+
/** The signals that reach it, deduplicated, in pipeline order. */
|
|
49
|
+
signals: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CollectorTopology {
|
|
53
|
+
pipelines: TopologyPipeline[];
|
|
54
|
+
components: TopologyComponent[];
|
|
55
|
+
/** The exporters again, grouped for the question "where does this telemetry go". */
|
|
56
|
+
exporters: TopologyExporter[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function endpointsOf(kind: ComponentKind, type: string, config: Record<string, unknown> | null | undefined): string[] {
|
|
60
|
+
const def = definitionOf(kind, type);
|
|
61
|
+
const cfg = config ?? {};
|
|
62
|
+
if (def?.endpoints) {
|
|
63
|
+
try {
|
|
64
|
+
return def.endpoints(cfg as never);
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return typeof cfg.endpoint === "string" && cfg.endpoint !== "" ? [cfg.endpoint] : [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The topology of a collector config. */
|
|
73
|
+
export function collectorTopology(config: CollectorConfig): CollectorTopology {
|
|
74
|
+
const pipelines: TopologyPipeline[] = Object.entries(config.service?.pipelines ?? {}).map(([id, p]) => ({
|
|
75
|
+
id,
|
|
76
|
+
signal: pipelineSignal(id),
|
|
77
|
+
receivers: (p?.receivers ?? []).map(String),
|
|
78
|
+
processors: (p?.processors ?? []).map(String),
|
|
79
|
+
exporters: (p?.exporters ?? []).map(String),
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
const components: TopologyComponent[] = [];
|
|
83
|
+
for (const kind of Object.keys(SECTION_OF) as ComponentKind[]) {
|
|
84
|
+
const section = SECTION_OF[kind];
|
|
85
|
+
for (const [id, cfg] of Object.entries(config[section] ?? {})) {
|
|
86
|
+
const parsed = parseComponentId(id);
|
|
87
|
+
const type = parsed?.type ?? id;
|
|
88
|
+
const def = definitionOf(kind, type);
|
|
89
|
+
const field = section as "receivers" | "processors" | "exporters" | "extensions";
|
|
90
|
+
const inPipelines =
|
|
91
|
+
field === "extensions" ? [] : pipelines.filter((p) => p[field].includes(id)).map((p) => p.id);
|
|
92
|
+
components.push({
|
|
93
|
+
id,
|
|
94
|
+
kind,
|
|
95
|
+
type,
|
|
96
|
+
...(parsed?.name !== undefined ? { name: parsed.name } : {}),
|
|
97
|
+
builtin: def?.builtin ?? false,
|
|
98
|
+
...(def ? { schema: { ...def.pin } } : {}),
|
|
99
|
+
endpoints: endpointsOf(kind, type, cfg),
|
|
100
|
+
pipelines: inPipelines,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const exporters: TopologyExporter[] = components
|
|
106
|
+
.filter((c) => c.kind === "exporter")
|
|
107
|
+
.map((c) => {
|
|
108
|
+
const signals: string[] = [];
|
|
109
|
+
for (const pid of c.pipelines) {
|
|
110
|
+
const s = pipelineSignal(pid);
|
|
111
|
+
if (!signals.includes(s)) signals.push(s);
|
|
112
|
+
}
|
|
113
|
+
return { id: c.id, type: c.type, endpoints: c.endpoints, pipelines: c.pipelines, signals };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return { pipelines, components, exporters };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The topology of declared entities, i.e. of the config the serializer would emit for them. */
|
|
120
|
+
export function collectorTopologyOf(entities: Iterable<Declarable> | Map<string, Declarable>): CollectorTopology {
|
|
121
|
+
return collectorTopology(buildCollectorConfig(entities).config);
|
|
122
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collector config checks, as plain functions over `CollectorConfig` and over
|
|
3
|
+
* declared entities.
|
|
4
|
+
*
|
|
5
|
+
* The post-synth checks in `lint/post-synth/` are thin wrappers around these,
|
|
6
|
+
* so the same rules can run anywhere a config exists: on a build's output, on
|
|
7
|
+
* a composite's generated YAML, or on a collector file parsed by some other
|
|
8
|
+
* tool.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
12
|
+
import { definitionFor, isOTelComponent, isUsablePin, runValidator } from "./define";
|
|
13
|
+
import { componentConfig } from "./collector";
|
|
14
|
+
import { isComponentId, pipelineSignal, SIGNALS, type CollectorConfig } from "./model";
|
|
15
|
+
import { isPipelineEntity } from "./pipeline";
|
|
16
|
+
|
|
17
|
+
export type CollectorIssueCode =
|
|
18
|
+
| "OTEL101"
|
|
19
|
+
| "OTEL102"
|
|
20
|
+
| "OTEL103"
|
|
21
|
+
| "OTEL104"
|
|
22
|
+
| "OTEL105"
|
|
23
|
+
| "OTEL106"
|
|
24
|
+
| "OTEL107"
|
|
25
|
+
| "OTEL108"
|
|
26
|
+
| "OTEL109";
|
|
27
|
+
|
|
28
|
+
export interface CollectorIssue {
|
|
29
|
+
code: CollectorIssueCode;
|
|
30
|
+
severity: "error" | "warning";
|
|
31
|
+
message: string;
|
|
32
|
+
/** The pipeline id the issue is about, when it is about one. */
|
|
33
|
+
pipeline?: string;
|
|
34
|
+
/** The component id the issue is about, when it is about one. */
|
|
35
|
+
component?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const KNOWN_SIGNALS = new Set<string>([...SIGNALS, "profiles"]);
|
|
39
|
+
|
|
40
|
+
function ids(section: Record<string, unknown> | undefined): Set<string> {
|
|
41
|
+
return new Set(Object.keys(section ?? {}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Check a collector config's references and pipeline shape (OTEL101-OTEL106). */
|
|
45
|
+
export function validateCollectorConfig(config: CollectorConfig): CollectorIssue[] {
|
|
46
|
+
const issues: CollectorIssue[] = [];
|
|
47
|
+
const receivers = ids(config.receivers);
|
|
48
|
+
const processors = ids(config.processors);
|
|
49
|
+
const exporters = ids(config.exporters);
|
|
50
|
+
const extensions = ids(config.extensions);
|
|
51
|
+
const connectors = ids(config.connectors);
|
|
52
|
+
const pipelines = config.service?.pipelines ?? {};
|
|
53
|
+
|
|
54
|
+
const used = { receivers: new Set<string>(), processors: new Set<string>(), exporters: new Set<string>() };
|
|
55
|
+
|
|
56
|
+
for (const [pipelineId, pipeline] of Object.entries(pipelines)) {
|
|
57
|
+
const signal = pipelineSignal(pipelineId);
|
|
58
|
+
if (!KNOWN_SIGNALS.has(signal) || !isComponentId(pipelineId)) {
|
|
59
|
+
issues.push({
|
|
60
|
+
code: "OTEL106",
|
|
61
|
+
severity: "error",
|
|
62
|
+
pipeline: pipelineId,
|
|
63
|
+
message: `pipeline "${pipelineId}" does not name a signal; a pipeline id is traces, metrics or logs, optionally followed by /name`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const p = pipeline ?? {};
|
|
67
|
+
const lists: Array<["receivers" | "processors" | "exporters", string[] | undefined, Set<string>, string]> = [
|
|
68
|
+
["receivers", p.receivers, receivers, "receiver"],
|
|
69
|
+
["processors", p.processors, processors, "processor"],
|
|
70
|
+
["exporters", p.exporters, exporters, "exporter"],
|
|
71
|
+
];
|
|
72
|
+
for (const [field, refs, declared, noun] of lists) {
|
|
73
|
+
for (const ref of refs ?? []) {
|
|
74
|
+
const id = String(ref);
|
|
75
|
+
used[field].add(id);
|
|
76
|
+
if (!isComponentId(id)) {
|
|
77
|
+
issues.push({
|
|
78
|
+
code: "OTEL106",
|
|
79
|
+
severity: "error",
|
|
80
|
+
pipeline: pipelineId,
|
|
81
|
+
component: id,
|
|
82
|
+
message: `pipeline "${pipelineId}" lists ${noun} "${id}", which is not a component id (type or type/name)`,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const viaConnector = field !== "processors" && connectors.has(id);
|
|
87
|
+
if (!declared.has(id) && !viaConnector) {
|
|
88
|
+
issues.push({
|
|
89
|
+
code: "OTEL101",
|
|
90
|
+
severity: "error",
|
|
91
|
+
pipeline: pipelineId,
|
|
92
|
+
component: id,
|
|
93
|
+
message: `pipeline "${pipelineId}" uses ${noun} "${id}", which is not declared under ${field}; the collector refuses to start`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if ((p.receivers ?? []).length === 0) {
|
|
99
|
+
issues.push({
|
|
100
|
+
code: "OTEL102",
|
|
101
|
+
severity: "error",
|
|
102
|
+
pipeline: pipelineId,
|
|
103
|
+
message: `pipeline "${pipelineId}" has no receivers, so nothing enters it`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if ((p.exporters ?? []).length === 0) {
|
|
107
|
+
issues.push({
|
|
108
|
+
code: "OTEL102",
|
|
109
|
+
severity: "error",
|
|
110
|
+
pipeline: pipelineId,
|
|
111
|
+
message: `pipeline "${pipelineId}" has no exporters, so what enters it goes nowhere`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const procs = (p.processors ?? []).map(String);
|
|
115
|
+
const limiterAt = procs.findIndex((id) => id === "memory_limiter" || id.startsWith("memory_limiter/"));
|
|
116
|
+
if (limiterAt > 0) {
|
|
117
|
+
issues.push({
|
|
118
|
+
code: "OTEL105",
|
|
119
|
+
severity: "warning",
|
|
120
|
+
pipeline: pipelineId,
|
|
121
|
+
component: procs[limiterAt],
|
|
122
|
+
message: `pipeline "${pipelineId}" runs ${procs[limiterAt]} at position ${limiterAt + 1}; put it first so it can refuse data before other processors buffer it`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const unused: Array<[Set<string>, Set<string>, string]> = [
|
|
128
|
+
[receivers, used.receivers, "receiver"],
|
|
129
|
+
[processors, used.processors, "processor"],
|
|
130
|
+
[exporters, used.exporters, "exporter"],
|
|
131
|
+
];
|
|
132
|
+
for (const [declared, usedIds, noun] of unused) {
|
|
133
|
+
for (const id of declared) {
|
|
134
|
+
if (!usedIds.has(id)) {
|
|
135
|
+
issues.push({
|
|
136
|
+
code: "OTEL103",
|
|
137
|
+
severity: "warning",
|
|
138
|
+
component: id,
|
|
139
|
+
message: `${noun} "${id}" is declared but no pipeline uses it; the collector ignores it`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const enabled = new Set((config.service?.extensions ?? []).map(String));
|
|
146
|
+
for (const id of enabled) {
|
|
147
|
+
if (!extensions.has(id)) {
|
|
148
|
+
issues.push({
|
|
149
|
+
code: "OTEL104",
|
|
150
|
+
severity: "error",
|
|
151
|
+
component: id,
|
|
152
|
+
message: `service.extensions enables "${id}", which is not declared under extensions; the collector refuses to start`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const id of extensions) {
|
|
157
|
+
if (!enabled.has(id)) {
|
|
158
|
+
issues.push({
|
|
159
|
+
code: "OTEL103",
|
|
160
|
+
severity: "warning",
|
|
161
|
+
component: id,
|
|
162
|
+
message: `extension "${id}" is declared but not listed in service.extensions, so it never starts`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return issues;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check declared entities for what only the declaration knows (OTEL107-OTEL109):
|
|
172
|
+
* each component's own config rules, duplicate ids, and custom components'
|
|
173
|
+
* schema pins.
|
|
174
|
+
*/
|
|
175
|
+
export function validateCollectorEntities(entities: Iterable<Declarable> | Map<string, Declarable>): CollectorIssue[] {
|
|
176
|
+
const list = entities instanceof Map ? [...entities.values()] : [...entities];
|
|
177
|
+
const issues: CollectorIssue[] = [];
|
|
178
|
+
const seen = new Map<string, number>();
|
|
179
|
+
const pipelineIds = new Map<string, number>();
|
|
180
|
+
|
|
181
|
+
for (const e of list) {
|
|
182
|
+
if (isPipelineEntity(e)) {
|
|
183
|
+
pipelineIds.set(e.pipelineId, (pipelineIds.get(e.pipelineId) ?? 0) + 1);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!isOTelComponent(e)) continue;
|
|
187
|
+
const key = `${e.componentKind} ${e.componentId}`;
|
|
188
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
189
|
+
|
|
190
|
+
const def = definitionFor(e.entityType);
|
|
191
|
+
if (!def) continue;
|
|
192
|
+
if (!def.builtin && !isUsablePin(def.pin)) {
|
|
193
|
+
issues.push({
|
|
194
|
+
code: "OTEL109",
|
|
195
|
+
severity: "error",
|
|
196
|
+
component: e.componentId,
|
|
197
|
+
message: `custom ${e.componentKind} "${e.componentId}" has no schema pin; give defineComponent a pin with a source and a version`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
for (const problem of runValidator(def.validate, componentConfig(e) as never)) {
|
|
201
|
+
issues.push({
|
|
202
|
+
code: "OTEL107",
|
|
203
|
+
severity: "error",
|
|
204
|
+
component: e.componentId,
|
|
205
|
+
message: `${e.componentKind} "${e.componentId}": ${problem}`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const [key, count] of seen) {
|
|
211
|
+
if (count > 1) {
|
|
212
|
+
const [kind, id] = key.split(" ");
|
|
213
|
+
issues.push({
|
|
214
|
+
code: "OTEL108",
|
|
215
|
+
severity: "error",
|
|
216
|
+
component: id,
|
|
217
|
+
message: `${count} ${kind}s declare the id "${id}"; give each a distinct name`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const [id, count] of pipelineIds) {
|
|
222
|
+
if (count > 1) {
|
|
223
|
+
issues.push({
|
|
224
|
+
code: "OTEL108",
|
|
225
|
+
severity: "error",
|
|
226
|
+
pipeline: id,
|
|
227
|
+
message: `${count} pipelines declare the id "${id}"; give each a distinct name`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return issues;
|
|
232
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the otel lexicon's own artifacts: every built-in class is in the
|
|
3
|
+
* registry, every built-in definition carries the collector pin, and every
|
|
4
|
+
* built-in constructs and serializes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
|
|
8
|
+
import { BUILTIN_CATALOG, lexiconRegistry } from "./catalog";
|
|
9
|
+
import * as components from "./components";
|
|
10
|
+
import { COLLECTOR_PIN, type ComponentClass } from "./define";
|
|
11
|
+
import { collectorYaml } from "./collector";
|
|
12
|
+
|
|
13
|
+
export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
|
|
14
|
+
|
|
15
|
+
/** Every entity class the package must keep exporting. */
|
|
16
|
+
export const REQUIRED_NAMES = [
|
|
17
|
+
"OtlpReceiver",
|
|
18
|
+
"PrometheusReceiver",
|
|
19
|
+
"HostMetricsReceiver",
|
|
20
|
+
"FileLogReceiver",
|
|
21
|
+
"BatchProcessor",
|
|
22
|
+
"MemoryLimiterProcessor",
|
|
23
|
+
"ResourceProcessor",
|
|
24
|
+
"AttributesProcessor",
|
|
25
|
+
"K8sAttributesProcessor",
|
|
26
|
+
"ResourceDetectionProcessor",
|
|
27
|
+
"OtlpExporter",
|
|
28
|
+
"OtlpHttpExporter",
|
|
29
|
+
"DebugExporter",
|
|
30
|
+
"PrometheusExporter",
|
|
31
|
+
"GoogleCloudExporter",
|
|
32
|
+
"HealthCheckExtension",
|
|
33
|
+
"PprofExtension",
|
|
34
|
+
"ZPagesExtension",
|
|
35
|
+
"Pipeline",
|
|
36
|
+
"Service",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export async function validate(): Promise<ValidateResult> {
|
|
40
|
+
const checks: ValidateCheck[] = [];
|
|
41
|
+
const registry = lexiconRegistry();
|
|
42
|
+
|
|
43
|
+
const missing = REQUIRED_NAMES.filter((n) => !(n in registry));
|
|
44
|
+
checks.push(
|
|
45
|
+
missing.length === 0
|
|
46
|
+
? { name: "required-names", ok: true }
|
|
47
|
+
: { name: "required-names", ok: false, error: `Missing required names: ${missing.join(", ")}` },
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const classes = (Object.entries(components) as Array<[string, unknown]>).filter(
|
|
51
|
+
(e): e is [string, ComponentClass] => typeof e[1] === "function" && "definition" in (e[1] as object),
|
|
52
|
+
);
|
|
53
|
+
const unpinned = classes.filter(([, c]) => c.definition.pin !== COLLECTOR_PIN || !c.definition.builtin).map(([n]) => n);
|
|
54
|
+
checks.push(
|
|
55
|
+
unpinned.length === 0
|
|
56
|
+
? { name: "builtins-pinned", ok: true }
|
|
57
|
+
: { name: "builtins-pinned", ok: false, error: `Not pinned to the collector release: ${unpinned.join(", ")}` },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const broken: string[] = [];
|
|
61
|
+
for (const [name, Cls] of classes) {
|
|
62
|
+
try {
|
|
63
|
+
const yaml = collectorYaml([new (Cls as unknown as new (p: object) => never)({})]);
|
|
64
|
+
if (!yaml.includes(Cls.definition.type)) broken.push(name);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
broken.push(`${name} (${err instanceof Error ? err.message : String(err)})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
checks.push(
|
|
70
|
+
broken.length === 0
|
|
71
|
+
? { name: "builtins-serialize", ok: true }
|
|
72
|
+
: { name: "builtins-serialize", ok: false, error: `Failed to serialize: ${broken.join(", ")}` },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
checks.push(
|
|
76
|
+
BUILTIN_CATALOG.length === Object.keys(registry).length
|
|
77
|
+
? { name: "catalog-matches-registry", ok: true }
|
|
78
|
+
: { name: "catalog-matches-registry", ok: false, error: "catalog and registry disagree" },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return { success: checks.every((c) => c.ok), checks };
|
|
82
|
+
}
|
package/src/yaml.test.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { load } from "js-yaml";
|
|
3
|
+
import { emitCollectorYaml, isPlainSafe } from "./yaml";
|
|
4
|
+
|
|
5
|
+
describe("collector YAML emitter", () => {
|
|
6
|
+
test.each([
|
|
7
|
+
["0.0.0.0:4317", true],
|
|
8
|
+
["custom.googleapis.com/c1", true],
|
|
9
|
+
["${env:KEY}", true],
|
|
10
|
+
["true", false],
|
|
11
|
+
["8080", false],
|
|
12
|
+
["1:30", false],
|
|
13
|
+
["a: b", false],
|
|
14
|
+
["- x", false],
|
|
15
|
+
["", false],
|
|
16
|
+
[" x", false],
|
|
17
|
+
])("isPlainSafe(%j) in block context is %s", (s, safe) => {
|
|
18
|
+
expect(isPlainSafe(s, false)).toBe(safe);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("a string with flow indicators is quoted inside a flow list", () => {
|
|
22
|
+
expect(isPlainSafe("${env:KEY}", true)).toBe(false);
|
|
23
|
+
const yaml = emitCollectorYaml({ receivers: { x: { list: ["${env:A}", "b"] } } });
|
|
24
|
+
expect(yaml).toContain('list: ["${env:A}", b]');
|
|
25
|
+
expect((load(yaml) as any).receivers.x.list).toEqual(["${env:A}", "b"]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("lists of maps are block style, nested lists and empty values survive", () => {
|
|
29
|
+
const config = {
|
|
30
|
+
processors: {
|
|
31
|
+
attributes: {
|
|
32
|
+
actions: [
|
|
33
|
+
{ key: "env", action: "upsert", value: "prod" },
|
|
34
|
+
{ key: "secret", action: "delete" },
|
|
35
|
+
],
|
|
36
|
+
empty: [],
|
|
37
|
+
nested: [[1, 2], [{ a: 1 }]],
|
|
38
|
+
none: null,
|
|
39
|
+
blank: {},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const yaml = emitCollectorYaml(config);
|
|
44
|
+
expect(yaml).toContain(" actions:\n - key: env\n action: upsert\n value: prod\n - key: secret\n");
|
|
45
|
+
expect(load(yaml)).toEqual(config);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a header becomes comment lines above the config", () => {
|
|
49
|
+
const yaml = emitCollectorYaml({ exporters: { debug: {} } }, { header: ["chant: hello"] });
|
|
50
|
+
expect(yaml.startsWith("# chant: hello\n\nexporters:\n debug: {}\n")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("an empty config is empty", () => {
|
|
54
|
+
expect(emitCollectorYaml({})).toBe("");
|
|
55
|
+
});
|
|
56
|
+
});
|