@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.
Files changed (96) hide show
  1. package/dist/attribution.js +391 -0
  2. package/dist/catalog.js +42 -0
  3. package/dist/codegen/docs-cli.js +3 -0
  4. package/dist/codegen/docs.js +101 -0
  5. package/dist/codegen/generate-cli.js +7 -0
  6. package/dist/codegen/generate.js +43 -0
  7. package/dist/codegen/package.js +28 -0
  8. package/dist/collector.js +150 -0
  9. package/dist/components/common.js +50 -0
  10. package/dist/components/connectors.js +242 -0
  11. package/dist/components/exporters.js +57 -0
  12. package/dist/components/extensions.js +58 -0
  13. package/dist/components/filtering.js +106 -0
  14. package/dist/components/index.js +9 -0
  15. package/dist/components/k8s-receivers.js +36 -0
  16. package/dist/components/processors.js +103 -0
  17. package/dist/components/receivers.js +61 -0
  18. package/dist/components/sampling.js +257 -0
  19. package/dist/composites/catalog.js +368 -0
  20. package/dist/composites/genai-pipeline.js +28 -0
  21. package/dist/composites/index.js +8 -0
  22. package/dist/composites/node-agent.js +156 -0
  23. package/dist/composites/otlp-collector.js +57 -0
  24. package/dist/composites/red-metrics.js +95 -0
  25. package/dist/composites/tail-sampling-tier.js +132 -0
  26. package/dist/config-hygiene.js +464 -0
  27. package/dist/configmap.js +86 -0
  28. package/dist/define.js +204 -0
  29. package/dist/detect.js +22 -0
  30. package/dist/genai.js +569 -0
  31. package/dist/import/embedded.js +57 -0
  32. package/dist/import/generator.js +459 -0
  33. package/dist/import/parser.js +199 -0
  34. package/dist/index.js +34 -0
  35. package/dist/init-templates.js +119 -0
  36. package/dist/integrity.json +2 -2
  37. package/dist/lint/audit-catalog.js +49 -0
  38. package/dist/lint/post-synth/index.js +52 -0
  39. package/dist/lint/post-synth/otel-helpers.js +103 -0
  40. package/dist/lint/post-synth/otel101.js +13 -0
  41. package/dist/lint/post-synth/otel102.js +13 -0
  42. package/dist/lint/post-synth/otel103.js +13 -0
  43. package/dist/lint/post-synth/otel104.js +13 -0
  44. package/dist/lint/post-synth/otel105.js +13 -0
  45. package/dist/lint/post-synth/otel106.js +13 -0
  46. package/dist/lint/post-synth/otel107.js +13 -0
  47. package/dist/lint/post-synth/otel108.js +13 -0
  48. package/dist/lint/post-synth/otel109.js +13 -0
  49. package/dist/lint/post-synth/otel112.js +13 -0
  50. package/dist/lint/post-synth/otel113.js +13 -0
  51. package/dist/lint/post-synth/otel114.js +13 -0
  52. package/dist/lint/post-synth/otel115.js +13 -0
  53. package/dist/lint/post-synth/otel116.js +13 -0
  54. package/dist/lint/post-synth/otel117.js +13 -0
  55. package/dist/lint/post-synth/otel118.js +13 -0
  56. package/dist/lint/post-synth/otel119.js +13 -0
  57. package/dist/lint/post-synth/otel120.js +13 -0
  58. package/dist/lint/post-synth/otel121.js +13 -0
  59. package/dist/lint/post-synth/otel122.js +13 -0
  60. package/dist/lint/post-synth/otel123.js +13 -0
  61. package/dist/lint/post-synth/otel124.js +13 -0
  62. package/dist/lint/post-synth/otel125.js +13 -0
  63. package/dist/lint/post-synth/otel126.js +13 -0
  64. package/dist/lint/post-synth/otel127.js +13 -0
  65. package/dist/lint/rules/component-id-syntax.js +66 -0
  66. package/dist/lint/rules/index.js +6 -0
  67. package/dist/lint/rules/literal-credential.js +91 -0
  68. package/dist/lint/rules/otel-ast.js +30 -0
  69. package/dist/lsp/completions.js +11 -0
  70. package/dist/lsp/hover.js +23 -0
  71. package/dist/manifest.json +1 -1
  72. package/dist/metric-names.js +244 -0
  73. package/dist/model.js +90 -0
  74. package/dist/op/activities/collector-audit.js +213 -0
  75. package/dist/op/activities/collector-health.js +176 -0
  76. package/dist/op/activities/exec.js +16 -0
  77. package/dist/op/activities/github.js +110 -0
  78. package/dist/op/activities/index.js +16 -0
  79. package/dist/op/activities/otelcol.js +173 -0
  80. package/dist/op/activity-contracts.js +64 -0
  81. package/dist/op/audit-op.js +33 -0
  82. package/dist/op/builders.js +38 -0
  83. package/dist/package-cli.js +14 -0
  84. package/dist/pipeline.js +31 -0
  85. package/dist/platform.js +144 -0
  86. package/dist/plugin.js +126 -0
  87. package/dist/semconv.js +50 -0
  88. package/dist/serializer.js +26 -0
  89. package/dist/skill-defs.js +34 -0
  90. package/dist/topology.js +104 -0
  91. package/dist/validate-cli.js +7 -0
  92. package/dist/validate-config.js +706 -0
  93. package/dist/validate.js +85 -0
  94. package/dist/yaml.js +157 -0
  95. package/package.json +8 -8
  96. 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
+ }
@@ -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,3 @@
1
+ #!/usr/bin/env tsx
2
+ import { generateDocs } from "./docs.js";
3
+ await generateDocs({ verbose: true });
@@ -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,7 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry point for `npm run generate` in lexicon-otel.
4
+ */
5
+ import { generate, writeGeneratedFiles } from "./generate.js";
6
+ const result = await generate({ verbose: true });
7
+ writeGeneratedFiles(result);
@@ -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
+ }