@intentius/chant-lexicon-otel 0.124.30 → 0.124.32
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,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL118: a collector processor that can remove or replace a resource
|
|
3
|
+
* attribute chant's telemetry attribution stamps (ws-060, #3375).
|
|
4
|
+
*
|
|
5
|
+
* A workload built inside a workspace carries `service.name`,
|
|
6
|
+
* `service.version`, `deployment.environment.name`, `vcs.ref.head.revision`
|
|
7
|
+
* and `chant.*` keys on its resource. hud and behold join a span to a member,
|
|
8
|
+
* a release and a graph node by them. A collector between the workload and the
|
|
9
|
+
* backend can undo that, and these are the processors that can:
|
|
10
|
+
*
|
|
11
|
+
* - `resource`: a `delete`, `update`, `upsert` or `hash` action on a protected
|
|
12
|
+
* `key`, or a `delete` or `hash` whose `pattern` matches one. `insert` only
|
|
13
|
+
* adds a missing key. `extract` writes the pattern's named groups, and an RE2
|
|
14
|
+
* group name has no dot, so it can't name a protected key.
|
|
15
|
+
* - `transform`: an OTTL statement on the resource's attributes that calls
|
|
16
|
+
* `set`, `delete_key` or `delete_matching_keys` on a protected key, or
|
|
17
|
+
* `keep_keys` or `keep_matching_keys` that leaves one out. This is a text
|
|
18
|
+
* match over the statement, not a parse.
|
|
19
|
+
* - `resourcedetection` with `override` on (its default at contrib v0.130.0,
|
|
20
|
+
* `factory.go` `createDefaultConfig`) and a detector that can write a
|
|
21
|
+
* protected key: `env` (the collector's own `OTEL_RESOURCE_ATTRIBUTES`, and
|
|
22
|
+
* the default detector list), `dynatrace` (any key in its properties file),
|
|
23
|
+
* `heroku` (`service.name`, `service.version`) and `elastic_beanstalk`
|
|
24
|
+
* (`service.version`), per each detector's `metadata.yaml` at that tag.
|
|
25
|
+
* - `groupbyattrs`: a protected key in `keys`. A record attribute with that
|
|
26
|
+
* key is copied over the resource's (`attribute_groups.go`
|
|
27
|
+
* `buildReferenceResource`).
|
|
28
|
+
* - `redaction`: it runs over resource attributes too. With `allow_all_keys`
|
|
29
|
+
* off, its default, every key not in `allowed_keys` or `ignored_keys` is
|
|
30
|
+
* deleted; a `blocked_key_patterns` entry matching a protected key that isn't
|
|
31
|
+
* ignored masks its value. `blocked_values` matches values, which a config
|
|
32
|
+
* doesn't show, so it is not checked.
|
|
33
|
+
* - `sumologic`: `translate_attributes`, on by default, renames `service.name`
|
|
34
|
+
* to `service` on log and metric resources; `nest_attributes` (off by
|
|
35
|
+
* default) turns dotted keys into maps; an `aggregate_attributes` prefix
|
|
36
|
+
* moves the keys it matches.
|
|
37
|
+
* - `schema`: a target in the `https://opentelemetry.io/schemas/` family below
|
|
38
|
+
* 1.29.0 renames `vcs.ref.head.revision` back to
|
|
39
|
+
* `vcs.repository.ref.revision`, and below 1.27.0 renames
|
|
40
|
+
* `deployment.environment.name` back to `deployment.environment`, for data
|
|
41
|
+
* with a newer schema URL.
|
|
42
|
+
* - `metricstransform`: a `group` transform whose `group_resource_labels`
|
|
43
|
+
* sets a protected key.
|
|
44
|
+
* - `logstransform`: an operator that adds, removes, moves or copies onto a
|
|
45
|
+
* `resource["<key>"]` field for a protected key, a `retain` that lists
|
|
46
|
+
* resource fields and leaves one out, or a parser with `parse_to: resource`.
|
|
47
|
+
*
|
|
48
|
+
* `k8sattributes` can extract `service.name` and `service.version`, but it
|
|
49
|
+
* writes pod metadata only where the key is absent or empty
|
|
50
|
+
* (`processor.go` `setResourceAttribute`), so it never replaces the
|
|
51
|
+
* workload's value. The `attributes` processor acts on span, log and metric
|
|
52
|
+
* attributes, not the resource's. Neither is checked. Only processors some
|
|
53
|
+
* pipeline lists are.
|
|
54
|
+
*/
|
|
55
|
+
import { TELEMETRY_ATTRIBUTES } from "@intentius/chant/telemetry-attribution";
|
|
56
|
+
import { canonicalTypeOf } from "./model.js";
|
|
57
|
+
/** The keys the attribution stamps. Any other `chant.*` key is protected too. */
|
|
58
|
+
export const PROTECTED_RESOURCE_ATTRIBUTES = TELEMETRY_ATTRIBUTES.map((a) => a.key);
|
|
59
|
+
/** True for a key OTEL118 protects: one of `PROTECTED_RESOURCE_ATTRIBUTES`, or any `chant.*` key. */
|
|
60
|
+
export function isProtectedResourceAttribute(key) {
|
|
61
|
+
return PROTECTED_RESOURCE_ATTRIBUTES.includes(key) || key.startsWith("chant.");
|
|
62
|
+
}
|
|
63
|
+
/** The protected keys a regular expression matches. An RE2 pattern JavaScript can't compile matches none. */
|
|
64
|
+
function protectedMatching(pattern) {
|
|
65
|
+
let re;
|
|
66
|
+
try {
|
|
67
|
+
re = new RegExp(pattern);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => re.test(k));
|
|
73
|
+
}
|
|
74
|
+
function quoteAll(keys) {
|
|
75
|
+
return keys.map((k) => `"${k}"`).join(", ");
|
|
76
|
+
}
|
|
77
|
+
const REPLACING_ACTIONS = {
|
|
78
|
+
delete: "removes",
|
|
79
|
+
update: "replaces",
|
|
80
|
+
upsert: "replaces",
|
|
81
|
+
hash: "replaces with its hash",
|
|
82
|
+
};
|
|
83
|
+
function resourceActionFindings(body) {
|
|
84
|
+
const out = [];
|
|
85
|
+
const actions = Array.isArray(body.attributes) ? body.attributes : [];
|
|
86
|
+
for (const raw of actions) {
|
|
87
|
+
if (typeof raw !== "object" || raw === null)
|
|
88
|
+
continue;
|
|
89
|
+
const { key, action, pattern } = raw;
|
|
90
|
+
const verb = typeof action === "string" ? REPLACING_ACTIONS[action] : undefined;
|
|
91
|
+
if (!verb)
|
|
92
|
+
continue;
|
|
93
|
+
if (typeof key === "string" && isProtectedResourceAttribute(key)) {
|
|
94
|
+
out.push(`its ${action} action ${verb} "${key}"`);
|
|
95
|
+
}
|
|
96
|
+
else if (typeof pattern === "string" && (action === "delete" || action === "hash")) {
|
|
97
|
+
const hit = protectedMatching(pattern);
|
|
98
|
+
if (hit.length > 0)
|
|
99
|
+
out.push(`its ${action} action's pattern "${pattern}" matches ${quoteAll(hit)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
// ── transform: a text match over OTTL ────────────────────────────────
|
|
105
|
+
const OTTL_CALL = /\b(set|delete_key|delete_matching_keys|keep_keys|keep_matching_keys)\s*\(/g;
|
|
106
|
+
/** The top-level arguments of the call whose `(` is at `open`, as written. */
|
|
107
|
+
function callArguments(text, open) {
|
|
108
|
+
const args = [];
|
|
109
|
+
let depth = 0;
|
|
110
|
+
let start = open + 1;
|
|
111
|
+
let quoted = false;
|
|
112
|
+
for (let i = open; i < text.length; i++) {
|
|
113
|
+
const c = text[i];
|
|
114
|
+
if (quoted) {
|
|
115
|
+
if (c === "\\")
|
|
116
|
+
i++;
|
|
117
|
+
else if (c === '"')
|
|
118
|
+
quoted = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (c === '"')
|
|
122
|
+
quoted = true;
|
|
123
|
+
else if (c === "(" || c === "[" || c === "{")
|
|
124
|
+
depth++;
|
|
125
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
126
|
+
depth--;
|
|
127
|
+
if (depth === 0) {
|
|
128
|
+
args.push(text.slice(start, i).trim());
|
|
129
|
+
return args;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (c === "," && depth === 1) {
|
|
133
|
+
args.push(text.slice(start, i).trim());
|
|
134
|
+
start = i + 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return args;
|
|
138
|
+
}
|
|
139
|
+
/** The OTTL string literals in `text`, unescaped. */
|
|
140
|
+
function stringLiterals(text) {
|
|
141
|
+
return [...text.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1].replace(/\\(["\\])/g, "$1"));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Whether `path` is the resource's attribute map, and the key it indexes.
|
|
145
|
+
* `resource.attributes` always is; a bare `attributes` is only in a statement
|
|
146
|
+
* group whose context is `resource`.
|
|
147
|
+
*/
|
|
148
|
+
function resourcePath(path, resourceContext) {
|
|
149
|
+
const m = /^(resource\.)?attributes\s*(?:\[\s*"((?:[^"\\]|\\.)*)"\s*\])?$/.exec(path);
|
|
150
|
+
if (!m || (!m[1] && !resourceContext))
|
|
151
|
+
return undefined;
|
|
152
|
+
return m[2] === undefined ? {} : { key: m[2].replace(/\\(["\\])/g, "$1") };
|
|
153
|
+
}
|
|
154
|
+
function statementFindings(statement, resourceContext) {
|
|
155
|
+
const out = [];
|
|
156
|
+
for (const call of statement.matchAll(OTTL_CALL)) {
|
|
157
|
+
const fn = call[1];
|
|
158
|
+
const args = callArguments(statement, call.index + call[0].length - 1);
|
|
159
|
+
const target = resourcePath(args[0] ?? "", resourceContext);
|
|
160
|
+
if (!target)
|
|
161
|
+
continue;
|
|
162
|
+
const [literal] = stringLiterals(args[1] ?? "");
|
|
163
|
+
if (fn === "set") {
|
|
164
|
+
if (target.key === undefined)
|
|
165
|
+
out.push(`set replaces the whole resource attribute map`);
|
|
166
|
+
else if (isProtectedResourceAttribute(target.key))
|
|
167
|
+
out.push(`set replaces "${target.key}"`);
|
|
168
|
+
}
|
|
169
|
+
else if (target.key !== undefined) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
else if (fn === "delete_key" && literal !== undefined && isProtectedResourceAttribute(literal)) {
|
|
173
|
+
out.push(`delete_key removes "${literal}"`);
|
|
174
|
+
}
|
|
175
|
+
else if (fn === "delete_matching_keys" && literal !== undefined) {
|
|
176
|
+
const hit = protectedMatching(literal);
|
|
177
|
+
if (hit.length > 0)
|
|
178
|
+
out.push(`delete_matching_keys "${literal}" removes ${quoteAll(hit)}`);
|
|
179
|
+
}
|
|
180
|
+
else if (fn === "keep_keys") {
|
|
181
|
+
const kept = new Set(stringLiterals(args[1] ?? ""));
|
|
182
|
+
const dropped = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !kept.has(k));
|
|
183
|
+
if (dropped.length > 0)
|
|
184
|
+
out.push(`keep_keys drops ${quoteAll(dropped)}`);
|
|
185
|
+
}
|
|
186
|
+
else if (fn === "keep_matching_keys" && literal !== undefined) {
|
|
187
|
+
const hit = new Set(protectedMatching(literal));
|
|
188
|
+
const dropped = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !hit.has(k));
|
|
189
|
+
if (dropped.length > 0)
|
|
190
|
+
out.push(`keep_matching_keys "${literal}" drops ${quoteAll(dropped)}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
function transformFindings(body) {
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const [field, groups] of Object.entries(body)) {
|
|
198
|
+
if (!field.endsWith("_statements") || !Array.isArray(groups))
|
|
199
|
+
continue;
|
|
200
|
+
for (const group of groups) {
|
|
201
|
+
// A plain string infers its context from its paths, so only `resource.` paths reach the resource.
|
|
202
|
+
const [statements, resourceContext] = typeof group === "string"
|
|
203
|
+
? [[group], false]
|
|
204
|
+
: typeof group === "object" && group !== null
|
|
205
|
+
? [group.statements, group.context === "resource"]
|
|
206
|
+
: [[], false];
|
|
207
|
+
if (!Array.isArray(statements))
|
|
208
|
+
continue;
|
|
209
|
+
for (const statement of statements) {
|
|
210
|
+
if (typeof statement !== "string")
|
|
211
|
+
continue;
|
|
212
|
+
for (const finding of statementFindings(statement, resourceContext)) {
|
|
213
|
+
out.push(`the OTTL text \`${statement}\` matches a call where ${finding}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
// ── resourcedetection ────────────────────────────────────────────────
|
|
221
|
+
/** Detectors that can write a protected key, and which. */
|
|
222
|
+
const OVERRIDING_DETECTORS = {
|
|
223
|
+
env: "any key in the collector's own OTEL_RESOURCE_ATTRIBUTES",
|
|
224
|
+
dynatrace: "any key in the Dynatrace properties file",
|
|
225
|
+
heroku: '"service.name" and "service.version"',
|
|
226
|
+
elastic_beanstalk: '"service.version"',
|
|
227
|
+
};
|
|
228
|
+
function detectionFindings(body) {
|
|
229
|
+
// `override` defaults to true, and `detectors` to [env], at contrib v0.130.0.
|
|
230
|
+
if (body.override === false)
|
|
231
|
+
return [];
|
|
232
|
+
const detectors = Array.isArray(body.detectors) ? body.detectors : ["env"];
|
|
233
|
+
const how = body.override === true ? "override: true" : "override on by default";
|
|
234
|
+
return detectors
|
|
235
|
+
.filter((d) => typeof d === "string" && d in OVERRIDING_DETECTORS)
|
|
236
|
+
.map((d) => `with ${how}, the ${d} detector writes ${OVERRIDING_DETECTORS[d]} over the service's own value; set override: false`);
|
|
237
|
+
}
|
|
238
|
+
// ── processors that rewrite the resource as a side effect ─────────────
|
|
239
|
+
function strings(value) {
|
|
240
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
241
|
+
}
|
|
242
|
+
function groupByAttrsFindings(body) {
|
|
243
|
+
const keys = strings(body.keys).filter(isProtectedResourceAttribute);
|
|
244
|
+
return keys.length > 0 ? [`it groups by ${quoteAll(keys)}, so a record attribute with that key is copied over the resource's value`] : [];
|
|
245
|
+
}
|
|
246
|
+
function redactionFindings(body) {
|
|
247
|
+
const out = [];
|
|
248
|
+
const ignored = new Set(strings(body.ignored_keys));
|
|
249
|
+
const allowed = new Set(strings(body.allowed_keys));
|
|
250
|
+
const exposed = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !ignored.has(k));
|
|
251
|
+
if (body.allow_all_keys !== true) {
|
|
252
|
+
const deleted = exposed.filter((k) => !allowed.has(k));
|
|
253
|
+
if (deleted.length > 0) {
|
|
254
|
+
const how = body.allow_all_keys === false ? "allow_all_keys: false" : "allow_all_keys off by default";
|
|
255
|
+
out.push(`with ${how}, it deletes resource keys missing from allowed_keys and ignored_keys: ${quoteAll(deleted)}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const pattern of strings(body.blocked_key_patterns)) {
|
|
259
|
+
const masked = exposed.filter((k) => protectedMatching(pattern).includes(k));
|
|
260
|
+
if (masked.length > 0)
|
|
261
|
+
out.push(`its blocked_key_patterns entry "${pattern}" masks the value of ${quoteAll(masked)}`);
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
function sumologicFindings(body) {
|
|
266
|
+
const out = [];
|
|
267
|
+
// `translate_attributes` defaults to true at contrib v0.130.0.
|
|
268
|
+
if (body.translate_attributes !== false) {
|
|
269
|
+
out.push(`translate_attributes${body.translate_attributes === true ? "" : " (on by default)"} renames "service.name" to "service" on log and metric resources`);
|
|
270
|
+
}
|
|
271
|
+
const nest = body.nest_attributes;
|
|
272
|
+
if (typeof nest === "object" && nest !== null && nest.enabled === true) {
|
|
273
|
+
const include = strings(nest.include);
|
|
274
|
+
const exclude = strings(nest.exclude);
|
|
275
|
+
const nested = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => (include.length === 0 || include.some((p) => k.startsWith(p))) && !exclude.some((p) => k.startsWith(p)));
|
|
276
|
+
if (nested.length > 0)
|
|
277
|
+
out.push(`nest_attributes turns ${quoteAll(nested)} into nested maps`);
|
|
278
|
+
}
|
|
279
|
+
for (const pair of Array.isArray(body.aggregate_attributes) ? body.aggregate_attributes : []) {
|
|
280
|
+
const prefixes = strings(pair?.prefixes);
|
|
281
|
+
const moved = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => prefixes.some((p) => k.startsWith(p)));
|
|
282
|
+
if (moved.length > 0)
|
|
283
|
+
out.push(`aggregate_attributes prefixes ${quoteAll(prefixes)} move ${quoteAll(moved)} into one attribute`);
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
// Schema versions whose `all` changes rename a protected key, in semantic-conventions' schemas/1.37.0.
|
|
288
|
+
const SCHEMA_RENAMES = [
|
|
289
|
+
{ version: [1, 29, 0], from: "vcs.repository.ref.revision", to: "vcs.ref.head.revision" },
|
|
290
|
+
{ version: [1, 27, 0], from: "deployment.environment", to: "deployment.environment.name" },
|
|
291
|
+
];
|
|
292
|
+
function schemaFindings(body) {
|
|
293
|
+
const out = [];
|
|
294
|
+
for (const target of strings(body.targets)) {
|
|
295
|
+
const m = /^https?:\/\/opentelemetry\.io\/schemas\/(\d+)\.(\d+)\.(\d+)$/.exec(target);
|
|
296
|
+
if (!m)
|
|
297
|
+
continue;
|
|
298
|
+
const v = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
299
|
+
const older = (r) => v[0] < r[0] || (v[0] === r[0] && (v[1] < r[1] || (v[1] === r[1] && v[2] < r[2])));
|
|
300
|
+
for (const rename of SCHEMA_RENAMES) {
|
|
301
|
+
if (older(rename.version))
|
|
302
|
+
out.push(`its target ${target} renames "${rename.to}" back to "${rename.from}" on data with a newer schema URL`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
function metricsTransformFindings(body) {
|
|
308
|
+
const out = [];
|
|
309
|
+
for (const t of Array.isArray(body.transforms) ? body.transforms : []) {
|
|
310
|
+
if (typeof t !== "object" || t === null || t.action !== "group")
|
|
311
|
+
continue;
|
|
312
|
+
const labels = t.group_resource_labels;
|
|
313
|
+
const keys = typeof labels === "object" && labels !== null ? Object.keys(labels).filter(isProtectedResourceAttribute) : [];
|
|
314
|
+
if (keys.length > 0)
|
|
315
|
+
out.push(`a group transform's group_resource_labels sets ${quoteAll(keys)} on the new resource`);
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
/** The key of a stanza `resource["<key>"]` field, or undefined. */
|
|
320
|
+
function resourceField(field) {
|
|
321
|
+
if (typeof field !== "string")
|
|
322
|
+
return undefined;
|
|
323
|
+
const m = /^resource\[\s*["']([^"']*)["']\s*\]$/.exec(field.trim());
|
|
324
|
+
return m?.[1];
|
|
325
|
+
}
|
|
326
|
+
function logsTransformFindings(body) {
|
|
327
|
+
const out = [];
|
|
328
|
+
for (const raw of Array.isArray(body.operators) ? body.operators : []) {
|
|
329
|
+
if (typeof raw !== "object" || raw === null)
|
|
330
|
+
continue;
|
|
331
|
+
const op = raw;
|
|
332
|
+
const type = String(op.type ?? "");
|
|
333
|
+
const touched = [
|
|
334
|
+
...(type === "add" || type === "remove" ? [resourceField(op.field)] : []),
|
|
335
|
+
...(type === "move" ? [resourceField(op.from), resourceField(op.to)] : []),
|
|
336
|
+
...(type === "copy" ? [resourceField(op.to)] : []),
|
|
337
|
+
].filter((k) => k !== undefined && isProtectedResourceAttribute(k));
|
|
338
|
+
if (touched.length > 0)
|
|
339
|
+
out.push(`its ${type} operator writes or removes ${quoteAll(touched)}`);
|
|
340
|
+
if (type === "retain") {
|
|
341
|
+
const kept = strings(op.fields).map(resourceField).filter((k) => k !== undefined);
|
|
342
|
+
const dropped = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !kept.includes(k));
|
|
343
|
+
if (kept.length > 0 && dropped.length > 0)
|
|
344
|
+
out.push(`its retain operator keeps only the resource fields it lists, dropping ${quoteAll(dropped)}`);
|
|
345
|
+
}
|
|
346
|
+
if (op.parse_to === "resource")
|
|
347
|
+
out.push(`its ${type} operator parses into the resource, so a parsed key can replace any protected key`);
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
const FINDINGS = {
|
|
352
|
+
resource: resourceActionFindings,
|
|
353
|
+
transform: transformFindings,
|
|
354
|
+
resourcedetection: detectionFindings,
|
|
355
|
+
groupbyattrs: groupByAttrsFindings,
|
|
356
|
+
redaction: redactionFindings,
|
|
357
|
+
sumologic: sumologicFindings,
|
|
358
|
+
schema: schemaFindings,
|
|
359
|
+
metricstransform: metricsTransformFindings,
|
|
360
|
+
logstransform: logsTransformFindings,
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* OTEL118 over one collector config: each processor a pipeline lists that can
|
|
364
|
+
* remove or replace a protected resource attribute. Not part of
|
|
365
|
+
* `validateCollectorConfig`, because it applies only to a build that stamps
|
|
366
|
+
* the attribution: the post-synth check runs it when the context carries
|
|
367
|
+
* `telemetry`.
|
|
368
|
+
*/
|
|
369
|
+
export function attributionIssues(config) {
|
|
370
|
+
const pipelinesOf = new Map();
|
|
371
|
+
for (const [pipeline, body] of Object.entries(config.service?.pipelines ?? {})) {
|
|
372
|
+
for (const id of body?.processors ?? [])
|
|
373
|
+
pipelinesOf.set(String(id), [...(pipelinesOf.get(String(id)) ?? []), pipeline]);
|
|
374
|
+
}
|
|
375
|
+
const issues = [];
|
|
376
|
+
for (const [id, pipelines] of pipelinesOf) {
|
|
377
|
+
const raw = config.processors?.[id];
|
|
378
|
+
const body = typeof raw === "object" && raw !== null ? raw : {};
|
|
379
|
+
const type = canonicalTypeOf("processor", id);
|
|
380
|
+
const findings = Object.prototype.hasOwnProperty.call(FINDINGS, type) ? FINDINGS[type](body) : [];
|
|
381
|
+
for (const finding of findings) {
|
|
382
|
+
issues.push({
|
|
383
|
+
code: "OTEL118",
|
|
384
|
+
severity: "warning",
|
|
385
|
+
component: id,
|
|
386
|
+
message: `processor "${id}" (pipelines ${pipelines.join(", ")}): ${finding}. Chant's telemetry attribution stamps those resource attributes, and a span that loses them can't be joined to its member, release or declaration`,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return issues;
|
|
391
|
+
}
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in entity catalog: every class this package exports, keyed by
|
|
3
|
+
* class name. It feeds the packaged registry (`dist/meta.json`), LSP
|
|
4
|
+
* completions and hover, and the docs.
|
|
5
|
+
*/
|
|
6
|
+
import { componentEntityType } from "./define.js";
|
|
7
|
+
import * as components from "./components/index.js";
|
|
8
|
+
import { PIPELINE_TYPE, SERVICE_TYPE } from "./pipeline.js";
|
|
9
|
+
function isComponentClass(v) {
|
|
10
|
+
return typeof v === "function" && typeof v.definition === "object";
|
|
11
|
+
}
|
|
12
|
+
export const BUILTIN_CATALOG = [
|
|
13
|
+
...Object.entries(components)
|
|
14
|
+
.filter((e) => isComponentClass(e[1]))
|
|
15
|
+
.map(([className, cls]) => ({
|
|
16
|
+
className,
|
|
17
|
+
entityType: componentEntityType(cls.definition.kind, cls.definition.type),
|
|
18
|
+
kind: cls.definition.kind,
|
|
19
|
+
type: cls.definition.type,
|
|
20
|
+
description: cls.definition.description ?? "",
|
|
21
|
+
})),
|
|
22
|
+
{
|
|
23
|
+
className: "Pipeline",
|
|
24
|
+
entityType: PIPELINE_TYPE,
|
|
25
|
+
kind: "pipeline",
|
|
26
|
+
description: "One pipeline under service.pipelines: receivers, then processors, then exporters, for one signal",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
className: "Service",
|
|
30
|
+
entityType: SERVICE_TYPE,
|
|
31
|
+
kind: "service",
|
|
32
|
+
description: "service.extensions and service.telemetry",
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
/** The catalog as a chant lexicon registry, keyed by class name. */
|
|
36
|
+
export function lexiconRegistry() {
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const e of [...BUILTIN_CATALOG].sort((a, b) => a.className.localeCompare(b.className))) {
|
|
39
|
+
out[e.className] = { resourceType: e.entityType, kind: "resource", lexicon: "otel" };
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation generation for the otel lexicon: the generated reference
|
|
3
|
+
* pages from the core docs pipeline, plus the authored pages in docs/pages/.
|
|
4
|
+
*/
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
|
|
8
|
+
function serviceFromType(resourceType) {
|
|
9
|
+
const parts = resourceType.split("::");
|
|
10
|
+
return parts.length >= 2 ? parts[1] : "OTel";
|
|
11
|
+
}
|
|
12
|
+
const overview = `The otel lexicon types [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/)
|
|
13
|
+
config: receivers, processors, exporters, connectors, extensions and the
|
|
14
|
+
pipelines that connect them. \`chant build\` emits one collector config file, the YAML
|
|
15
|
+
\`otelcol --config\` reads as it is.
|
|
16
|
+
|
|
17
|
+
\`\`\`ts
|
|
18
|
+
import { OtlpReceiver, BatchProcessor, OtlpExporter, Pipeline } from "@intentius/chant-lexicon-otel";
|
|
19
|
+
|
|
20
|
+
export const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
|
|
21
|
+
export const batch = new BatchProcessor({ timeout: "5s" });
|
|
22
|
+
export const backend = new OtlpExporter({ name: "backend", endpoint: "tempo.observability:4317" });
|
|
23
|
+
|
|
24
|
+
export const traces = new Pipeline({
|
|
25
|
+
signal: "traces",
|
|
26
|
+
receivers: [otlp],
|
|
27
|
+
processors: [batch],
|
|
28
|
+
exporters: [backend],
|
|
29
|
+
});
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
The built-in component set is otlp, prometheus, hostmetrics, filelog,
|
|
33
|
+
k8s_cluster and kubeletstats receivers; batch, memory_limiter, resource,
|
|
34
|
+
attributes, k8sattributes, resourcedetection, filter, transform, redaction,
|
|
35
|
+
tail_sampling, probabilistic_sampler and deltatocumulative processors; otlp, otlphttp, debug,
|
|
36
|
+
prometheus, googlecloud and loadbalancing exporters; spanmetrics,
|
|
37
|
+
servicegraph, routing, forward, count, sum and signaltometrics connectors; and the
|
|
38
|
+
health_check, pprof, zpages and k8s_leader_elector extensions. \`genAiPipeline()\` is a preset for
|
|
39
|
+
GenAI workloads: it removes prompt and completion content and derives agent
|
|
40
|
+
RED and token metrics.
|
|
41
|
+
Their config types follow the collector-contrib release named on the
|
|
42
|
+
Custom components page. A component chant doesn't ship is added with
|
|
43
|
+
\`defineComponent\`, and it is serialized and checked the same way as the
|
|
44
|
+
built-ins. \`chant import\` turns an existing collector config file into this
|
|
45
|
+
TypeScript (see Importing a Collector Config).
|
|
46
|
+
|
|
47
|
+
Checks catch a pipeline that uses a component nobody declared (OTEL101), a
|
|
48
|
+
pipeline with no receivers or exporters (OTEL102), a declared component no
|
|
49
|
+
pipeline uses (OTEL103), a connector joining pipelines of signals it can't
|
|
50
|
+
convert (OTEL112), pipelines feeding each other in a cycle (OTEL113), a
|
|
51
|
+
connector id shared with a receiver or exporter (OTEL114), a \`routing\` route
|
|
52
|
+
to a pipeline that doesn't receive from it (OTEL115), a connector
|
|
53
|
+
splitting metrics by a per-request GenAI attribute such as
|
|
54
|
+
\`gen_ai.conversation.id\` (OTEL116), two started components listening
|
|
55
|
+
on the same address (OTEL117), and a literal credential in source (OTEL002)
|
|
56
|
+
or in the config (OTEL120). Others report deprecated fields (OTEL119),
|
|
57
|
+
credentials sent in plaintext (OTEL121), zpages or pprof off loopback
|
|
58
|
+
(OTEL122), detailed debug output beside a backend (OTEL123), delivery and
|
|
59
|
+
batching settings (OTEL124, OTEL125), and k8sattributes fields or
|
|
60
|
+
resourcedetection detectors the collector doesn't know (OTEL126, OTEL127). In
|
|
61
|
+
a build that stamps telemetry attribution, OTEL118 reports a processor that can
|
|
62
|
+
drop or overwrite one of the attribution's resource attributes.
|
|
63
|
+
`;
|
|
64
|
+
const outputFormat = `The otel lexicon serializes every otel entity in a build into **one
|
|
65
|
+
collector config file** in YAML, the file \`otelcol --config\` reads.
|
|
66
|
+
|
|
67
|
+
- Sections come out as receivers, processors, exporters, connectors, extensions, service,
|
|
68
|
+
separated by a blank line. Empty sections are left out.
|
|
69
|
+
- A component's id is its collector type, or \`type/name\` when its \`name\` is
|
|
70
|
+
set. The export name in source never appears in the output.
|
|
71
|
+
- A list of scalars is written in flow style (\`receivers: [otlp]\`), any
|
|
72
|
+
other list in block style. Strings are plain where YAML reads them back
|
|
73
|
+
unchanged and double-quoted otherwise.
|
|
74
|
+
- Declared extensions are enabled in declaration order unless a \`Service\`
|
|
75
|
+
lists \`extensions\`.
|
|
76
|
+
- Each custom component adds a \`# chant:\` comment line above the config
|
|
77
|
+
naming its schema pin. Built-ins add none.
|
|
78
|
+
- A collector config has no metadata channel, so no ownership marker is
|
|
79
|
+
stamped.
|
|
80
|
+
`;
|
|
81
|
+
export async function generateDocs(opts) {
|
|
82
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
83
|
+
const config = {
|
|
84
|
+
name: "otel",
|
|
85
|
+
displayName: "OpenTelemetry Collector",
|
|
86
|
+
description: "Typed OpenTelemetry Collector config: receivers, processors, exporters and pipelines",
|
|
87
|
+
distDir: join(pkgDir, "dist"),
|
|
88
|
+
outDir: join(pkgDir, "docs"),
|
|
89
|
+
basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/otel/",
|
|
90
|
+
overview,
|
|
91
|
+
outputFormat,
|
|
92
|
+
serviceFromType,
|
|
93
|
+
srcDir: join(pkgDir, "src"),
|
|
94
|
+
examplesDir: join(pkgDir, "examples"),
|
|
95
|
+
};
|
|
96
|
+
const result = docsPipeline(config);
|
|
97
|
+
writeDocsSite(config, result);
|
|
98
|
+
if (opts?.verbose) {
|
|
99
|
+
console.error(`Generated ${result.pages.size} documentation pages`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The otel lexicon's "generation" step.
|
|
3
|
+
*
|
|
4
|
+
* There is no upstream schema to fetch: the collector defines each
|
|
5
|
+
* component's config as a Go struct, and publishes no machine-readable schema
|
|
6
|
+
* for the set as a whole. The built-in types are written by hand against the
|
|
7
|
+
* collector release named in `COLLECTOR_PIN` (src/define.ts), and this step
|
|
8
|
+
* derives the registry from them, so the packaged `dist/meta.json` can never
|
|
9
|
+
* disagree with the classes the package exports.
|
|
10
|
+
*/
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
14
|
+
import { BUILTIN_CATALOG, lexiconRegistry } from "../catalog.js";
|
|
15
|
+
/** The packaged types entry re-exports the compiled declarations, which carry every config type. */
|
|
16
|
+
const TYPES_DTS = `// The otel lexicon's entity classes and config types are hand-written in
|
|
17
|
+
// src/ and compiled to dist/index.d.ts; this entry re-exports them.
|
|
18
|
+
export * from "../index.js";
|
|
19
|
+
`;
|
|
20
|
+
export async function generate(opts) {
|
|
21
|
+
const registry = lexiconRegistry();
|
|
22
|
+
const lexiconJSON = `${JSON.stringify(registry, null, 2)}\n`;
|
|
23
|
+
const components = BUILTIN_CATALOG.filter((c) => c.type !== undefined).length;
|
|
24
|
+
if (opts?.verbose) {
|
|
25
|
+
console.error(`otel: ${components} built-in components, plus Pipeline and Service (${BUILTIN_CATALOG.length} entities)`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
lexiconJSON,
|
|
29
|
+
typesDTS: TYPES_DTS,
|
|
30
|
+
indexTS: "",
|
|
31
|
+
resources: BUILTIN_CATALOG.length,
|
|
32
|
+
properties: 0,
|
|
33
|
+
enums: 0,
|
|
34
|
+
warnings: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Write the registry to src/generated/lexicon-otel.json, for tools that read it from there. */
|
|
38
|
+
export function writeGeneratedFiles(result) {
|
|
39
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
40
|
+
const dir = join(pkgDir, "src", "generated");
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
writeFileSync(join(dir, "lexicon-otel.json"), result.lexiconJSON);
|
|
43
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* otel lexicon packaging, delegating to core's packagePipeline.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { join, dirname } from "path";
|
|
6
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
7
|
+
import { packagePipeline, collectSkills, } from "@intentius/chant/codegen/package";
|
|
8
|
+
import { generate } from "./generate.js";
|
|
9
|
+
import { otelSkills } from "../skill-defs.js";
|
|
10
|
+
// src/codegen/package.ts -> src/
|
|
11
|
+
const srcDir = dirname(moduleSourceDir(import.meta.url));
|
|
12
|
+
export async function packageLexicon(opts = {}) {
|
|
13
|
+
const pkgJson = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8"));
|
|
14
|
+
return packagePipeline({
|
|
15
|
+
generate: (genOpts) => generate({ verbose: genOpts.verbose, force: genOpts.force }),
|
|
16
|
+
buildManifest: () => ({
|
|
17
|
+
name: "otel",
|
|
18
|
+
version: pkgJson.version ?? "0.0.1",
|
|
19
|
+
chantVersion: ">=0.1.0",
|
|
20
|
+
namespace: "OTel",
|
|
21
|
+
intrinsics: [],
|
|
22
|
+
pseudoParameters: {},
|
|
23
|
+
}),
|
|
24
|
+
srcDir,
|
|
25
|
+
collectSkills: () => collectSkills(otelSkills()),
|
|
26
|
+
version: pkgJson.version ?? "0.0.1",
|
|
27
|
+
}, opts);
|
|
28
|
+
}
|