@intentius/chant-lexicon-otel 0.101.0 → 0.104.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 +1 -1
- package/dist/attribution.d.ts +69 -0
- package/dist/attribution.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/connectors.d.ts.map +1 -1
- package/dist/components/processors.d.ts +1 -1
- package/dist/components/processors.d.ts.map +1 -1
- package/dist/components/sampling.d.ts +3 -0
- package/dist/components/sampling.d.ts.map +1 -1
- package/dist/composites/node-agent.d.ts +4 -3
- package/dist/composites/node-agent.d.ts.map +1 -1
- package/dist/config-hygiene.d.ts +33 -0
- package/dist/config-hygiene.d.ts.map +1 -0
- package/dist/define.d.ts +18 -2
- package/dist/define.d.ts.map +1 -1
- package/dist/genai.d.ts.map +1 -1
- package/dist/import/generator.d.ts +5 -1
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/init-templates.d.ts.map +1 -1
- package/dist/integrity.json +15 -5
- package/dist/lint/audit-catalog.d.ts +3 -2
- 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 +2 -2
- package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/otel118.d.ts +8 -0
- package/dist/lint/post-synth/otel118.d.ts.map +1 -0
- package/dist/lint/post-synth/otel119.d.ts +8 -0
- package/dist/lint/post-synth/otel119.d.ts.map +1 -0
- package/dist/lint/post-synth/otel120.d.ts +8 -0
- package/dist/lint/post-synth/otel120.d.ts.map +1 -0
- package/dist/lint/post-synth/otel121.d.ts +8 -0
- package/dist/lint/post-synth/otel121.d.ts.map +1 -0
- package/dist/lint/post-synth/otel122.d.ts +8 -0
- package/dist/lint/post-synth/otel122.d.ts.map +1 -0
- package/dist/lint/post-synth/otel123.d.ts +8 -0
- package/dist/lint/post-synth/otel123.d.ts.map +1 -0
- package/dist/lint/post-synth/otel124.d.ts +8 -0
- package/dist/lint/post-synth/otel124.d.ts.map +1 -0
- package/dist/lint/post-synth/otel125.d.ts +8 -0
- package/dist/lint/post-synth/otel125.d.ts.map +1 -0
- package/dist/lint/post-synth/otel126.d.ts +8 -0
- package/dist/lint/post-synth/otel126.d.ts.map +1 -0
- package/dist/lint/post-synth/otel127.d.ts +8 -0
- package/dist/lint/post-synth/otel127.d.ts.map +1 -0
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/metric-names.d.ts +29 -0
- package/dist/metric-names.d.ts.map +1 -1
- package/dist/model.d.ts +24 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/okf/index.md +10 -0
- package/dist/okf/rules/OTEL118.md +11 -0
- package/dist/okf/rules/OTEL119.md +11 -0
- package/dist/okf/rules/OTEL120.md +11 -0
- package/dist/okf/rules/OTEL121.md +11 -0
- package/dist/okf/rules/OTEL122.md +11 -0
- package/dist/okf/rules/OTEL123.md +11 -0
- package/dist/okf/rules/OTEL124.md +11 -0
- package/dist/okf/rules/OTEL125.md +11 -0
- package/dist/okf/rules/OTEL126.md +11 -0
- package/dist/okf/rules/OTEL127.md +11 -0
- package/dist/platform.d.ts +11 -4
- package/dist/platform.d.ts.map +1 -1
- package/dist/rules/literal-credential.ts +2 -3
- package/dist/rules/otel-helpers.ts +15 -4
- package/dist/rules/otel118.ts +17 -0
- package/dist/rules/otel119.ts +17 -0
- package/dist/rules/otel120.ts +17 -0
- package/dist/rules/otel121.ts +17 -0
- package/dist/rules/otel122.ts +17 -0
- package/dist/rules/otel123.ts +17 -0
- package/dist/rules/otel124.ts +17 -0
- package/dist/rules/otel125.ts +17 -0
- package/dist/rules/otel126.ts +17 -0
- package/dist/rules/otel127.ts +17 -0
- package/dist/semconv.d.ts +2 -1
- package/dist/semconv.d.ts.map +1 -1
- package/dist/skill-defs.d.ts +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-otel.md +7 -3
- package/dist/validate-config.d.ts +29 -3
- package/dist/validate-config.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/attribution.ts +385 -0
- package/src/codegen/docs.ts +8 -1
- package/src/components/k8s-receivers.test.ts +5 -2
- package/src/components/processors.ts +1 -1
- package/src/components/sampling.ts +3 -0
- package/src/composites/composites.test.ts +16 -0
- package/src/composites/node-agent.ts +3 -2
- package/src/config-hygiene.ts +470 -0
- package/src/connectors.test.ts +3 -0
- package/src/define.ts +25 -4
- package/src/import/generator.test.ts +42 -4
- package/src/import/generator.ts +7 -2
- package/src/import/parser.test.ts +11 -0
- package/src/import/parser.ts +16 -4
- package/src/import/roundtrip.test.ts +90 -14
- package/src/index.ts +7 -0
- package/src/init-templates.ts +4 -2
- package/src/lint/audit-catalog.ts +87 -2
- package/src/lint/post-synth/hygiene.test.ts +334 -0
- package/src/lint/post-synth/index.ts +20 -0
- package/src/lint/post-synth/otel-helpers.ts +15 -4
- package/src/lint/post-synth/otel118.test.ts +308 -0
- package/src/lint/post-synth/otel118.ts +17 -0
- package/src/lint/post-synth/otel119.ts +17 -0
- package/src/lint/post-synth/otel120.ts +17 -0
- package/src/lint/post-synth/otel121.ts +17 -0
- package/src/lint/post-synth/otel122.ts +17 -0
- package/src/lint/post-synth/otel123.ts +17 -0
- package/src/lint/post-synth/otel124.ts +17 -0
- package/src/lint/post-synth/otel125.ts +17 -0
- package/src/lint/post-synth/otel126.ts +17 -0
- package/src/lint/post-synth/otel127.ts +17 -0
- package/src/lint/post-synth/post-synth.test.ts +31 -0
- package/src/lint/rules/literal-credential.ts +2 -3
- package/src/metric-names.test.ts +24 -2
- package/src/metric-names.ts +66 -1
- package/src/model.ts +48 -0
- package/src/platform.test.ts +61 -0
- package/src/platform.ts +35 -22
- package/src/plugin.test.ts +10 -0
- package/src/semconv.ts +6 -2
- package/src/skills/chant-otel.md +7 -3
- package/src/validate-config.ts +85 -9
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
* read-only for `filelog`, and a service account that may get, list and
|
|
30
30
|
* watch pods, namespaces and nodes (and replicasets, for
|
|
31
31
|
* `k8s.deployment.name`). The k8s lexicon's `OtelCollector` takes this
|
|
32
|
-
* composite's entities as its `config` and derives
|
|
33
|
-
*
|
|
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.
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
import { Composite, type CompositeInstance } from "@intentius/chant/composite";
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collector config checks for deprecated fields, exposure and delivery
|
|
3
|
+
* settings, and two fields the collector checks against a fixed list
|
|
4
|
+
* (OTEL119-OTEL127). Each is a plain function over a parsed `CollectorConfig`,
|
|
5
|
+
* so it runs on emitted YAML, on an imported file and on a config inside a
|
|
6
|
+
* ConfigMap alike. `validateCollectorConfig` calls `configHygieneIssues`.
|
|
7
|
+
*
|
|
8
|
+
* Every list and version here is read from collector or collector-contrib
|
|
9
|
+
* v0.130.0 (`COLLECTOR_PIN`); the lint rules page cites the file for each.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseComponentId, type CollectorConfig } from "./model";
|
|
13
|
+
import { componentListeners, listenerPaths, type CollectorIssue } from "./validate-config";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Keys whose value is a credential. `*_file` keys name a path, which is fine.
|
|
17
|
+
* OTEL002 reads TypeScript source with this pattern and OTEL120 reads YAML
|
|
18
|
+
* with it, so the two agree on what a credential is.
|
|
19
|
+
*/
|
|
20
|
+
export const SECRET_KEY = /(authorization|api[-_]?key|password|passwd|secret|token|key_pem|x-honeycomb-team)/i;
|
|
21
|
+
|
|
22
|
+
type Body = Record<string, unknown>;
|
|
23
|
+
type Section = "receivers" | "processors" | "exporters" | "extensions" | "connectors";
|
|
24
|
+
const SECTIONS: Array<[Section, string]> = [
|
|
25
|
+
["receivers", "receiver"],
|
|
26
|
+
["processors", "processor"],
|
|
27
|
+
["exporters", "exporter"],
|
|
28
|
+
["extensions", "extension"],
|
|
29
|
+
["connectors", "connector"],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function isObject(value: unknown): value is Body {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function typeOf(id: string): string {
|
|
37
|
+
return parseComponentId(id)?.type ?? id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The host an endpoint names: `http://h:4318/v1`, `dns:///h:4317`, `h:4317`, `[::1]:4317` or `h`. */
|
|
41
|
+
function endpointHost(endpoint: string): string {
|
|
42
|
+
let rest = endpoint.trim().replace(/^[a-z][a-z0-9+.-]*:\/\/\/?/i, "");
|
|
43
|
+
rest = rest.split("/")[0];
|
|
44
|
+
const bracketed = rest.match(/^\[([^\]]*)\]/);
|
|
45
|
+
if (bracketed) return bracketed[1];
|
|
46
|
+
const colon = rest.lastIndexOf(":");
|
|
47
|
+
return colon > 0 && /^\d+$/.test(rest.slice(colon + 1)) ? rest.slice(0, colon) : rest;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isLoopback(host: string): boolean {
|
|
51
|
+
const h = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
52
|
+
return h === "localhost" || h === "::1" || /^127\.\d+\.\d+\.\d+$/.test(h);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The pipelines' started receivers, processors and exporters, by id. */
|
|
56
|
+
function startedIds(config: CollectorConfig, field: "receivers" | "processors" | "exporters"): Set<string> {
|
|
57
|
+
return new Set(Object.values(config.service?.pipelines ?? {}).flatMap((p) => (p?.[field] ?? []).map(String)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Where a non-listener exporter sends to, when it names a host other than
|
|
62
|
+
* loopback. `${env:...}` hosts count as remote: only a literal loopback
|
|
63
|
+
* address is known to stay on the machine.
|
|
64
|
+
*/
|
|
65
|
+
function remoteEndpoint(id: string, body: Body): string | undefined {
|
|
66
|
+
if (listenerPaths("exporter", typeOf(id), body).length > 0) return undefined; // prometheus serves, it doesn't send
|
|
67
|
+
const endpoint = ["endpoint", "traces_endpoint", "metrics_endpoint", "logs_endpoint"].map((k) => body[k]).find((v) => typeof v === "string" && v !== "");
|
|
68
|
+
if (typeof endpoint !== "string" || isLoopback(endpointHost(endpoint))) return undefined;
|
|
69
|
+
return endpoint;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── OTEL119: deprecated before the pin ───────────────────────────────
|
|
73
|
+
|
|
74
|
+
/** The paths under a tail_sampling policy that set `invert_match: true`. */
|
|
75
|
+
function invertMatchPaths(value: unknown, path: string[], out: string[]): string[] {
|
|
76
|
+
if (Array.isArray(value)) value.forEach((v, i) => invertMatchPaths(v, [...path.slice(0, -1), `${path[path.length - 1] ?? ""}[${i}]`], out));
|
|
77
|
+
else if (isObject(value)) {
|
|
78
|
+
for (const [key, v] of Object.entries(value)) {
|
|
79
|
+
if (key === "invert_match" && v === true) out.push([...path, key].join("."));
|
|
80
|
+
else invertMatchPaths(v, [...path, key], out);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* OTEL119: a field deprecated at or before the pin. The collector still
|
|
88
|
+
* reads each of these at v0.130.0, and a later release drops them.
|
|
89
|
+
*
|
|
90
|
+
* - `invert_match: true` in a `tail_sampling` policy: inverted decisions are
|
|
91
|
+
* deprecated in contrib v0.126.0 (#39833) for a `drop` policy.
|
|
92
|
+
* - `service.telemetry.metrics.address`: deprecated in core v0.111.0
|
|
93
|
+
* (#11205) for `readers`.
|
|
94
|
+
* - `dimensions_cache_size` on `spanmetrics`: deprecated in contrib v0.125.0
|
|
95
|
+
* (#39646), and marked so in its config at v0.130.0 (#41101), for
|
|
96
|
+
* `aggregation_cardinality_limit`.
|
|
97
|
+
*/
|
|
98
|
+
function deprecatedFieldIssues(config: CollectorConfig): CollectorIssue[] {
|
|
99
|
+
const issues: CollectorIssue[] = [];
|
|
100
|
+
for (const [id, raw] of Object.entries(config.processors ?? {})) {
|
|
101
|
+
if (typeOf(id) !== "tail_sampling" || !isObject(raw)) continue;
|
|
102
|
+
const policies = Array.isArray(raw.policies) ? raw.policies : [];
|
|
103
|
+
policies.forEach((policy, i) => {
|
|
104
|
+
const name = isObject(policy) && typeof policy.name === "string" ? `"${policy.name}"` : `policies[${i}]`;
|
|
105
|
+
for (const path of invertMatchPaths(policy, [], [])) {
|
|
106
|
+
issues.push({
|
|
107
|
+
code: "OTEL119",
|
|
108
|
+
severity: "warning",
|
|
109
|
+
component: id,
|
|
110
|
+
message: `processor "${id}" policy ${name} sets ${path}; inverted decisions are deprecated since collector-contrib v0.126.0. Use a drop policy to keep the traces out instead`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const metrics = (config.service?.telemetry as Body | undefined)?.metrics;
|
|
116
|
+
if (isObject(metrics) && metrics.address !== undefined) {
|
|
117
|
+
issues.push({
|
|
118
|
+
code: "OTEL119",
|
|
119
|
+
severity: "warning",
|
|
120
|
+
message: `service.telemetry.metrics.address is deprecated since collector v0.111.0; serve the collector's own metrics with service.telemetry.metrics.readers (pull.exporter.prometheus host and port) instead`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
for (const [id, raw] of Object.entries(config.connectors ?? {})) {
|
|
124
|
+
if (typeOf(id) !== "spanmetrics" || !isObject(raw) || raw.dimensions_cache_size === undefined) continue;
|
|
125
|
+
issues.push({
|
|
126
|
+
code: "OTEL119",
|
|
127
|
+
severity: "warning",
|
|
128
|
+
component: id,
|
|
129
|
+
message: `connector "${id}" sets dimensions_cache_size, deprecated since collector-contrib v0.125.0; use aggregation_cardinality_limit to bound its series instead`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return issues;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── OTEL120: literal credential in the config ────────────────────────
|
|
136
|
+
|
|
137
|
+
/** Every `key: value` under a component whose key names a credential and whose value is written out. */
|
|
138
|
+
function literalSecrets(value: unknown, path: string[], out: string[]): string[] {
|
|
139
|
+
if (Array.isArray(value)) value.forEach((v) => (isObject(v) || Array.isArray(v) ? literalSecrets(v, path, out) : undefined));
|
|
140
|
+
else if (isObject(value)) {
|
|
141
|
+
for (const [key, v] of Object.entries(value)) {
|
|
142
|
+
if (isObject(v) || Array.isArray(v)) literalSecrets(v, [...path, key], out);
|
|
143
|
+
else if (SECRET_KEY.test(key) && !/_file$/i.test(key) && (typeof v === "string" || typeof v === "number")) {
|
|
144
|
+
const text = String(v);
|
|
145
|
+
if (text !== "" && !text.includes("${")) out.push([...path, key].join("."));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* OTEL120: OTEL002's check over the config itself, so an imported config or
|
|
154
|
+
* one inside a ConfigMap gets it too. Same key pattern, same exceptions: a
|
|
155
|
+
* value containing `${` is a reference the collector expands, and a key
|
|
156
|
+
* ending in `_file` is a path. List items don't inherit their parent's key,
|
|
157
|
+
* as in OTEL002, so a list of header names is not a credential.
|
|
158
|
+
*/
|
|
159
|
+
function literalCredentialIssues(config: CollectorConfig): CollectorIssue[] {
|
|
160
|
+
const issues: CollectorIssue[] = [];
|
|
161
|
+
for (const [section, noun] of SECTIONS) {
|
|
162
|
+
for (const [id, body] of Object.entries(config[section] ?? {})) {
|
|
163
|
+
for (const path of literalSecrets(body, [], [])) {
|
|
164
|
+
issues.push({
|
|
165
|
+
code: "OTEL120",
|
|
166
|
+
severity: "error",
|
|
167
|
+
component: id,
|
|
168
|
+
message: `${noun} "${id}" has a literal credential at ${path}; write "\${env:NAME}" (or "\${file:/path}") and let the collector read it at start-up`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return issues;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── OTEL121: credentials over plaintext ──────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* OTEL121: a started exporter sends a credential (a header such as
|
|
180
|
+
* `authorization` or `api-key`, or a top-level key such as `api_key` or
|
|
181
|
+
* `token`) to a non-loopback endpoint without TLS. Plaintext means an
|
|
182
|
+
* `http://` endpoint, or `tls.insecure: true` on an endpoint that isn't
|
|
183
|
+
* `https://`: an HTTP exporter picks TLS from the scheme, a gRPC exporter
|
|
184
|
+
* from `tls.insecure`. A credential sent through an `auth` extension is not
|
|
185
|
+
* checked.
|
|
186
|
+
*/
|
|
187
|
+
function plaintextCredentialIssues(config: CollectorConfig): CollectorIssue[] {
|
|
188
|
+
const issues: CollectorIssue[] = [];
|
|
189
|
+
const started = startedIds(config, "exporters");
|
|
190
|
+
for (const [id, raw] of Object.entries(config.exporters ?? {})) {
|
|
191
|
+
if (!started.has(id) || !isObject(raw)) continue;
|
|
192
|
+
const endpoint = remoteEndpoint(id, raw);
|
|
193
|
+
if (!endpoint) continue;
|
|
194
|
+
const insecure = isObject(raw.tls) && raw.tls.insecure === true;
|
|
195
|
+
const plaintext = /^http:\/\//i.test(endpoint) || (insecure && !/^https:\/\//i.test(endpoint));
|
|
196
|
+
if (!plaintext) continue;
|
|
197
|
+
const headers = isObject(raw.headers) ? Object.keys(raw.headers).filter((k) => SECRET_KEY.test(k)).map((k) => `headers.${k}`) : [];
|
|
198
|
+
const keys = Object.entries(raw)
|
|
199
|
+
.filter(([k, v]) => SECRET_KEY.test(k) && !/_file$/i.test(k) && (typeof v === "string" || typeof v === "number") && String(v) !== "")
|
|
200
|
+
.map(([k]) => k);
|
|
201
|
+
const sent = [...headers, ...keys];
|
|
202
|
+
if (sent.length === 0) continue;
|
|
203
|
+
const why = /^http:\/\//i.test(endpoint) ? "an http:// endpoint" : "tls.insecure: true";
|
|
204
|
+
issues.push({
|
|
205
|
+
code: "OTEL121",
|
|
206
|
+
severity: "warning",
|
|
207
|
+
component: id,
|
|
208
|
+
message: `exporter "${id}" sends ${sent.join(", ")} to ${endpoint} over ${why}, so the credential crosses the network in plaintext. Use https:// or drop tls.insecure`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return issues;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── OTEL122: debug endpoints off loopback ────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The extensions OTEL122 reports off loopback. zpages serves live span
|
|
218
|
+
* samples and pipeline internals, pprof serves heap and goroutine profiles
|
|
219
|
+
* and can be made to run CPU profiles, and neither has authentication.
|
|
220
|
+
* `health_check` is left out on purpose: it serves only a status, and a
|
|
221
|
+
* kubelet probe reaches it on the pod IP, so `otlpCollector()`, `NodeAgent`
|
|
222
|
+
* and `genAiPipeline()` bind it on 0.0.0.0.
|
|
223
|
+
*/
|
|
224
|
+
const DEBUG_EXTENSIONS = new Set(["zpages", "pprof"]);
|
|
225
|
+
|
|
226
|
+
/** OTEL122: a started zpages or pprof extension listens on a non-loopback address. */
|
|
227
|
+
function debugEndpointIssues(config: CollectorConfig): CollectorIssue[] {
|
|
228
|
+
const issues: CollectorIssue[] = [];
|
|
229
|
+
const enabled = new Set((config.service?.extensions ?? []).map(String));
|
|
230
|
+
for (const [id, body] of Object.entries(config.extensions ?? {})) {
|
|
231
|
+
if (!enabled.has(id) || !DEBUG_EXTENSIONS.has(typeOf(id))) continue;
|
|
232
|
+
for (const listener of componentListeners("extension", id, body)) {
|
|
233
|
+
if (isLoopback(listener.host)) continue;
|
|
234
|
+
const host = listener.host === "" ? "every interface" : listener.host;
|
|
235
|
+
issues.push({
|
|
236
|
+
code: "OTEL122",
|
|
237
|
+
severity: "warning",
|
|
238
|
+
component: id,
|
|
239
|
+
message: `extension "${id}" listens on ${host}:${listener.port}, so anyone who reaches that address can read the collector's internals without authentication. Bind it to localhost:${listener.port} and port-forward when you need it`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return issues;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── OTEL123: detailed debug output beside a real exporter ────────────
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* OTEL123: a pipeline that sends to a backend also sends to a `debug`
|
|
250
|
+
* exporter at `verbosity: detailed`, which writes every record, with all
|
|
251
|
+
* its attributes and bodies, to the collector's own log. That copies what
|
|
252
|
+
* the backend gets, sensitive values included, to wherever the collector's
|
|
253
|
+
* logs go. A pipeline with only `debug` exporters is left alone: that is a
|
|
254
|
+
* test setup.
|
|
255
|
+
*/
|
|
256
|
+
function detailedDebugIssues(config: CollectorConfig): CollectorIssue[] {
|
|
257
|
+
const issues: CollectorIssue[] = [];
|
|
258
|
+
const exporters = config.exporters ?? {};
|
|
259
|
+
for (const [pipelineId, pipeline] of Object.entries(config.service?.pipelines ?? {})) {
|
|
260
|
+
const ids = (pipeline?.exporters ?? []).map(String).filter((id) => id in exporters);
|
|
261
|
+
const others = ids.filter((id) => typeOf(id) !== "debug");
|
|
262
|
+
if (others.length === 0) continue;
|
|
263
|
+
for (const id of ids) {
|
|
264
|
+
const body = exporters[id];
|
|
265
|
+
if (typeOf(id) !== "debug" || !isObject(body) || body.verbosity !== "detailed") continue;
|
|
266
|
+
issues.push({
|
|
267
|
+
code: "OTEL123",
|
|
268
|
+
severity: "warning",
|
|
269
|
+
pipeline: pipelineId,
|
|
270
|
+
component: id,
|
|
271
|
+
message: `pipeline "${pipelineId}" exports to ${others.map((o) => `"${o}"`).join(", ")} and to "${id}" at verbosity: detailed, which writes every record in full to the collector's log. Use verbosity: basic, or keep detailed output to a test pipeline`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return issues;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── OTEL124: queue or retry off for a remote exporter ────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* OTEL124: a started exporter sends to a remote endpoint with
|
|
282
|
+
* `sending_queue.enabled: false` or `retry_on_failure.enabled: false`.
|
|
283
|
+
* Without the queue a slow backend blocks the pipeline; without retries one
|
|
284
|
+
* failed request drops its data.
|
|
285
|
+
*/
|
|
286
|
+
function deliveryIssues(config: CollectorConfig): CollectorIssue[] {
|
|
287
|
+
const issues: CollectorIssue[] = [];
|
|
288
|
+
const started = startedIds(config, "exporters");
|
|
289
|
+
for (const [id, raw] of Object.entries(config.exporters ?? {})) {
|
|
290
|
+
if (!started.has(id) || !isObject(raw)) continue;
|
|
291
|
+
const endpoint = remoteEndpoint(id, raw);
|
|
292
|
+
if (!endpoint) continue;
|
|
293
|
+
if (isObject(raw.sending_queue) && raw.sending_queue.enabled === false) {
|
|
294
|
+
issues.push({
|
|
295
|
+
code: "OTEL124",
|
|
296
|
+
severity: "warning",
|
|
297
|
+
component: id,
|
|
298
|
+
message: `exporter "${id}" sends to ${endpoint} with sending_queue.enabled: false, so a slow or unreachable backend blocks the pipeline and data is refused upstream. Leave the queue on`,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (isObject(raw.retry_on_failure) && raw.retry_on_failure.enabled === false) {
|
|
302
|
+
issues.push({
|
|
303
|
+
code: "OTEL124",
|
|
304
|
+
severity: "warning",
|
|
305
|
+
component: id,
|
|
306
|
+
message: `exporter "${id}" sends to ${endpoint} with retry_on_failure.enabled: false, so one failed request drops its data. Leave retries on`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return issues;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── OTEL125: no batching before a network exporter ───────────────────
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The exporters OTEL125 knows send one request per incoming batch unless
|
|
317
|
+
* told otherwise: `otlp` and `otlphttp`, whose default `sending_queue` at
|
|
318
|
+
* v0.130.0 has no `batch` (`exporterhelper/internal/queue_sender.go`,
|
|
319
|
+
* `NewDefaultQueueConfig`). Other exporters may batch on their own, so they
|
|
320
|
+
* are not checked.
|
|
321
|
+
*/
|
|
322
|
+
const UNBATCHED_EXPORTERS = new Set(["otlp", "otlphttp"]);
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* OTEL125: a pipeline sends to a remote `otlp` or `otlphttp` exporter, has
|
|
326
|
+
* no `batch` processor, and the exporter does not batch in its own
|
|
327
|
+
* `sending_queue.batch`. Each small request then goes out on its own.
|
|
328
|
+
*/
|
|
329
|
+
function batchIssues(config: CollectorConfig): CollectorIssue[] {
|
|
330
|
+
const issues: CollectorIssue[] = [];
|
|
331
|
+
const exporters = config.exporters ?? {};
|
|
332
|
+
for (const [pipelineId, pipeline] of Object.entries(config.service?.pipelines ?? {})) {
|
|
333
|
+
if ((pipeline?.processors ?? []).some((id) => typeOf(String(id)) === "batch")) continue;
|
|
334
|
+
for (const id of (pipeline?.exporters ?? []).map(String)) {
|
|
335
|
+
const body = exporters[id];
|
|
336
|
+
if (!UNBATCHED_EXPORTERS.has(typeOf(id)) || !isObject(body)) continue;
|
|
337
|
+
const endpoint = remoteEndpoint(id, body);
|
|
338
|
+
if (!endpoint) continue;
|
|
339
|
+
const queue = body.sending_queue;
|
|
340
|
+
if (isObject(queue) && queue.batch !== undefined && queue.batch !== null) continue;
|
|
341
|
+
issues.push({
|
|
342
|
+
code: "OTEL125",
|
|
343
|
+
severity: "warning",
|
|
344
|
+
pipeline: pipelineId,
|
|
345
|
+
component: id,
|
|
346
|
+
message: `pipeline "${pipelineId}" sends to "${id}" (${endpoint}) with no batch processor, and the exporter doesn't batch on its own, so every small request goes out by itself. Add a batch processor to the pipeline, or set sending_queue.batch on the exporter`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return issues;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── OTEL126, OTEL127: names checked against a fixed list ─────────────
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The fields `k8sattributes` can extract at v0.130.0: the cases of
|
|
357
|
+
* `Config.Validate` (`processor/k8sattributesprocessor/config.go`), with
|
|
358
|
+
* the semconv v1.6.1 keys and `options.go` constants they name.
|
|
359
|
+
*/
|
|
360
|
+
export const K8S_ATTRIBUTES_METADATA: readonly string[] = Object.freeze([
|
|
361
|
+
"k8s.namespace.name",
|
|
362
|
+
"k8s.pod.name",
|
|
363
|
+
"k8s.pod.uid",
|
|
364
|
+
"k8s.pod.hostname",
|
|
365
|
+
"k8s.pod.start_time",
|
|
366
|
+
"k8s.pod.ip",
|
|
367
|
+
"k8s.deployment.name",
|
|
368
|
+
"k8s.deployment.uid",
|
|
369
|
+
"k8s.replicaset.name",
|
|
370
|
+
"k8s.replicaset.uid",
|
|
371
|
+
"k8s.daemonset.name",
|
|
372
|
+
"k8s.daemonset.uid",
|
|
373
|
+
"k8s.statefulset.name",
|
|
374
|
+
"k8s.statefulset.uid",
|
|
375
|
+
"k8s.job.name",
|
|
376
|
+
"k8s.job.uid",
|
|
377
|
+
"k8s.cronjob.name",
|
|
378
|
+
"k8s.node.name",
|
|
379
|
+
"k8s.node.uid",
|
|
380
|
+
"k8s.container.name",
|
|
381
|
+
"container.id",
|
|
382
|
+
"container.image.name",
|
|
383
|
+
"container.image.tag",
|
|
384
|
+
"service.namespace",
|
|
385
|
+
"service.name",
|
|
386
|
+
"service.version",
|
|
387
|
+
"service.instance.id",
|
|
388
|
+
"container.image.repo_digests",
|
|
389
|
+
"k8s.cluster.uid",
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The detectors `resourcedetection` knows at v0.130.0: the keys of the map
|
|
394
|
+
* in `NewFactory` (`processor/resourcedetectionprocessor/factory.go`), as
|
|
395
|
+
* each detector's `TypeStr` spells them.
|
|
396
|
+
*/
|
|
397
|
+
export const RESOURCE_DETECTORS: readonly string[] = Object.freeze([
|
|
398
|
+
"aks",
|
|
399
|
+
"azure",
|
|
400
|
+
"consul",
|
|
401
|
+
"docker",
|
|
402
|
+
"ec2",
|
|
403
|
+
"ecs",
|
|
404
|
+
"eks",
|
|
405
|
+
"elastic_beanstalk",
|
|
406
|
+
"lambda",
|
|
407
|
+
"env",
|
|
408
|
+
"gcp",
|
|
409
|
+
"heroku",
|
|
410
|
+
"system",
|
|
411
|
+
"openshift",
|
|
412
|
+
"k8snode",
|
|
413
|
+
"kubeadm",
|
|
414
|
+
"dynatrace",
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
/** OTEL126: `k8sattributes` `extract.metadata` names a field the processor can't extract, so the collector refuses the config. */
|
|
418
|
+
function k8sAttributesIssues(config: CollectorConfig): CollectorIssue[] {
|
|
419
|
+
const known = new Set(K8S_ATTRIBUTES_METADATA);
|
|
420
|
+
const issues: CollectorIssue[] = [];
|
|
421
|
+
for (const [id, raw] of Object.entries(config.processors ?? {})) {
|
|
422
|
+
if (typeOf(id) !== "k8sattributes" || !isObject(raw) || !isObject(raw.extract)) continue;
|
|
423
|
+
for (const field of Array.isArray(raw.extract.metadata) ? raw.extract.metadata : []) {
|
|
424
|
+
if (typeof field !== "string" || known.has(field)) continue;
|
|
425
|
+
issues.push({
|
|
426
|
+
code: "OTEL126",
|
|
427
|
+
severity: "error",
|
|
428
|
+
component: id,
|
|
429
|
+
message: `processor "${id}" extracts "${field}" (extract.metadata), which k8sattributes does not support at the pinned release; the collector refuses to start`,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return issues;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** OTEL127: a started `resourcedetection` lists a detector the processor doesn't have, so it fails to build and the collector exits. */
|
|
437
|
+
function detectorIssues(config: CollectorConfig): CollectorIssue[] {
|
|
438
|
+
const known = new Set(RESOURCE_DETECTORS);
|
|
439
|
+
const started = startedIds(config, "processors");
|
|
440
|
+
const issues: CollectorIssue[] = [];
|
|
441
|
+
for (const [id, raw] of Object.entries(config.processors ?? {})) {
|
|
442
|
+
if (typeOf(id) !== "resourcedetection" || !started.has(id) || !isObject(raw)) continue;
|
|
443
|
+
for (const detector of Array.isArray(raw.detectors) ? raw.detectors : []) {
|
|
444
|
+
if (typeof detector !== "string" || detector.includes("${") || known.has(detector.trim())) continue;
|
|
445
|
+
const hint = detector === "elasticbeanstalk" ? ' (the detector is "elastic_beanstalk")' : "";
|
|
446
|
+
issues.push({
|
|
447
|
+
code: "OTEL127",
|
|
448
|
+
severity: "error",
|
|
449
|
+
component: id,
|
|
450
|
+
message: `processor "${id}" lists detector "${detector}"${hint}, which resourcedetection does not have at the pinned release; the processor fails to build and the collector exits`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return issues;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** OTEL119-OTEL127 over one collector config. */
|
|
458
|
+
export function configHygieneIssues(config: CollectorConfig): CollectorIssue[] {
|
|
459
|
+
return [
|
|
460
|
+
...deprecatedFieldIssues(config),
|
|
461
|
+
...literalCredentialIssues(config),
|
|
462
|
+
...plaintextCredentialIssues(config),
|
|
463
|
+
...debugEndpointIssues(config),
|
|
464
|
+
...detailedDebugIssues(config),
|
|
465
|
+
...deliveryIssues(config),
|
|
466
|
+
...batchIssues(config),
|
|
467
|
+
...k8sAttributesIssues(config),
|
|
468
|
+
...detectorIssues(config),
|
|
469
|
+
];
|
|
470
|
+
}
|
package/src/connectors.test.ts
CHANGED
|
@@ -235,6 +235,9 @@ service:
|
|
|
235
235
|
expect(diags.every((d) => d.severity === "error" && d.checkId === "OTEL112")).toBe(true);
|
|
236
236
|
// One-sided use is OTEL101's, not this rule's.
|
|
237
237
|
expect(otel101.check(ctxFor(yaml))).toEqual([]);
|
|
238
|
+
// span_metrics, the collector's newer name, is checked the same.
|
|
239
|
+
const renamed = otel112.check(ctxFor(yaml.replace(/spanmetrics/g, "span_metrics")));
|
|
240
|
+
expect(renamed.map((d) => d.message)).toEqual(diags.map((d) => d.message.replace(/spanmetrics/g, "span_metrics")));
|
|
238
241
|
});
|
|
239
242
|
|
|
240
243
|
test("flags only the pipeline that pairs with nothing", () => {
|
package/src/define.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* non-built-in component was checked against. The collector ignores it.
|
|
26
26
|
* - OTEL109 fails a build whose custom component has no usable pin.
|
|
27
27
|
*
|
|
28
|
-
* Semantic-convention pins (`GENAI_SEMCONV_PIN`) record which version of an
|
|
28
|
+
* Semantic-convention pins (`SEMCONV_PIN`, `GENAI_SEMCONV_PIN`) record which version of an
|
|
29
29
|
* attribute vocabulary a preset's keys follow. They are not tied to a
|
|
30
30
|
* component type, so `collectorTopology()` reports them under `semconv` for
|
|
31
31
|
* each component whose config uses that vocabulary, and the serializer writes
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import { createResource } from "@intentius/chant/runtime";
|
|
36
36
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
37
|
-
import { componentId, type ComponentKind, type ConnectorSignalPair } from "./model";
|
|
37
|
+
import { canonicalComponentType, componentId, type ComponentKind, type ConnectorSignalPair } from "./model";
|
|
38
38
|
|
|
39
39
|
/** Where a component's config schema comes from, and which version of it the type follows. */
|
|
40
40
|
export interface SchemaPin {
|
|
@@ -55,6 +55,22 @@ export const COLLECTOR_PIN: SchemaPin = Object.freeze({
|
|
|
55
55
|
version: "v0.130.0",
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The semantic-conventions release this package's other attribute keys
|
|
60
|
+
* follow, today the `k8s.*` keys `NodeAgent` writes. v1.27.0 is the newest
|
|
61
|
+
* release the Kubernetes components of contrib v0.130.0 import:
|
|
62
|
+
* `go.opentelemetry.io/otel/semconv/v1.27.0` in the kubeletstats, k8sobjects
|
|
63
|
+
* and k8sevents receivers. The k8sattributes processor still imports v1.6.1
|
|
64
|
+
* and v1.8.0, and v1.27.0 keeps the `k8s.*` keys it writes. Like
|
|
65
|
+
* `COLLECTOR_PIN`, it moves only when this package does, and
|
|
66
|
+
* `collectorTopology()` reports it for every component whose config names a
|
|
67
|
+
* `k8s.` attribute.
|
|
68
|
+
*/
|
|
69
|
+
export const SEMCONV_PIN: SchemaPin = Object.freeze({
|
|
70
|
+
source: "github.com/open-telemetry/semantic-conventions",
|
|
71
|
+
version: "v1.27.0",
|
|
72
|
+
});
|
|
73
|
+
|
|
58
74
|
/**
|
|
59
75
|
* The OpenTelemetry GenAI semantic conventions the GenAI preset
|
|
60
76
|
* (`genAiPipeline()`) follows: which attributes carry prompt and completion
|
|
@@ -177,9 +193,14 @@ export function definitionFor(entityType: string): ComponentDefinition | undefin
|
|
|
177
193
|
return registry().get(entityType);
|
|
178
194
|
}
|
|
179
195
|
|
|
180
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* The definition for a collector `kind` + `type`, e.g. the built-in
|
|
198
|
+
* `exporter` `otlp`. A renamed built-in's new name (`otlp_grpc`) finds the
|
|
199
|
+
* built-in unless a component is registered under the new name itself.
|
|
200
|
+
*/
|
|
181
201
|
export function definitionOf(kind: ComponentKind, type: string): ComponentDefinition | undefined {
|
|
182
|
-
|
|
202
|
+
const reg = registry();
|
|
203
|
+
return reg.get(componentEntityType(kind, type)) ?? reg.get(componentEntityType(kind, canonicalComponentType(kind, type)));
|
|
183
204
|
}
|
|
184
205
|
|
|
185
206
|
/** Every registered definition, built-ins first in registration order. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import type
|
|
2
|
+
import { COMPONENT_TYPE_ALIASES, type CollectorConfig } from "../model";
|
|
3
3
|
import { builtinClassName, generateCollectorFiles, OtelCollectorGenerator, tsLiteral } from "./generator";
|
|
4
4
|
import { OtelCollectorParser } from "./parser";
|
|
5
5
|
|
|
@@ -17,6 +17,44 @@ describe("builtinClassName", () => {
|
|
|
17
17
|
expect(builtinClassName("extension", "health_check")).toBe("HealthCheckExtension");
|
|
18
18
|
expect(builtinClassName("exporter", "datadog")).toBeUndefined();
|
|
19
19
|
});
|
|
20
|
+
|
|
21
|
+
test("maps a renamed built-in's newer name to the class of the old one", () => {
|
|
22
|
+
for (const a of COMPONENT_TYPE_ALIASES) {
|
|
23
|
+
expect(builtinClassName(a.kind, a.type), a.type).toBe(builtinClassName(a.kind, a.builtin));
|
|
24
|
+
expect(builtinClassName(a.kind, a.type), a.type).toBeDefined();
|
|
25
|
+
}
|
|
26
|
+
expect(builtinClassName("exporter", "otlp_grpc")).toBe("OtlpExporter");
|
|
27
|
+
// The rename is per kind: the otlp receiver kept its name.
|
|
28
|
+
expect(builtinClassName("receiver", "otlp_grpc")).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("a config using the collector's newer names", () => {
|
|
33
|
+
test("imports to the built-ins and warns that the old name is written", () => {
|
|
34
|
+
const yaml = [
|
|
35
|
+
"receivers: { otlp: { protocols: { grpc: null } } }",
|
|
36
|
+
"exporters: { otlp_grpc/tempo: { endpoint: 'tempo:4317' }, prometheus: { endpoint: '0.0.0.0:8889' } }",
|
|
37
|
+
"connectors: { span_metrics: {} }",
|
|
38
|
+
"service:",
|
|
39
|
+
" pipelines:",
|
|
40
|
+
" traces: { receivers: [otlp], exporters: [otlp_grpc/tempo, span_metrics] }",
|
|
41
|
+
" metrics: { receivers: [span_metrics], exporters: [prometheus] }",
|
|
42
|
+
].join("\n");
|
|
43
|
+
const ir = new OtelCollectorParser().parse(yaml);
|
|
44
|
+
expect(ir.warnings).toEqual([
|
|
45
|
+
'exporters.otlp_grpc/tempo uses "otlp_grpc", the collector\'s newer name for "otlp"; it imports as the built-in, which writes "otlp"',
|
|
46
|
+
'connectors.span_metrics uses "span_metrics", the collector\'s newer name for "spanmetrics"; it imports as the built-in, which writes "spanmetrics"',
|
|
47
|
+
]);
|
|
48
|
+
const out = Object.fromEntries(new OtelCollectorGenerator().generate(ir).map((f) => [f.path, f.content]));
|
|
49
|
+
expect(out["exporters.ts"]).toContain('new OtlpExporter({ name: "tempo", endpoint: "tempo:4317" })');
|
|
50
|
+
expect(out["connectors.ts"]).toContain("new SpanMetricsConnector()");
|
|
51
|
+
expect(Object.keys(out)).not.toContain("custom-components.ts");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("warns when an old and a new name collapse to one id", () => {
|
|
55
|
+
const ir = new OtelCollectorParser().parse("connectors: { spanmetrics: {}, span_metrics: {} }\n");
|
|
56
|
+
expect(ir.warnings).toContain('connectors.span_metrics and connectors.spanmetrics become the same id "spanmetrics"; rename one of them');
|
|
57
|
+
});
|
|
20
58
|
});
|
|
21
59
|
|
|
22
60
|
describe("tsLiteral", () => {
|
|
@@ -83,14 +121,14 @@ describe("generateCollectorFiles", () => {
|
|
|
83
121
|
|
|
84
122
|
test("a component type chant does not ship is defined once with defineComponent and COLLECTOR_PIN", () => {
|
|
85
123
|
const out = files({
|
|
86
|
-
exporters: { "datadog/a": { api: { key: "${env:DD}" } }, "datadog/b": {},
|
|
87
|
-
service: { pipelines: { logs: { receivers: [], exporters: ["datadog/a", "datadog/b", "
|
|
124
|
+
exporters: { "datadog/a": { api: { key: "${env:DD}" } }, "datadog/b": {}, otlp__http: {} },
|
|
125
|
+
service: { pipelines: { logs: { receivers: [], exporters: ["datadog/a", "datadog/b", "otlp__http"] } } },
|
|
88
126
|
});
|
|
89
127
|
const custom = out["custom-components.ts"];
|
|
90
128
|
expect(custom).toContain('import { COLLECTOR_PIN, defineComponent } from "@intentius/chant-lexicon-otel";');
|
|
91
129
|
expect(custom.match(/defineComponent<Record<string, unknown>>\(\)/g)).toHaveLength(2);
|
|
92
130
|
expect(custom).toContain('const DatadogExporter = defineComponent<Record<string, unknown>>()({\n kind: "exporter",\n type: "datadog",\n pin: COLLECTOR_PIN,\n});');
|
|
93
|
-
// `OtlpHttpExporter` is the built-in otlphttp exporter's class, so
|
|
131
|
+
// `OtlpHttpExporter` is the built-in otlphttp exporter's class, so `otlp__http` gets a numbered one.
|
|
94
132
|
expect(custom).toContain("const OtlpHttpExporter2 = defineComponent");
|
|
95
133
|
expect(custom).toContain("export { DatadogExporter, OtlpHttpExporter2 };");
|
|
96
134
|
expect(out["exporters.ts"]).toContain('import { DatadogExporter, OtlpHttpExporter2 } from "./custom-components";');
|
package/src/import/generator.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { COLLECTOR_PIN, type ComponentClass, type SchemaPin } from "../define";
|
|
|
25
25
|
import {
|
|
26
26
|
COMPONENT_KINDS,
|
|
27
27
|
SECTION_OF,
|
|
28
|
+
canonicalComponentType,
|
|
28
29
|
SIGNALS,
|
|
29
30
|
parseComponentId,
|
|
30
31
|
pipelineSignal,
|
|
@@ -58,9 +59,13 @@ const BUILTIN_CLASSES: ReadonlyMap<string, string> = (() => {
|
|
|
58
59
|
return map;
|
|
59
60
|
})();
|
|
60
61
|
|
|
61
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* The class name of the built-in for `kind` + `type`, if chant ships one.
|
|
64
|
+
* A renamed built-in's new name (`span_metrics`) gives the class for the old
|
|
65
|
+
* one, which emits the old name.
|
|
66
|
+
*/
|
|
62
67
|
export function builtinClassName(kind: ComponentKind, type: string): string | undefined {
|
|
63
|
-
return BUILTIN_CLASSES.get(`${kind}:${type}`);
|
|
68
|
+
return BUILTIN_CLASSES.get(`${kind}:${type}`) ?? BUILTIN_CLASSES.get(`${kind}:${canonicalComponentType(kind, type)}`);
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
const RESERVED = new Set(
|
|
@@ -110,6 +110,17 @@ exporters:
|
|
|
110
110
|
expect(pins).toEqual([]);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
test("a k8s semconv line at another version is read and warned about", () => {
|
|
114
|
+
const { semconv, warnings } = parseCollectorYaml(
|
|
115
|
+
["# chant: semconv k8s github.com/open-telemetry/semantic-conventions@v1.21.0 (k8sattributes)", "receivers: {}"].join("\n"),
|
|
116
|
+
);
|
|
117
|
+
expect(semconv).toEqual([{ namespace: "k8s", pin: { source: "github.com/open-telemetry/semantic-conventions", version: "v1.21.0" } }]);
|
|
118
|
+
expect(warnings).toEqual([
|
|
119
|
+
"the config was built against k8s semantic conventions github.com/open-telemetry/semantic-conventions@v1.21.0; " +
|
|
120
|
+
"this lexicon follows github.com/open-telemetry/semantic-conventions@v1.27.0, which the rebuilt config will name",
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
113
124
|
test("names what it cannot carry", () => {
|
|
114
125
|
const { config, warnings } = parseCollectorYaml(
|
|
115
126
|
[
|