@intentius/chant-lexicon-otel 0.97.0 → 0.99.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 +5 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/common.d.ts +7 -2
- package/dist/components/common.d.ts.map +1 -1
- package/dist/components/filtering.d.ts +58 -1
- package/dist/components/filtering.d.ts.map +1 -1
- package/dist/components/processors.d.ts +10 -0
- package/dist/components/processors.d.ts.map +1 -1
- package/dist/components/receivers.d.ts +87 -7
- package/dist/components/receivers.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +12 -0
- package/dist/import/embedded.d.ts.map +1 -0
- package/dist/import/generator.d.ts +39 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/parser.d.ts +61 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/integrity.json +4 -4
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/literal-credential.ts +46 -17
- package/dist/skills/chant-otel.md +11 -0
- package/package.json +2 -2
- package/src/codegen/docs.ts +2 -1
- package/src/components/common.ts +7 -2
- package/src/components/filtering.test.ts +27 -0
- package/src/components/filtering.ts +65 -2
- package/src/components/processors.ts +4 -0
- package/src/components/receivers.ts +88 -4
- package/src/import/embedded.test.ts +56 -0
- package/src/import/embedded.ts +41 -0
- package/src/import/generated-types.e2e.test.ts +110 -0
- package/src/import/generator.test.ts +165 -0
- package/src/import/generator.ts +509 -0
- package/src/import/parser.test.ts +174 -0
- package/src/import/parser.ts +236 -0
- package/src/import/roundtrip.test.ts +365 -0
- package/src/import/testdata/agent-observability-agent.yaml +54 -0
- package/src/import/testdata/agent-observability-gateway.yaml +128 -0
- package/src/import/testdata/fixtures.ts +274 -0
- package/src/import/testdata/gateway.yaml +153 -0
- package/src/import/testdata/upstream/couchbase.yaml +150 -0
- package/src/import/testdata/upstream/fault-tolerant-logs.yaml +27 -0
- package/src/import/testdata/upstream/kubernetes-filelog.yaml +86 -0
- package/src/import/testdata/upstream/loadbalancing-agent.yaml +38 -0
- package/src/import/testdata/upstream/loadbalancing-backend.yaml +27 -0
- package/src/import/testdata/upstream/logline-filter-in.yaml +20 -0
- package/src/import/testdata/upstream/logline-filter-out.yaml +20 -0
- package/src/import/testdata/upstream/secure-tracing.yaml +26 -0
- package/src/import/testdata/upstream/servicegraph-nop.yaml +28 -0
- package/src/lint/rules/literal-credential.ts +46 -17
- package/src/lint/rules/rules.test.ts +16 -0
- package/src/plugin.ts +15 -0
- package/src/skills/chant-otel.md +11 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { COLLECTOR_RESOURCE_TYPE, OtelCollectorParser, parseCollectorYaml } from "./parser";
|
|
3
|
+
|
|
4
|
+
const parser = new OtelCollectorParser();
|
|
5
|
+
|
|
6
|
+
describe("parseCollectorYaml", () => {
|
|
7
|
+
test("reads every section as the collector's own layout", () => {
|
|
8
|
+
const { config, warnings } = parseCollectorYaml(`
|
|
9
|
+
receivers:
|
|
10
|
+
otlp:
|
|
11
|
+
protocols:
|
|
12
|
+
grpc:
|
|
13
|
+
processors:
|
|
14
|
+
batch:
|
|
15
|
+
exporters:
|
|
16
|
+
otlp/tempo:
|
|
17
|
+
endpoint: tempo:4317
|
|
18
|
+
connectors:
|
|
19
|
+
forward/x: {}
|
|
20
|
+
extensions:
|
|
21
|
+
health_check:
|
|
22
|
+
service:
|
|
23
|
+
extensions: [health_check]
|
|
24
|
+
telemetry:
|
|
25
|
+
logs: { level: debug }
|
|
26
|
+
pipelines:
|
|
27
|
+
traces:
|
|
28
|
+
receivers: [otlp]
|
|
29
|
+
processors: [batch]
|
|
30
|
+
exporters: [otlp/tempo, forward/x]
|
|
31
|
+
`);
|
|
32
|
+
expect(warnings).toEqual([]);
|
|
33
|
+
expect(config.receivers).toEqual({ otlp: { protocols: { grpc: null } } });
|
|
34
|
+
expect(config.processors).toEqual({ batch: null });
|
|
35
|
+
expect(config.exporters).toEqual({ "otlp/tempo": { endpoint: "tempo:4317" } });
|
|
36
|
+
expect(config.connectors).toEqual({ "forward/x": {} });
|
|
37
|
+
expect(config.extensions).toEqual({ health_check: null });
|
|
38
|
+
expect(config.service).toEqual({
|
|
39
|
+
extensions: ["health_check"],
|
|
40
|
+
telemetry: { logs: { level: "debug" } },
|
|
41
|
+
pipelines: { traces: { receivers: ["otlp"], processors: ["batch"], exporters: ["otlp/tempo", "forward/x"] } },
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("keeps ${env:VAR} references and quoted scalars as strings", () => {
|
|
46
|
+
const { config } = parseCollectorYaml(`
|
|
47
|
+
exporters:
|
|
48
|
+
otlphttp:
|
|
49
|
+
endpoint: \${env:OTLP_ENDPOINT}
|
|
50
|
+
headers:
|
|
51
|
+
api-key: "\${env:API_KEY}"
|
|
52
|
+
compression: "none"
|
|
53
|
+
prometheus:
|
|
54
|
+
endpoint: "0.0.0.0:8889"
|
|
55
|
+
const_labels:
|
|
56
|
+
port: "8080"
|
|
57
|
+
started: 2024-01-01
|
|
58
|
+
`);
|
|
59
|
+
expect(config.exporters?.otlphttp).toEqual({
|
|
60
|
+
endpoint: "${env:OTLP_ENDPOINT}",
|
|
61
|
+
headers: { "api-key": "${env:API_KEY}" },
|
|
62
|
+
compression: "none",
|
|
63
|
+
});
|
|
64
|
+
// No YAML 1.1 dates: the collector reads this as a string, and so does the importer.
|
|
65
|
+
expect(config.exporters?.prometheus).toEqual({ endpoint: "0.0.0.0:8889", const_labels: { port: "8080", started: "2024-01-01" } });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("resolves anchors and merge keys", () => {
|
|
69
|
+
const { config } = parseCollectorYaml(`
|
|
70
|
+
exporters:
|
|
71
|
+
otlp/a: &base
|
|
72
|
+
endpoint: a:4317
|
|
73
|
+
tls: { insecure: true }
|
|
74
|
+
otlp/b:
|
|
75
|
+
<<: *base
|
|
76
|
+
endpoint: b:4317
|
|
77
|
+
`);
|
|
78
|
+
expect(config.exporters?.["otlp/b"]).toEqual({ endpoint: "b:4317", tls: { insecure: true } });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("reads the # chant: header back", () => {
|
|
82
|
+
const { pins, semconv, warnings } = parseCollectorYaml(
|
|
83
|
+
[
|
|
84
|
+
"# chant: exporter splunk_hec/security schema github.com/open-telemetry/opentelemetry-collector-contrib/exporter/splunkhecexporter@v0.130.0",
|
|
85
|
+
"# chant: processor acme schema @acme/otel-components@1.2.3 sha256:deadbeef",
|
|
86
|
+
"# chant: semconv gen_ai github.com/open-telemetry/semantic-conventions@v1.41.1 (transform, redaction)",
|
|
87
|
+
"receivers: {}",
|
|
88
|
+
].join("\n"),
|
|
89
|
+
);
|
|
90
|
+
expect(pins).toEqual([
|
|
91
|
+
{
|
|
92
|
+
kind: "exporter",
|
|
93
|
+
id: "splunk_hec/security",
|
|
94
|
+
pin: { source: "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/splunkhecexporter", version: "v0.130.0" },
|
|
95
|
+
},
|
|
96
|
+
{ kind: "processor", id: "acme", pin: { source: "@acme/otel-components", version: "1.2.3", digest: "sha256:deadbeef" } },
|
|
97
|
+
]);
|
|
98
|
+
expect(semconv).toEqual([{ namespace: "gen_ai", pin: { source: "github.com/open-telemetry/semantic-conventions", version: "v1.41.1" } }]);
|
|
99
|
+
expect(warnings).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("only the leading comment block is a header, and an unpinned line pins nothing", () => {
|
|
103
|
+
const { pins } = parseCollectorYaml(
|
|
104
|
+
[
|
|
105
|
+
"# chant: exporter x schema (unpinned)@(unpinned)",
|
|
106
|
+
"receivers: {}",
|
|
107
|
+
"# chant: exporter y schema src@v1",
|
|
108
|
+
].join("\n"),
|
|
109
|
+
);
|
|
110
|
+
expect(pins).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("names what it cannot carry", () => {
|
|
114
|
+
const { config, warnings } = parseCollectorYaml(
|
|
115
|
+
[
|
|
116
|
+
"# chant: semconv gen_ai github.com/open-telemetry/semantic-conventions@v1.30.0 (x)",
|
|
117
|
+
"# chant: something else",
|
|
118
|
+
"receivers:",
|
|
119
|
+
" thing:",
|
|
120
|
+
" name: dropped",
|
|
121
|
+
" other: kept",
|
|
122
|
+
" bad id: {}",
|
|
123
|
+
"exporters:",
|
|
124
|
+
" debug: 3",
|
|
125
|
+
"connectors: []",
|
|
126
|
+
"extras: {}",
|
|
127
|
+
"service:",
|
|
128
|
+
" pipelines:",
|
|
129
|
+
" profiles:",
|
|
130
|
+
" receivers: [thing]",
|
|
131
|
+
" exporters: [debug]",
|
|
132
|
+
" extra: [x]",
|
|
133
|
+
" unknown: true",
|
|
134
|
+
].join("\n"),
|
|
135
|
+
);
|
|
136
|
+
expect(warnings).toEqual([
|
|
137
|
+
"the config was built against gen_ai semantic conventions github.com/open-telemetry/semantic-conventions@v1.30.0; this lexicon follows github.com/open-telemetry/semantic-conventions@v1.41.1, which the rebuilt config will name",
|
|
138
|
+
'header line "# chant: something else" is not a schema pin chant recognises; it is not carried',
|
|
139
|
+
'receivers.thing has a config key "name", which chant uses for the instance name; the key is not carried',
|
|
140
|
+
'receivers: "bad id" is not a type[/name] component id',
|
|
141
|
+
"exporters.debug is not a mapping; it is carried as an empty config",
|
|
142
|
+
'"connectors" is not a mapping; it is not carried',
|
|
143
|
+
'top-level section "extras" is not part of a collector config; it is not carried',
|
|
144
|
+
'pipeline "profiles" carries signal "profiles", which Pipeline does not type; it is imported with a type suppression',
|
|
145
|
+
"service.pipelines.profiles.extra is not carried; a Pipeline has receivers, processors and exporters",
|
|
146
|
+
"service.unknown is not carried; chant's Service declares extensions and telemetry",
|
|
147
|
+
]);
|
|
148
|
+
expect(config.receivers?.thing).toEqual({ name: "dropped", other: "kept" });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("an empty file is an empty config; a non-mapping is an error", () => {
|
|
152
|
+
expect(parseCollectorYaml("").config).toEqual({});
|
|
153
|
+
expect(parseCollectorYaml("# just a comment\n").config).toEqual({});
|
|
154
|
+
expect(() => parseCollectorYaml("- a\n- b\n")).toThrow(/is a YAML mapping/);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("OtelCollectorParser", () => {
|
|
159
|
+
test("carries the whole config as one OTel::Collector resource, with the header in metadata", () => {
|
|
160
|
+
const ir = parser.parse(
|
|
161
|
+
"# chant: exporter x/y schema src@v1\nexporters:\n x/y: {}\nservice:\n pipelines:\n logs:\n receivers: []\n exporters: [x/y]\n",
|
|
162
|
+
);
|
|
163
|
+
expect(ir.resources).toHaveLength(1);
|
|
164
|
+
const [r] = ir.resources;
|
|
165
|
+
expect(r.type).toBe(COLLECTOR_RESOURCE_TYPE);
|
|
166
|
+
expect(r.logicalId).toBe("collector");
|
|
167
|
+
expect(r.properties).toEqual({
|
|
168
|
+
config: { exporters: { "x/y": {} }, service: { pipelines: { logs: { receivers: [], exporters: ["x/y"] } } } },
|
|
169
|
+
});
|
|
170
|
+
expect(r.metadata).toEqual({ pins: [{ kind: "exporter", id: "x/y", pin: { source: "src", version: "v1" } }], semconv: [] });
|
|
171
|
+
expect(ir.parameters).toEqual([]);
|
|
172
|
+
expect(ir.warnings).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collector YAML -> `TemplateIR`, for `chant import`.
|
|
3
|
+
*
|
|
4
|
+
* A collector config is one document whose parts name each other: pipelines
|
|
5
|
+
* list component ids, a connector sits in two pipelines, `service.extensions`
|
|
6
|
+
* lists extension ids. So the IR carries the whole config as one resource of
|
|
7
|
+
* type `OTel::Collector`, and the generator lays out the modules itself, with
|
|
8
|
+
* those names turned into imports of declared entities. (Core splits an IR
|
|
9
|
+
* with more than three resources into one generate() call per category and
|
|
10
|
+
* keeps only the first file of each, which would lose the imports between a
|
|
11
|
+
* pipeline and the components it references.)
|
|
12
|
+
*
|
|
13
|
+
* The config is carried as parsed, with nothing dropped from a component's
|
|
14
|
+
* config. What the lexicon cannot express is named in `warnings`.
|
|
15
|
+
*
|
|
16
|
+
* The `# chant:` lines the serializer writes above a config are read back:
|
|
17
|
+
* a custom component's schema pin becomes the pin of the `defineComponent`
|
|
18
|
+
* the generator writes for it. A config without them imports against
|
|
19
|
+
* `COLLECTOR_PIN`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as jsYaml from "js-yaml";
|
|
23
|
+
import type { TemplateIR, TemplateParser } from "@intentius/chant/import/parser";
|
|
24
|
+
import { GENAI_SEMCONV_PIN, type SchemaPin } from "../define";
|
|
25
|
+
import {
|
|
26
|
+
COMPONENT_KINDS,
|
|
27
|
+
SECTION_OF,
|
|
28
|
+
SIGNALS,
|
|
29
|
+
parseComponentId,
|
|
30
|
+
pipelineSignal,
|
|
31
|
+
type CollectorConfig,
|
|
32
|
+
type ComponentKind,
|
|
33
|
+
} from "../model";
|
|
34
|
+
|
|
35
|
+
/** The IR resource type for a whole collector config. */
|
|
36
|
+
export const COLLECTOR_RESOURCE_TYPE = "OTel::Collector";
|
|
37
|
+
|
|
38
|
+
/** One `# chant: <kind> <id> schema <source>@<version>` header line, read back. */
|
|
39
|
+
export interface HeaderPin {
|
|
40
|
+
kind: ComponentKind;
|
|
41
|
+
id: string;
|
|
42
|
+
pin: SchemaPin;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One `# chant: semconv <namespace> <source>@<version>` header line, read back. */
|
|
46
|
+
export interface HeaderSemconv {
|
|
47
|
+
namespace: string;
|
|
48
|
+
pin: SchemaPin;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What the parser read from a collector YAML file. */
|
|
52
|
+
export interface ParsedCollector {
|
|
53
|
+
config: CollectorConfig;
|
|
54
|
+
pins: HeaderPin[];
|
|
55
|
+
semconv: HeaderSemconv[];
|
|
56
|
+
warnings: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `properties` of the `OTel::Collector` resource. */
|
|
60
|
+
export interface CollectorResourceProperties {
|
|
61
|
+
config: CollectorConfig;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `metadata` of the `OTel::Collector` resource. */
|
|
65
|
+
export interface CollectorResourceMetadata {
|
|
66
|
+
pins: HeaderPin[];
|
|
67
|
+
semconv: HeaderSemconv[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// YAML 1.2 core types plus `<<` merge keys. The default schema would also
|
|
71
|
+
// turn an unquoted date into a Date, which the collector never does.
|
|
72
|
+
// js-yaml exports its built-in types at runtime; @types/js-yaml does not declare them.
|
|
73
|
+
const MERGE_TYPE = (jsYaml as unknown as { types: { merge: jsYaml.Type } }).types.merge;
|
|
74
|
+
const COLLECTOR_YAML_SCHEMA = jsYaml.CORE_SCHEMA.extend({ implicit: [MERGE_TYPE] });
|
|
75
|
+
|
|
76
|
+
const SECTIONS = new Set<string>(["receivers", "processors", "exporters", "connectors", "extensions", "service"]);
|
|
77
|
+
const SERVICE_KEYS = new Set(["extensions", "telemetry", "pipelines"]);
|
|
78
|
+
const PIPELINE_KEYS = new Set(["receivers", "processors", "exporters"]);
|
|
79
|
+
|
|
80
|
+
const PIN_LINE = /^#\s*chant:\s*(receiver|processor|exporter|connector|extension)\s+(\S+)\s+schema\s+(\S+)@([^@\s]+)(?:\s+(\S+))?\s*$/;
|
|
81
|
+
const SEMCONV_LINE = /^#\s*chant:\s*semconv\s+(\S+)\s+(\S+)@([^@\s]+)(?:\s+([^\s(]+))?(?:\s+\(.*\))?\s*$/;
|
|
82
|
+
|
|
83
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
84
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The `# chant:` lines of the comment block at the top of the file. */
|
|
88
|
+
function readHeader(content: string, warnings: string[]): { pins: HeaderPin[]; semconv: HeaderSemconv[] } {
|
|
89
|
+
const pins: HeaderPin[] = [];
|
|
90
|
+
const semconv: HeaderSemconv[] = [];
|
|
91
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
92
|
+
const line = raw.trim();
|
|
93
|
+
if (line === "") continue;
|
|
94
|
+
if (!line.startsWith("#")) break;
|
|
95
|
+
if (!/^#\s*chant:/.test(line)) continue;
|
|
96
|
+
const pin = PIN_LINE.exec(line);
|
|
97
|
+
if (pin) {
|
|
98
|
+
const [, kind, id, source, version, digest] = pin;
|
|
99
|
+
if (source === "(unpinned)" || version === "(unpinned)") continue;
|
|
100
|
+
pins.push({ kind: kind as ComponentKind, id, pin: digest ? { source, version, digest } : { source, version } });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const sc = SEMCONV_LINE.exec(line);
|
|
104
|
+
if (sc) {
|
|
105
|
+
const [, namespace, source, version, digest] = sc;
|
|
106
|
+
semconv.push({ namespace, pin: digest ? { source, version, digest } : { source, version } });
|
|
107
|
+
if (namespace === "gen_ai" && (source !== GENAI_SEMCONV_PIN.source || version !== GENAI_SEMCONV_PIN.version)) {
|
|
108
|
+
warnings.push(
|
|
109
|
+
`the config was built against ${namespace} semantic conventions ${source}@${version}; ` +
|
|
110
|
+
`this lexicon follows ${GENAI_SEMCONV_PIN.source}@${GENAI_SEMCONV_PIN.version}, which the rebuilt config will name`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
warnings.push(`header line "${line}" is not a schema pin chant recognises; it is not carried`);
|
|
116
|
+
}
|
|
117
|
+
return { pins, semconv };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Parse collector YAML into the plain config model, the header pins and what cannot be carried. */
|
|
121
|
+
export function parseCollectorYaml(content: string): ParsedCollector {
|
|
122
|
+
const warnings: string[] = [];
|
|
123
|
+
const { pins, semconv } = readHeader(content, warnings);
|
|
124
|
+
const doc = content.trim() === "" ? {} : jsYaml.load(content, { schema: COLLECTOR_YAML_SCHEMA });
|
|
125
|
+
if (doc === null || doc === undefined) return { config: {}, pins, semconv, warnings };
|
|
126
|
+
if (!isPlainObject(doc)) {
|
|
127
|
+
throw new Error("an OpenTelemetry Collector config is a YAML mapping; this document is not one");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const config: CollectorConfig = {};
|
|
131
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
132
|
+
if (!SECTIONS.has(key)) {
|
|
133
|
+
warnings.push(`top-level section "${key}" is not part of a collector config; it is not carried`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (value === null || value === undefined) continue;
|
|
137
|
+
if (!isPlainObject(value)) {
|
|
138
|
+
warnings.push(`"${key}" is not a mapping; it is not carried`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (key === "service") continue;
|
|
142
|
+
const section: Record<string, Record<string, unknown> | null> = {};
|
|
143
|
+
for (const [id, cfg] of Object.entries(value)) {
|
|
144
|
+
if (!parseComponentId(id)) warnings.push(`${key}: "${id}" is not a type[/name] component id`);
|
|
145
|
+
if (cfg !== null && cfg !== undefined && !isPlainObject(cfg)) {
|
|
146
|
+
warnings.push(`${key}.${id} is not a mapping; it is carried as an empty config`);
|
|
147
|
+
section[id] = null;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (isPlainObject(cfg) && "name" in cfg) {
|
|
151
|
+
warnings.push(
|
|
152
|
+
`${key}.${id} has a config key "name", which chant uses for the instance name; the key is not carried`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
section[id] = (cfg as Record<string, unknown> | null | undefined) ?? null;
|
|
156
|
+
}
|
|
157
|
+
(config as Record<string, unknown>)[key] = section;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const service = (doc as Record<string, unknown>).service;
|
|
161
|
+
if (isPlainObject(service)) {
|
|
162
|
+
config.service = {};
|
|
163
|
+
for (const [key, value] of Object.entries(service)) {
|
|
164
|
+
if (!SERVICE_KEYS.has(key)) {
|
|
165
|
+
warnings.push(`service.${key} is not carried; chant's Service declares extensions and telemetry`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (value === null || value === undefined) continue;
|
|
169
|
+
if (key === "extensions") {
|
|
170
|
+
if (!Array.isArray(value)) warnings.push("service.extensions is not a list; it is not carried");
|
|
171
|
+
else config.service.extensions = value.map(String);
|
|
172
|
+
} else if (key === "telemetry") {
|
|
173
|
+
if (!isPlainObject(value)) warnings.push("service.telemetry is not a mapping; it is not carried");
|
|
174
|
+
else config.service.telemetry = value;
|
|
175
|
+
} else if (key === "pipelines") {
|
|
176
|
+
if (!isPlainObject(value)) {
|
|
177
|
+
warnings.push("service.pipelines is not a mapping; it is not carried");
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
config.service.pipelines = {};
|
|
181
|
+
for (const [pid, p] of Object.entries(value)) {
|
|
182
|
+
const signal = pipelineSignal(pid);
|
|
183
|
+
if (!(SIGNALS as readonly string[]).includes(signal)) {
|
|
184
|
+
warnings.push(
|
|
185
|
+
`pipeline "${pid}" carries signal "${signal}", which Pipeline does not type; it is imported with a type suppression`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const entry: Record<string, string[]> = {};
|
|
189
|
+
if (isPlainObject(p)) {
|
|
190
|
+
for (const [k, list] of Object.entries(p)) {
|
|
191
|
+
if (!PIPELINE_KEYS.has(k)) {
|
|
192
|
+
warnings.push(`service.pipelines.${pid}.${k} is not carried; a Pipeline has receivers, processors and exporters`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (list === null || list === undefined) continue;
|
|
196
|
+
if (!Array.isArray(list)) {
|
|
197
|
+
warnings.push(`service.pipelines.${pid}.${k} is not a list; it is not carried`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
entry[k] = list.map(String);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
config.service.pipelines[pid] = entry;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { config, pins, semconv, warnings };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Which kind a section's components are. */
|
|
213
|
+
export const KIND_OF_SECTION: Record<string, ComponentKind> = Object.fromEntries(
|
|
214
|
+
COMPONENT_KINDS.map((k) => [SECTION_OF[k], k]),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
/** The OpenTelemetry Collector config parser `chant import` runs. */
|
|
218
|
+
export class OtelCollectorParser implements TemplateParser {
|
|
219
|
+
parse(content: string): TemplateIR {
|
|
220
|
+
const parsed = parseCollectorYaml(content);
|
|
221
|
+
const properties: CollectorResourceProperties = { config: parsed.config };
|
|
222
|
+
const metadata: CollectorResourceMetadata = { pins: parsed.pins, semconv: parsed.semconv };
|
|
223
|
+
return {
|
|
224
|
+
resources: [
|
|
225
|
+
{
|
|
226
|
+
logicalId: "collector",
|
|
227
|
+
type: COLLECTOR_RESOURCE_TYPE,
|
|
228
|
+
properties: properties as unknown as Record<string, unknown>,
|
|
229
|
+
metadata: metadata as unknown as Record<string, unknown>,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
parameters: [],
|
|
233
|
+
warnings: parsed.warnings,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|