@intentius/chant-lexicon-otel 0.124.31 → 0.124.33
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/dist/attribution.js +391 -0
- package/dist/catalog.js +42 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +101 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate.js +43 -0
- package/dist/codegen/package.js +28 -0
- package/dist/collector.js +150 -0
- package/dist/components/common.js +50 -0
- package/dist/components/connectors.js +242 -0
- package/dist/components/exporters.js +57 -0
- package/dist/components/extensions.js +58 -0
- package/dist/components/filtering.js +106 -0
- package/dist/components/index.js +9 -0
- package/dist/components/k8s-receivers.js +36 -0
- package/dist/components/processors.js +103 -0
- package/dist/components/receivers.js +61 -0
- package/dist/components/sampling.js +257 -0
- package/dist/composites/catalog.js +368 -0
- package/dist/composites/genai-pipeline.js +28 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/node-agent.js +156 -0
- package/dist/composites/otlp-collector.js +57 -0
- package/dist/composites/red-metrics.js +95 -0
- package/dist/composites/tail-sampling-tier.js +132 -0
- package/dist/config-hygiene.js +464 -0
- package/dist/configmap.js +86 -0
- package/dist/define.js +204 -0
- package/dist/detect.js +22 -0
- package/dist/genai.js +569 -0
- package/dist/import/embedded.js +57 -0
- package/dist/import/generator.js +459 -0
- package/dist/import/parser.js +199 -0
- package/dist/index.js +34 -0
- package/dist/init-templates.js +119 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +49 -0
- package/dist/lint/post-synth/index.js +52 -0
- package/dist/lint/post-synth/otel-helpers.js +103 -0
- package/dist/lint/post-synth/otel101.js +13 -0
- package/dist/lint/post-synth/otel102.js +13 -0
- package/dist/lint/post-synth/otel103.js +13 -0
- package/dist/lint/post-synth/otel104.js +13 -0
- package/dist/lint/post-synth/otel105.js +13 -0
- package/dist/lint/post-synth/otel106.js +13 -0
- package/dist/lint/post-synth/otel107.js +13 -0
- package/dist/lint/post-synth/otel108.js +13 -0
- package/dist/lint/post-synth/otel109.js +13 -0
- package/dist/lint/post-synth/otel112.js +13 -0
- package/dist/lint/post-synth/otel113.js +13 -0
- package/dist/lint/post-synth/otel114.js +13 -0
- package/dist/lint/post-synth/otel115.js +13 -0
- package/dist/lint/post-synth/otel116.js +13 -0
- package/dist/lint/post-synth/otel117.js +13 -0
- package/dist/lint/post-synth/otel118.js +13 -0
- package/dist/lint/post-synth/otel119.js +13 -0
- package/dist/lint/post-synth/otel120.js +13 -0
- package/dist/lint/post-synth/otel121.js +13 -0
- package/dist/lint/post-synth/otel122.js +13 -0
- package/dist/lint/post-synth/otel123.js +13 -0
- package/dist/lint/post-synth/otel124.js +13 -0
- package/dist/lint/post-synth/otel125.js +13 -0
- package/dist/lint/post-synth/otel126.js +13 -0
- package/dist/lint/post-synth/otel127.js +13 -0
- package/dist/lint/rules/component-id-syntax.js +66 -0
- package/dist/lint/rules/index.js +6 -0
- package/dist/lint/rules/literal-credential.js +91 -0
- package/dist/lint/rules/otel-ast.js +30 -0
- package/dist/lsp/completions.js +11 -0
- package/dist/lsp/hover.js +23 -0
- package/dist/manifest.json +1 -1
- package/dist/metric-names.js +244 -0
- package/dist/model.js +90 -0
- package/dist/op/activities/collector-audit.js +213 -0
- package/dist/op/activities/collector-health.js +176 -0
- package/dist/op/activities/exec.js +16 -0
- package/dist/op/activities/github.js +110 -0
- package/dist/op/activities/index.js +16 -0
- package/dist/op/activities/otelcol.js +173 -0
- package/dist/op/activity-contracts.js +64 -0
- package/dist/op/audit-op.js +33 -0
- package/dist/op/builders.js +38 -0
- package/dist/package-cli.js +14 -0
- package/dist/pipeline.js +31 -0
- package/dist/platform.js +144 -0
- package/dist/plugin.js +126 -0
- package/dist/semconv.js +50 -0
- package/dist/serializer.js +26 -0
- package/dist/skill-defs.js +34 -0
- package/dist/topology.js +104 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate-config.js +706 -0
- package/dist/validate.js +85 -0
- package/dist/yaml.js +157 -0
- package/package.json +8 -8
- package/src/codegen/package.ts +2 -2
|
@@ -0,0 +1,464 @@
|
|
|
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
|
+
import { parseComponentId } from "./model.js";
|
|
12
|
+
import { componentListeners, listenerPaths } from "./validate-config.js";
|
|
13
|
+
/**
|
|
14
|
+
* Keys whose value is a credential. `*_file` keys name a path, which is fine.
|
|
15
|
+
* OTEL002 reads TypeScript source with this pattern and OTEL120 reads YAML
|
|
16
|
+
* with it, so the two agree on what a credential is.
|
|
17
|
+
*/
|
|
18
|
+
export const SECRET_KEY = /(authorization|api[-_]?key|password|passwd|secret|token|key_pem|x-honeycomb-team)/i;
|
|
19
|
+
const SECTIONS = [
|
|
20
|
+
["receivers", "receiver"],
|
|
21
|
+
["processors", "processor"],
|
|
22
|
+
["exporters", "exporter"],
|
|
23
|
+
["extensions", "extension"],
|
|
24
|
+
["connectors", "connector"],
|
|
25
|
+
];
|
|
26
|
+
function isObject(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function typeOf(id) {
|
|
30
|
+
return parseComponentId(id)?.type ?? id;
|
|
31
|
+
}
|
|
32
|
+
/** The host an endpoint names: `http://h:4318/v1`, `dns:///h:4317`, `h:4317`, `[::1]:4317` or `h`. */
|
|
33
|
+
function endpointHost(endpoint) {
|
|
34
|
+
let rest = endpoint.trim().replace(/^[a-z][a-z0-9+.-]*:\/\/\/?/i, "");
|
|
35
|
+
rest = rest.split("/")[0];
|
|
36
|
+
const bracketed = rest.match(/^\[([^\]]*)\]/);
|
|
37
|
+
if (bracketed)
|
|
38
|
+
return bracketed[1];
|
|
39
|
+
const colon = rest.lastIndexOf(":");
|
|
40
|
+
return colon > 0 && /^\d+$/.test(rest.slice(colon + 1)) ? rest.slice(0, colon) : rest;
|
|
41
|
+
}
|
|
42
|
+
function isLoopback(host) {
|
|
43
|
+
const h = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
44
|
+
return h === "localhost" || h === "::1" || /^127\.\d+\.\d+\.\d+$/.test(h);
|
|
45
|
+
}
|
|
46
|
+
/** The pipelines' started receivers, processors and exporters, by id. */
|
|
47
|
+
function startedIds(config, field) {
|
|
48
|
+
return new Set(Object.values(config.service?.pipelines ?? {}).flatMap((p) => (p?.[field] ?? []).map(String)));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Where a non-listener exporter sends to, when it names a host other than
|
|
52
|
+
* loopback. `${env:...}` hosts count as remote: only a literal loopback
|
|
53
|
+
* address is known to stay on the machine.
|
|
54
|
+
*/
|
|
55
|
+
function remoteEndpoint(id, body) {
|
|
56
|
+
if (listenerPaths("exporter", typeOf(id), body).length > 0)
|
|
57
|
+
return undefined; // prometheus serves, it doesn't send
|
|
58
|
+
const endpoint = ["endpoint", "traces_endpoint", "metrics_endpoint", "logs_endpoint"].map((k) => body[k]).find((v) => typeof v === "string" && v !== "");
|
|
59
|
+
if (typeof endpoint !== "string" || isLoopback(endpointHost(endpoint)))
|
|
60
|
+
return undefined;
|
|
61
|
+
return endpoint;
|
|
62
|
+
}
|
|
63
|
+
// ── OTEL119: deprecated before the pin ───────────────────────────────
|
|
64
|
+
/** The paths under a tail_sampling policy that set `invert_match: true`. */
|
|
65
|
+
function invertMatchPaths(value, path, out) {
|
|
66
|
+
if (Array.isArray(value))
|
|
67
|
+
value.forEach((v, i) => invertMatchPaths(v, [...path.slice(0, -1), `${path[path.length - 1] ?? ""}[${i}]`], out));
|
|
68
|
+
else if (isObject(value)) {
|
|
69
|
+
for (const [key, v] of Object.entries(value)) {
|
|
70
|
+
if (key === "invert_match" && v === true)
|
|
71
|
+
out.push([...path, key].join("."));
|
|
72
|
+
else
|
|
73
|
+
invertMatchPaths(v, [...path, key], out);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* OTEL119: a field deprecated at or before the pin. The collector still
|
|
80
|
+
* reads each of these at v0.130.0, and a later release drops them.
|
|
81
|
+
*
|
|
82
|
+
* - `invert_match: true` in a `tail_sampling` policy: inverted decisions are
|
|
83
|
+
* deprecated in contrib v0.126.0 (#39833) for a `drop` policy.
|
|
84
|
+
* - `service.telemetry.metrics.address`: deprecated in core v0.111.0
|
|
85
|
+
* (#11205) for `readers`.
|
|
86
|
+
* - `dimensions_cache_size` on `spanmetrics`: deprecated in contrib v0.125.0
|
|
87
|
+
* (#39646), and marked so in its config at v0.130.0 (#41101), for
|
|
88
|
+
* `aggregation_cardinality_limit`.
|
|
89
|
+
*/
|
|
90
|
+
function deprecatedFieldIssues(config) {
|
|
91
|
+
const issues = [];
|
|
92
|
+
for (const [id, raw] of Object.entries(config.processors ?? {})) {
|
|
93
|
+
if (typeOf(id) !== "tail_sampling" || !isObject(raw))
|
|
94
|
+
continue;
|
|
95
|
+
const policies = Array.isArray(raw.policies) ? raw.policies : [];
|
|
96
|
+
policies.forEach((policy, i) => {
|
|
97
|
+
const name = isObject(policy) && typeof policy.name === "string" ? `"${policy.name}"` : `policies[${i}]`;
|
|
98
|
+
for (const path of invertMatchPaths(policy, [], [])) {
|
|
99
|
+
issues.push({
|
|
100
|
+
code: "OTEL119",
|
|
101
|
+
severity: "warning",
|
|
102
|
+
component: id,
|
|
103
|
+
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`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const metrics = config.service?.telemetry?.metrics;
|
|
109
|
+
if (isObject(metrics) && metrics.address !== undefined) {
|
|
110
|
+
issues.push({
|
|
111
|
+
code: "OTEL119",
|
|
112
|
+
severity: "warning",
|
|
113
|
+
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`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
for (const [id, raw] of Object.entries(config.connectors ?? {})) {
|
|
117
|
+
if (typeOf(id) !== "spanmetrics" || !isObject(raw) || raw.dimensions_cache_size === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
issues.push({
|
|
120
|
+
code: "OTEL119",
|
|
121
|
+
severity: "warning",
|
|
122
|
+
component: id,
|
|
123
|
+
message: `connector "${id}" sets dimensions_cache_size, deprecated since collector-contrib v0.125.0; use aggregation_cardinality_limit to bound its series instead`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return issues;
|
|
127
|
+
}
|
|
128
|
+
// ── OTEL120: literal credential in the config ────────────────────────
|
|
129
|
+
/** Every `key: value` under a component whose key names a credential and whose value is written out. */
|
|
130
|
+
function literalSecrets(value, path, out) {
|
|
131
|
+
if (Array.isArray(value))
|
|
132
|
+
value.forEach((v) => (isObject(v) || Array.isArray(v) ? literalSecrets(v, path, out) : undefined));
|
|
133
|
+
else if (isObject(value)) {
|
|
134
|
+
for (const [key, v] of Object.entries(value)) {
|
|
135
|
+
if (isObject(v) || Array.isArray(v))
|
|
136
|
+
literalSecrets(v, [...path, key], out);
|
|
137
|
+
else if (SECRET_KEY.test(key) && !/_file$/i.test(key) && (typeof v === "string" || typeof v === "number")) {
|
|
138
|
+
const text = String(v);
|
|
139
|
+
if (text !== "" && !text.includes("${"))
|
|
140
|
+
out.push([...path, key].join("."));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* OTEL120: OTEL002's check over the config itself, so an imported config or
|
|
148
|
+
* one inside a ConfigMap gets it too. Same key pattern, same exceptions: a
|
|
149
|
+
* value containing `${` is a reference the collector expands, and a key
|
|
150
|
+
* ending in `_file` is a path. List items don't inherit their parent's key,
|
|
151
|
+
* as in OTEL002, so a list of header names is not a credential.
|
|
152
|
+
*/
|
|
153
|
+
function literalCredentialIssues(config) {
|
|
154
|
+
const issues = [];
|
|
155
|
+
for (const [section, noun] of SECTIONS) {
|
|
156
|
+
for (const [id, body] of Object.entries(config[section] ?? {})) {
|
|
157
|
+
for (const path of literalSecrets(body, [], [])) {
|
|
158
|
+
issues.push({
|
|
159
|
+
code: "OTEL120",
|
|
160
|
+
severity: "error",
|
|
161
|
+
component: id,
|
|
162
|
+
message: `${noun} "${id}" has a literal credential at ${path}; write "\${env:NAME}" (or "\${file:/path}") and let the collector read it at start-up`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return issues;
|
|
168
|
+
}
|
|
169
|
+
// ── OTEL121: credentials over plaintext ──────────────────────────────
|
|
170
|
+
/**
|
|
171
|
+
* OTEL121: a started exporter sends a credential (a header such as
|
|
172
|
+
* `authorization` or `api-key`, or a top-level key such as `api_key` or
|
|
173
|
+
* `token`) to a non-loopback endpoint without TLS. Plaintext means an
|
|
174
|
+
* `http://` endpoint, or `tls.insecure: true` on an endpoint that isn't
|
|
175
|
+
* `https://`: an HTTP exporter picks TLS from the scheme, a gRPC exporter
|
|
176
|
+
* from `tls.insecure`. A credential sent through an `auth` extension is not
|
|
177
|
+
* checked.
|
|
178
|
+
*/
|
|
179
|
+
function plaintextCredentialIssues(config) {
|
|
180
|
+
const issues = [];
|
|
181
|
+
const started = startedIds(config, "exporters");
|
|
182
|
+
for (const [id, raw] of Object.entries(config.exporters ?? {})) {
|
|
183
|
+
if (!started.has(id) || !isObject(raw))
|
|
184
|
+
continue;
|
|
185
|
+
const endpoint = remoteEndpoint(id, raw);
|
|
186
|
+
if (!endpoint)
|
|
187
|
+
continue;
|
|
188
|
+
const insecure = isObject(raw.tls) && raw.tls.insecure === true;
|
|
189
|
+
const plaintext = /^http:\/\//i.test(endpoint) || (insecure && !/^https:\/\//i.test(endpoint));
|
|
190
|
+
if (!plaintext)
|
|
191
|
+
continue;
|
|
192
|
+
const headers = isObject(raw.headers) ? Object.keys(raw.headers).filter((k) => SECRET_KEY.test(k)).map((k) => `headers.${k}`) : [];
|
|
193
|
+
const keys = Object.entries(raw)
|
|
194
|
+
.filter(([k, v]) => SECRET_KEY.test(k) && !/_file$/i.test(k) && (typeof v === "string" || typeof v === "number") && String(v) !== "")
|
|
195
|
+
.map(([k]) => k);
|
|
196
|
+
const sent = [...headers, ...keys];
|
|
197
|
+
if (sent.length === 0)
|
|
198
|
+
continue;
|
|
199
|
+
const why = /^http:\/\//i.test(endpoint) ? "an http:// endpoint" : "tls.insecure: true";
|
|
200
|
+
issues.push({
|
|
201
|
+
code: "OTEL121",
|
|
202
|
+
severity: "warning",
|
|
203
|
+
component: id,
|
|
204
|
+
message: `exporter "${id}" sends ${sent.join(", ")} to ${endpoint} over ${why}, so the credential crosses the network in plaintext. Use https:// or drop tls.insecure`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return issues;
|
|
208
|
+
}
|
|
209
|
+
// ── OTEL122: debug endpoints off loopback ────────────────────────────
|
|
210
|
+
/**
|
|
211
|
+
* The extensions OTEL122 reports off loopback. zpages serves live span
|
|
212
|
+
* samples and pipeline internals, pprof serves heap and goroutine profiles
|
|
213
|
+
* and can be made to run CPU profiles, and neither has authentication.
|
|
214
|
+
* `health_check` is left out on purpose: it serves only a status, and a
|
|
215
|
+
* kubelet probe reaches it on the pod IP, so `otlpCollector()`, `NodeAgent`
|
|
216
|
+
* and `genAiPipeline()` bind it on 0.0.0.0.
|
|
217
|
+
*/
|
|
218
|
+
const DEBUG_EXTENSIONS = new Set(["zpages", "pprof"]);
|
|
219
|
+
/** OTEL122: a started zpages or pprof extension listens on a non-loopback address. */
|
|
220
|
+
function debugEndpointIssues(config) {
|
|
221
|
+
const issues = [];
|
|
222
|
+
const enabled = new Set((config.service?.extensions ?? []).map(String));
|
|
223
|
+
for (const [id, body] of Object.entries(config.extensions ?? {})) {
|
|
224
|
+
if (!enabled.has(id) || !DEBUG_EXTENSIONS.has(typeOf(id)))
|
|
225
|
+
continue;
|
|
226
|
+
for (const listener of componentListeners("extension", id, body)) {
|
|
227
|
+
if (isLoopback(listener.host))
|
|
228
|
+
continue;
|
|
229
|
+
const host = listener.host === "" ? "every interface" : listener.host;
|
|
230
|
+
issues.push({
|
|
231
|
+
code: "OTEL122",
|
|
232
|
+
severity: "warning",
|
|
233
|
+
component: id,
|
|
234
|
+
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`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return issues;
|
|
239
|
+
}
|
|
240
|
+
// ── OTEL123: detailed debug output beside a real exporter ────────────
|
|
241
|
+
/**
|
|
242
|
+
* OTEL123: a pipeline that sends to a backend also sends to a `debug`
|
|
243
|
+
* exporter at `verbosity: detailed`, which writes every record, with all
|
|
244
|
+
* its attributes and bodies, to the collector's own log. That copies what
|
|
245
|
+
* the backend gets, sensitive values included, to wherever the collector's
|
|
246
|
+
* logs go. A pipeline with only `debug` exporters is left alone: that is a
|
|
247
|
+
* test setup.
|
|
248
|
+
*/
|
|
249
|
+
function detailedDebugIssues(config) {
|
|
250
|
+
const issues = [];
|
|
251
|
+
const exporters = config.exporters ?? {};
|
|
252
|
+
for (const [pipelineId, pipeline] of Object.entries(config.service?.pipelines ?? {})) {
|
|
253
|
+
const ids = (pipeline?.exporters ?? []).map(String).filter((id) => id in exporters);
|
|
254
|
+
const others = ids.filter((id) => typeOf(id) !== "debug");
|
|
255
|
+
if (others.length === 0)
|
|
256
|
+
continue;
|
|
257
|
+
for (const id of ids) {
|
|
258
|
+
const body = exporters[id];
|
|
259
|
+
if (typeOf(id) !== "debug" || !isObject(body) || body.verbosity !== "detailed")
|
|
260
|
+
continue;
|
|
261
|
+
issues.push({
|
|
262
|
+
code: "OTEL123",
|
|
263
|
+
severity: "warning",
|
|
264
|
+
pipeline: pipelineId,
|
|
265
|
+
component: id,
|
|
266
|
+
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`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return issues;
|
|
271
|
+
}
|
|
272
|
+
// ── OTEL124: queue or retry off for a remote exporter ────────────────
|
|
273
|
+
/**
|
|
274
|
+
* OTEL124: a started exporter sends to a remote endpoint with
|
|
275
|
+
* `sending_queue.enabled: false` or `retry_on_failure.enabled: false`.
|
|
276
|
+
* Without the queue a slow backend blocks the pipeline; without retries one
|
|
277
|
+
* failed request drops its data.
|
|
278
|
+
*/
|
|
279
|
+
function deliveryIssues(config) {
|
|
280
|
+
const issues = [];
|
|
281
|
+
const started = startedIds(config, "exporters");
|
|
282
|
+
for (const [id, raw] of Object.entries(config.exporters ?? {})) {
|
|
283
|
+
if (!started.has(id) || !isObject(raw))
|
|
284
|
+
continue;
|
|
285
|
+
const endpoint = remoteEndpoint(id, raw);
|
|
286
|
+
if (!endpoint)
|
|
287
|
+
continue;
|
|
288
|
+
if (isObject(raw.sending_queue) && raw.sending_queue.enabled === false) {
|
|
289
|
+
issues.push({
|
|
290
|
+
code: "OTEL124",
|
|
291
|
+
severity: "warning",
|
|
292
|
+
component: id,
|
|
293
|
+
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`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (isObject(raw.retry_on_failure) && raw.retry_on_failure.enabled === false) {
|
|
297
|
+
issues.push({
|
|
298
|
+
code: "OTEL124",
|
|
299
|
+
severity: "warning",
|
|
300
|
+
component: id,
|
|
301
|
+
message: `exporter "${id}" sends to ${endpoint} with retry_on_failure.enabled: false, so one failed request drops its data. Leave retries on`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return issues;
|
|
306
|
+
}
|
|
307
|
+
// ── OTEL125: no batching before a network exporter ───────────────────
|
|
308
|
+
/**
|
|
309
|
+
* The exporters OTEL125 knows send one request per incoming batch unless
|
|
310
|
+
* told otherwise: `otlp` and `otlphttp`, whose default `sending_queue` at
|
|
311
|
+
* v0.130.0 has no `batch` (`exporterhelper/internal/queue_sender.go`,
|
|
312
|
+
* `NewDefaultQueueConfig`). Other exporters may batch on their own, so they
|
|
313
|
+
* are not checked.
|
|
314
|
+
*/
|
|
315
|
+
const UNBATCHED_EXPORTERS = new Set(["otlp", "otlphttp"]);
|
|
316
|
+
/**
|
|
317
|
+
* OTEL125: a pipeline sends to a remote `otlp` or `otlphttp` exporter, has
|
|
318
|
+
* no `batch` processor, and the exporter does not batch in its own
|
|
319
|
+
* `sending_queue.batch`. Each small request then goes out on its own.
|
|
320
|
+
*/
|
|
321
|
+
function batchIssues(config) {
|
|
322
|
+
const issues = [];
|
|
323
|
+
const exporters = config.exporters ?? {};
|
|
324
|
+
for (const [pipelineId, pipeline] of Object.entries(config.service?.pipelines ?? {})) {
|
|
325
|
+
if ((pipeline?.processors ?? []).some((id) => typeOf(String(id)) === "batch"))
|
|
326
|
+
continue;
|
|
327
|
+
for (const id of (pipeline?.exporters ?? []).map(String)) {
|
|
328
|
+
const body = exporters[id];
|
|
329
|
+
if (!UNBATCHED_EXPORTERS.has(typeOf(id)) || !isObject(body))
|
|
330
|
+
continue;
|
|
331
|
+
const endpoint = remoteEndpoint(id, body);
|
|
332
|
+
if (!endpoint)
|
|
333
|
+
continue;
|
|
334
|
+
const queue = body.sending_queue;
|
|
335
|
+
if (isObject(queue) && queue.batch !== undefined && queue.batch !== null)
|
|
336
|
+
continue;
|
|
337
|
+
issues.push({
|
|
338
|
+
code: "OTEL125",
|
|
339
|
+
severity: "warning",
|
|
340
|
+
pipeline: pipelineId,
|
|
341
|
+
component: id,
|
|
342
|
+
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`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return issues;
|
|
347
|
+
}
|
|
348
|
+
// ── OTEL126, OTEL127: names checked against a fixed list ─────────────
|
|
349
|
+
/**
|
|
350
|
+
* The fields `k8sattributes` can extract at v0.130.0: the cases of
|
|
351
|
+
* `Config.Validate` (`processor/k8sattributesprocessor/config.go`), with
|
|
352
|
+
* the semconv v1.6.1 keys and `options.go` constants they name.
|
|
353
|
+
*/
|
|
354
|
+
export const K8S_ATTRIBUTES_METADATA = Object.freeze([
|
|
355
|
+
"k8s.namespace.name",
|
|
356
|
+
"k8s.pod.name",
|
|
357
|
+
"k8s.pod.uid",
|
|
358
|
+
"k8s.pod.hostname",
|
|
359
|
+
"k8s.pod.start_time",
|
|
360
|
+
"k8s.pod.ip",
|
|
361
|
+
"k8s.deployment.name",
|
|
362
|
+
"k8s.deployment.uid",
|
|
363
|
+
"k8s.replicaset.name",
|
|
364
|
+
"k8s.replicaset.uid",
|
|
365
|
+
"k8s.daemonset.name",
|
|
366
|
+
"k8s.daemonset.uid",
|
|
367
|
+
"k8s.statefulset.name",
|
|
368
|
+
"k8s.statefulset.uid",
|
|
369
|
+
"k8s.job.name",
|
|
370
|
+
"k8s.job.uid",
|
|
371
|
+
"k8s.cronjob.name",
|
|
372
|
+
"k8s.node.name",
|
|
373
|
+
"k8s.node.uid",
|
|
374
|
+
"k8s.container.name",
|
|
375
|
+
"container.id",
|
|
376
|
+
"container.image.name",
|
|
377
|
+
"container.image.tag",
|
|
378
|
+
"service.namespace",
|
|
379
|
+
"service.name",
|
|
380
|
+
"service.version",
|
|
381
|
+
"service.instance.id",
|
|
382
|
+
"container.image.repo_digests",
|
|
383
|
+
"k8s.cluster.uid",
|
|
384
|
+
]);
|
|
385
|
+
/**
|
|
386
|
+
* The detectors `resourcedetection` knows at v0.130.0: the keys of the map
|
|
387
|
+
* in `NewFactory` (`processor/resourcedetectionprocessor/factory.go`), as
|
|
388
|
+
* each detector's `TypeStr` spells them.
|
|
389
|
+
*/
|
|
390
|
+
export const RESOURCE_DETECTORS = Object.freeze([
|
|
391
|
+
"aks",
|
|
392
|
+
"azure",
|
|
393
|
+
"consul",
|
|
394
|
+
"docker",
|
|
395
|
+
"ec2",
|
|
396
|
+
"ecs",
|
|
397
|
+
"eks",
|
|
398
|
+
"elastic_beanstalk",
|
|
399
|
+
"lambda",
|
|
400
|
+
"env",
|
|
401
|
+
"gcp",
|
|
402
|
+
"heroku",
|
|
403
|
+
"system",
|
|
404
|
+
"openshift",
|
|
405
|
+
"k8snode",
|
|
406
|
+
"kubeadm",
|
|
407
|
+
"dynatrace",
|
|
408
|
+
]);
|
|
409
|
+
/** OTEL126: `k8sattributes` `extract.metadata` names a field the processor can't extract, so the collector refuses the config. */
|
|
410
|
+
function k8sAttributesIssues(config) {
|
|
411
|
+
const known = new Set(K8S_ATTRIBUTES_METADATA);
|
|
412
|
+
const issues = [];
|
|
413
|
+
for (const [id, raw] of Object.entries(config.processors ?? {})) {
|
|
414
|
+
if (typeOf(id) !== "k8sattributes" || !isObject(raw) || !isObject(raw.extract))
|
|
415
|
+
continue;
|
|
416
|
+
for (const field of Array.isArray(raw.extract.metadata) ? raw.extract.metadata : []) {
|
|
417
|
+
if (typeof field !== "string" || known.has(field))
|
|
418
|
+
continue;
|
|
419
|
+
issues.push({
|
|
420
|
+
code: "OTEL126",
|
|
421
|
+
severity: "error",
|
|
422
|
+
component: id,
|
|
423
|
+
message: `processor "${id}" extracts "${field}" (extract.metadata), which k8sattributes does not support at the pinned release; the collector refuses to start`,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return issues;
|
|
428
|
+
}
|
|
429
|
+
/** OTEL127: a started `resourcedetection` lists a detector the processor doesn't have, so it fails to build and the collector exits. */
|
|
430
|
+
function detectorIssues(config) {
|
|
431
|
+
const known = new Set(RESOURCE_DETECTORS);
|
|
432
|
+
const started = startedIds(config, "processors");
|
|
433
|
+
const issues = [];
|
|
434
|
+
for (const [id, raw] of Object.entries(config.processors ?? {})) {
|
|
435
|
+
if (typeOf(id) !== "resourcedetection" || !started.has(id) || !isObject(raw))
|
|
436
|
+
continue;
|
|
437
|
+
for (const detector of Array.isArray(raw.detectors) ? raw.detectors : []) {
|
|
438
|
+
if (typeof detector !== "string" || detector.includes("${") || known.has(detector.trim()))
|
|
439
|
+
continue;
|
|
440
|
+
const hint = detector === "elasticbeanstalk" ? ' (the detector is "elastic_beanstalk")' : "";
|
|
441
|
+
issues.push({
|
|
442
|
+
code: "OTEL127",
|
|
443
|
+
severity: "error",
|
|
444
|
+
component: id,
|
|
445
|
+
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`,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return issues;
|
|
450
|
+
}
|
|
451
|
+
/** OTEL119-OTEL127 over one collector config. */
|
|
452
|
+
export function configHygieneIssues(config) {
|
|
453
|
+
return [
|
|
454
|
+
...deprecatedFieldIssues(config),
|
|
455
|
+
...literalCredentialIssues(config),
|
|
456
|
+
...plaintextCredentialIssues(config),
|
|
457
|
+
...debugEndpointIssues(config),
|
|
458
|
+
...detailedDebugIssues(config),
|
|
459
|
+
...deliveryIssues(config),
|
|
460
|
+
...batchIssues(config),
|
|
461
|
+
...k8sAttributesIssues(config),
|
|
462
|
+
...detectorIssues(config),
|
|
463
|
+
];
|
|
464
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collector configs held in Kubernetes ConfigMaps (chant #2930).
|
|
3
|
+
*
|
|
4
|
+
* `OtelCollector`, `OtelCollectorGateway`, `GkeOtelCollector` and most
|
|
5
|
+
* hand-written manifests carry the collector config as a YAML string under a
|
|
6
|
+
* ConfigMap data key, usually `config.yaml`. The otel config checks
|
|
7
|
+
* (OTEL101-OTEL106, OTEL112) and the k8s placement checks (WK8601-WK8603)
|
|
8
|
+
* both read configs from there, through these two functions.
|
|
9
|
+
*
|
|
10
|
+
* Values are parsed with js-yaml rather than core's YAML reader, which keeps
|
|
11
|
+
* flow lists (`[otlp, batch]`) as strings (chant #2925, #3006).
|
|
12
|
+
*/
|
|
13
|
+
import { load } from "js-yaml";
|
|
14
|
+
function isRecord(v) {
|
|
15
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse a ConfigMap value as a collector config: YAML with a
|
|
19
|
+
* `service.pipelines` map. Undefined for anything else.
|
|
20
|
+
*/
|
|
21
|
+
export function parseCollectorConfig(text) {
|
|
22
|
+
if (typeof text !== "string")
|
|
23
|
+
return undefined;
|
|
24
|
+
let value;
|
|
25
|
+
try {
|
|
26
|
+
value = load(text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (!isRecord(value) || !isRecord(value.service) || !isRecord(value.service.pipelines))
|
|
32
|
+
return undefined;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The collector configs in one parsed manifest: every `data` value of a
|
|
37
|
+
* ConfigMap that parses as a collector config. Empty for any other document.
|
|
38
|
+
*/
|
|
39
|
+
export function configMapCollectorConfigs(doc) {
|
|
40
|
+
if (!isRecord(doc) || doc.kind !== "ConfigMap" || !isRecord(doc.data))
|
|
41
|
+
return [];
|
|
42
|
+
const metadata = isRecord(doc.metadata) ? doc.metadata : {};
|
|
43
|
+
if (typeof metadata.name !== "string")
|
|
44
|
+
return [];
|
|
45
|
+
const namespace = typeof metadata.namespace === "string" && metadata.namespace.length > 0 ? metadata.namespace : "default";
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const [key, text] of Object.entries(doc.data)) {
|
|
48
|
+
const config = parseCollectorConfig(text);
|
|
49
|
+
if (config)
|
|
50
|
+
out.push({ namespace, name: metadata.name, key, config });
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** How a ConfigMap config is named in a message: `ConfigMap observability/otel-agent-config, key config.yaml`. */
|
|
55
|
+
export function describeConfigMapConfig(c) {
|
|
56
|
+
return `ConfigMap ${c.namespace}/${c.name}, key ${c.key}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The collector config in one parsed manifest when it is an OpenTelemetry
|
|
60
|
+
* Operator `OpenTelemetryCollector`: `spec.config`, an object in v1beta1 and
|
|
61
|
+
* YAML text in v1alpha1. Undefined for any other document, and for a config
|
|
62
|
+
* without a `service.pipelines` map.
|
|
63
|
+
*/
|
|
64
|
+
export function operatorCollectorConfig(doc) {
|
|
65
|
+
if (!isRecord(doc) || doc.kind !== "OpenTelemetryCollector")
|
|
66
|
+
return undefined;
|
|
67
|
+
if (typeof doc.apiVersion !== "string" || !doc.apiVersion.startsWith("opentelemetry.io/"))
|
|
68
|
+
return undefined;
|
|
69
|
+
const metadata = isRecord(doc.metadata) ? doc.metadata : {};
|
|
70
|
+
if (typeof metadata.name !== "string")
|
|
71
|
+
return undefined;
|
|
72
|
+
const raw = isRecord(doc.spec) ? doc.spec.config : undefined;
|
|
73
|
+
const config = isRecord(raw)
|
|
74
|
+
? isRecord(raw.service) && isRecord(raw.service.pipelines)
|
|
75
|
+
? raw
|
|
76
|
+
: undefined
|
|
77
|
+
: parseCollectorConfig(raw);
|
|
78
|
+
if (!config)
|
|
79
|
+
return undefined;
|
|
80
|
+
const namespace = typeof metadata.namespace === "string" && metadata.namespace.length > 0 ? metadata.namespace : "default";
|
|
81
|
+
return { namespace, name: metadata.name, config };
|
|
82
|
+
}
|
|
83
|
+
/** How an operator CR's config is named in a message: `OpenTelemetryCollector observability/gateway, spec.config`. */
|
|
84
|
+
export function describeOperatorCollectorConfig(c) {
|
|
85
|
+
return `OpenTelemetryCollector ${c.namespace}/${c.name}, spec.config`;
|
|
86
|
+
}
|