@intentius/chant-lexicon-otel 0.102.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +1 -1
  2. package/dist/attribution.d.ts +69 -0
  3. package/dist/attribution.d.ts.map +1 -0
  4. package/dist/codegen/docs.d.ts.map +1 -1
  5. package/dist/components/processors.d.ts +1 -1
  6. package/dist/components/processors.d.ts.map +1 -1
  7. package/dist/components/sampling.d.ts +3 -0
  8. package/dist/components/sampling.d.ts.map +1 -1
  9. package/dist/config-hygiene.d.ts +33 -0
  10. package/dist/config-hygiene.d.ts.map +1 -0
  11. package/dist/define.d.ts +18 -2
  12. package/dist/define.d.ts.map +1 -1
  13. package/dist/import/generator.d.ts +5 -1
  14. package/dist/import/generator.d.ts.map +1 -1
  15. package/dist/import/parser.d.ts +1 -1
  16. package/dist/import/parser.d.ts.map +1 -1
  17. package/dist/index.d.ts +4 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/integrity.json +15 -5
  20. package/dist/lint/audit-catalog.d.ts +3 -2
  21. package/dist/lint/audit-catalog.d.ts.map +1 -1
  22. package/dist/lint/post-synth/index.d.ts.map +1 -1
  23. package/dist/lint/post-synth/otel-helpers.d.ts +2 -2
  24. package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
  25. package/dist/lint/post-synth/otel118.d.ts +8 -0
  26. package/dist/lint/post-synth/otel118.d.ts.map +1 -0
  27. package/dist/lint/post-synth/otel119.d.ts +8 -0
  28. package/dist/lint/post-synth/otel119.d.ts.map +1 -0
  29. package/dist/lint/post-synth/otel120.d.ts +8 -0
  30. package/dist/lint/post-synth/otel120.d.ts.map +1 -0
  31. package/dist/lint/post-synth/otel121.d.ts +8 -0
  32. package/dist/lint/post-synth/otel121.d.ts.map +1 -0
  33. package/dist/lint/post-synth/otel122.d.ts +8 -0
  34. package/dist/lint/post-synth/otel122.d.ts.map +1 -0
  35. package/dist/lint/post-synth/otel123.d.ts +8 -0
  36. package/dist/lint/post-synth/otel123.d.ts.map +1 -0
  37. package/dist/lint/post-synth/otel124.d.ts +8 -0
  38. package/dist/lint/post-synth/otel124.d.ts.map +1 -0
  39. package/dist/lint/post-synth/otel125.d.ts +8 -0
  40. package/dist/lint/post-synth/otel125.d.ts.map +1 -0
  41. package/dist/lint/post-synth/otel126.d.ts +8 -0
  42. package/dist/lint/post-synth/otel126.d.ts.map +1 -0
  43. package/dist/lint/post-synth/otel127.d.ts +8 -0
  44. package/dist/lint/post-synth/otel127.d.ts.map +1 -0
  45. package/dist/lint/rules/literal-credential.d.ts.map +1 -1
  46. package/dist/manifest.json +1 -1
  47. package/dist/metric-names.d.ts +29 -0
  48. package/dist/metric-names.d.ts.map +1 -1
  49. package/dist/model.d.ts +24 -0
  50. package/dist/model.d.ts.map +1 -1
  51. package/dist/okf/index.md +10 -0
  52. package/dist/okf/rules/OTEL118.md +11 -0
  53. package/dist/okf/rules/OTEL119.md +11 -0
  54. package/dist/okf/rules/OTEL120.md +11 -0
  55. package/dist/okf/rules/OTEL121.md +11 -0
  56. package/dist/okf/rules/OTEL122.md +11 -0
  57. package/dist/okf/rules/OTEL123.md +11 -0
  58. package/dist/okf/rules/OTEL124.md +11 -0
  59. package/dist/okf/rules/OTEL125.md +11 -0
  60. package/dist/okf/rules/OTEL126.md +11 -0
  61. package/dist/okf/rules/OTEL127.md +11 -0
  62. package/dist/platform.d.ts +11 -8
  63. package/dist/platform.d.ts.map +1 -1
  64. package/dist/rules/literal-credential.ts +2 -3
  65. package/dist/rules/otel-helpers.ts +15 -4
  66. package/dist/rules/otel118.ts +17 -0
  67. package/dist/rules/otel119.ts +17 -0
  68. package/dist/rules/otel120.ts +17 -0
  69. package/dist/rules/otel121.ts +17 -0
  70. package/dist/rules/otel122.ts +17 -0
  71. package/dist/rules/otel123.ts +17 -0
  72. package/dist/rules/otel124.ts +17 -0
  73. package/dist/rules/otel125.ts +17 -0
  74. package/dist/rules/otel126.ts +17 -0
  75. package/dist/rules/otel127.ts +17 -0
  76. package/dist/semconv.d.ts +2 -1
  77. package/dist/semconv.d.ts.map +1 -1
  78. package/dist/skills/chant-otel.md +6 -2
  79. package/dist/validate-config.d.ts +27 -9
  80. package/dist/validate-config.d.ts.map +1 -1
  81. package/package.json +2 -2
  82. package/src/attribution.ts +385 -0
  83. package/src/codegen/docs.ts +8 -1
  84. package/src/components/k8s-receivers.test.ts +5 -2
  85. package/src/components/processors.ts +1 -1
  86. package/src/components/sampling.ts +3 -0
  87. package/src/composites/composites.test.ts +16 -0
  88. package/src/config-hygiene.ts +470 -0
  89. package/src/connectors.test.ts +3 -0
  90. package/src/define.ts +25 -4
  91. package/src/import/generator.test.ts +42 -4
  92. package/src/import/generator.ts +7 -2
  93. package/src/import/parser.test.ts +11 -0
  94. package/src/import/parser.ts +16 -4
  95. package/src/import/roundtrip.test.ts +90 -14
  96. package/src/index.ts +7 -0
  97. package/src/lint/audit-catalog.ts +87 -2
  98. package/src/lint/post-synth/hygiene.test.ts +334 -0
  99. package/src/lint/post-synth/index.ts +20 -0
  100. package/src/lint/post-synth/otel-helpers.ts +15 -4
  101. package/src/lint/post-synth/otel118.test.ts +308 -0
  102. package/src/lint/post-synth/otel118.ts +17 -0
  103. package/src/lint/post-synth/otel119.ts +17 -0
  104. package/src/lint/post-synth/otel120.ts +17 -0
  105. package/src/lint/post-synth/otel121.ts +17 -0
  106. package/src/lint/post-synth/otel122.ts +17 -0
  107. package/src/lint/post-synth/otel123.ts +17 -0
  108. package/src/lint/post-synth/otel124.ts +17 -0
  109. package/src/lint/post-synth/otel125.ts +17 -0
  110. package/src/lint/post-synth/otel126.ts +17 -0
  111. package/src/lint/post-synth/otel127.ts +17 -0
  112. package/src/lint/post-synth/post-synth.test.ts +18 -0
  113. package/src/lint/rules/literal-credential.ts +2 -3
  114. package/src/metric-names.test.ts +24 -2
  115. package/src/metric-names.ts +66 -1
  116. package/src/model.ts +48 -0
  117. package/src/platform.test.ts +35 -0
  118. package/src/platform.ts +28 -19
  119. package/src/plugin.test.ts +10 -0
  120. package/src/semconv.ts +6 -2
  121. package/src/skills/chant-otel.md +6 -2
  122. package/src/validate-config.ts +81 -16
@@ -10,7 +10,7 @@
10
10
  import type { Declarable } from "@intentius/chant/declarable";
11
11
  import { type CollectorConfig } from "./model.js";
12
12
  import "./components/connectors.js";
13
- export type CollectorIssueCode = "OTEL101" | "OTEL102" | "OTEL103" | "OTEL104" | "OTEL105" | "OTEL106" | "OTEL107" | "OTEL108" | "OTEL109" | "OTEL112" | "OTEL113" | "OTEL114" | "OTEL115" | "OTEL116" | "OTEL117";
13
+ export type CollectorIssueCode = "OTEL101" | "OTEL102" | "OTEL103" | "OTEL104" | "OTEL105" | "OTEL106" | "OTEL107" | "OTEL108" | "OTEL109" | "OTEL112" | "OTEL113" | "OTEL114" | "OTEL115" | "OTEL116" | "OTEL117" | "OTEL118" | "OTEL119" | "OTEL120" | "OTEL121" | "OTEL122" | "OTEL123" | "OTEL124" | "OTEL125" | "OTEL126" | "OTEL127";
14
14
  export interface CollectorIssue {
15
15
  code: CollectorIssueCode;
16
16
  severity: "error" | "warning";
@@ -25,18 +25,36 @@ export interface CollectorIssue {
25
25
  * each connector's signals against its definition (OTEL112), cycles through
26
26
  * connectors (OTEL113), connector ids shared with a receiver or exporter
27
27
  * (OTEL114), `routing` connector targets (OTEL115), the attributes
28
- * connectors split metrics by (OTEL116), and started components that listen
29
- * on the same address (OTEL117).
28
+ * connectors split metrics by (OTEL116), started components that listen
29
+ * on the same address (OTEL117), and the deprecation, exposure, delivery and
30
+ * name-list checks in `config-hygiene.ts` (OTEL119-OTEL127).
30
31
  */
31
32
  export declare function validateCollectorConfig(config: CollectorConfig): CollectorIssue[];
33
+ /** One address a started component listens on. */
34
+ export interface Listener {
35
+ /** Who listens: `receiver "otlp" (protocols.grpc.endpoint)`. */
36
+ owner: string;
37
+ component?: string;
38
+ host: string;
39
+ port: number;
40
+ transport: "tcp" | "udp";
41
+ }
42
+ /** A config key path a component listens on, and the transport it listens with. */
43
+ export interface ListenerPath {
44
+ path: string[];
45
+ transport: "tcp" | "udp";
46
+ }
32
47
  /**
33
- * The config key paths to the addresses a receiver of this type listens on,
34
- * from the table OTEL117 checks. Empty for a type not in the table, whose
35
- * `endpoint` may be a server the collector connects to. `collectorEndpoints`
36
- * reads the receiver ports from here, so the ports a platform composite
37
- * publishes and the listeners OTEL117 compares are the same set.
48
+ * The config key paths to the addresses a receiver or exporter of this type
49
+ * listens on, from the table OTEL117 checks, with the transport each listens
50
+ * with in `body`. Empty for a type not in the table, whose `endpoint` may be
51
+ * a server it connects to. `collectorEndpoints` reads the ports from here, so
52
+ * the ports a platform composite publishes and the listeners OTEL117 compares
53
+ * are the same set.
38
54
  */
39
- export declare function receiverListenerPaths(type: string): string[][];
55
+ export declare function listenerPaths(kind: "receiver" | "exporter", type: string, body: unknown): ListenerPath[];
56
+ /** The addresses a component of this kind listens on, from the OTEL117 table and its defaults. */
57
+ export declare function componentListeners(kind: "receiver" | "exporter" | "extension", id: string, body: unknown): Listener[];
40
58
  /**
41
59
  * Check declared entities for what only the declaration knows (OTEL107-OTEL109):
42
60
  * each component's own config rules, duplicate ids, and custom components'
@@ -1 +1 @@
1
- {"version":3,"file":"validate-config.d.ts","sourceRoot":"","sources":["../src/validate-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,OAAO,EAA4D,KAAK,eAAe,EAA4B,MAAM,SAAS,CAAC;AAMnI,OAAO,yBAAyB,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,EAAE,CAkKjF;AA+RD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAE9D;AAuJD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,cAAc,EAAE,CAyDpH"}
1
+ {"version":3,"file":"validate-config.d.ts","sourceRoot":"","sources":["../src/validate-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,OAAO,EAAoF,KAAK,eAAe,EAA4B,MAAM,SAAS,CAAC;AAO3J,OAAO,yBAAyB,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,EAAE,CAmKjF;AA6OD,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;CAC1B;AA4ED,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;CAC1B;AAYD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,YAAY,EAAE,CAExG;AAuBD,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,CAWrH;AAqHD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,cAAc,EAAE,CAyDpH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-otel",
3
- "version": "0.102.0",
3
+ "version": "0.104.0",
4
4
  "description": "OpenTelemetry Collector lexicon for chant — typed receivers, processors, exporters, connectors, extensions and pipelines, serialized to collector YAML",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -68,7 +68,7 @@
68
68
  "typescript": "^5.9.3"
69
69
  },
70
70
  "peerDependencies": {
71
- "@intentius/chant": "^0.102.0",
71
+ "@intentius/chant": "^0.104.0",
72
72
  "typescript": "^5.9.3"
73
73
  }
74
74
  }
@@ -0,0 +1,385 @@
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
+
56
+ import { TELEMETRY_ATTRIBUTES } from "@intentius/chant/telemetry-attribution";
57
+ import { canonicalTypeOf, type CollectorConfig } from "./model";
58
+ import type { CollectorIssue } from "./validate-config";
59
+
60
+ /** The keys the attribution stamps. Any other `chant.*` key is protected too. */
61
+ export const PROTECTED_RESOURCE_ATTRIBUTES: readonly string[] = TELEMETRY_ATTRIBUTES.map((a) => a.key);
62
+
63
+ /** True for a key OTEL118 protects: one of `PROTECTED_RESOURCE_ATTRIBUTES`, or any `chant.*` key. */
64
+ export function isProtectedResourceAttribute(key: string): boolean {
65
+ return PROTECTED_RESOURCE_ATTRIBUTES.includes(key) || key.startsWith("chant.");
66
+ }
67
+
68
+ /** The protected keys a regular expression matches. An RE2 pattern JavaScript can't compile matches none. */
69
+ function protectedMatching(pattern: string): string[] {
70
+ let re: RegExp;
71
+ try {
72
+ re = new RegExp(pattern);
73
+ } catch {
74
+ return [];
75
+ }
76
+ return PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => re.test(k));
77
+ }
78
+
79
+ function quoteAll(keys: string[]): string {
80
+ return keys.map((k) => `"${k}"`).join(", ");
81
+ }
82
+
83
+ const REPLACING_ACTIONS: Record<string, string> = {
84
+ delete: "removes",
85
+ update: "replaces",
86
+ upsert: "replaces",
87
+ hash: "replaces with its hash",
88
+ };
89
+
90
+ function resourceActionFindings(body: Record<string, unknown>): string[] {
91
+ const out: string[] = [];
92
+ const actions = Array.isArray(body.attributes) ? body.attributes : [];
93
+ for (const raw of actions) {
94
+ if (typeof raw !== "object" || raw === null) continue;
95
+ const { key, action, pattern } = raw as { key?: unknown; action?: unknown; pattern?: unknown };
96
+ const verb = typeof action === "string" ? REPLACING_ACTIONS[action] : undefined;
97
+ if (!verb) continue;
98
+ if (typeof key === "string" && isProtectedResourceAttribute(key)) {
99
+ out.push(`its ${action} action ${verb} "${key}"`);
100
+ } else if (typeof pattern === "string" && (action === "delete" || action === "hash")) {
101
+ const hit = protectedMatching(pattern);
102
+ if (hit.length > 0) out.push(`its ${action} action's pattern "${pattern}" matches ${quoteAll(hit)}`);
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+
108
+ // ── transform: a text match over OTTL ────────────────────────────────
109
+
110
+ const OTTL_CALL = /\b(set|delete_key|delete_matching_keys|keep_keys|keep_matching_keys)\s*\(/g;
111
+
112
+ /** The top-level arguments of the call whose `(` is at `open`, as written. */
113
+ function callArguments(text: string, open: number): string[] {
114
+ const args: string[] = [];
115
+ let depth = 0;
116
+ let start = open + 1;
117
+ let quoted = false;
118
+ for (let i = open; i < text.length; i++) {
119
+ const c = text[i];
120
+ if (quoted) {
121
+ if (c === "\\") i++;
122
+ else if (c === '"') quoted = false;
123
+ continue;
124
+ }
125
+ if (c === '"') quoted = true;
126
+ else if (c === "(" || c === "[" || c === "{") depth++;
127
+ else if (c === ")" || c === "]" || c === "}") {
128
+ depth--;
129
+ if (depth === 0) {
130
+ args.push(text.slice(start, i).trim());
131
+ return args;
132
+ }
133
+ } else if (c === "," && depth === 1) {
134
+ args.push(text.slice(start, i).trim());
135
+ start = i + 1;
136
+ }
137
+ }
138
+ return args;
139
+ }
140
+
141
+ /** The OTTL string literals in `text`, unescaped. */
142
+ function stringLiterals(text: string): string[] {
143
+ return [...text.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1].replace(/\\(["\\])/g, "$1"));
144
+ }
145
+
146
+ /**
147
+ * Whether `path` is the resource's attribute map, and the key it indexes.
148
+ * `resource.attributes` always is; a bare `attributes` is only in a statement
149
+ * group whose context is `resource`.
150
+ */
151
+ function resourcePath(path: string, resourceContext: boolean): { key?: string } | undefined {
152
+ const m = /^(resource\.)?attributes\s*(?:\[\s*"((?:[^"\\]|\\.)*)"\s*\])?$/.exec(path);
153
+ if (!m || (!m[1] && !resourceContext)) return undefined;
154
+ return m[2] === undefined ? {} : { key: m[2].replace(/\\(["\\])/g, "$1") };
155
+ }
156
+
157
+ function statementFindings(statement: string, resourceContext: boolean): string[] {
158
+ const out: string[] = [];
159
+ for (const call of statement.matchAll(OTTL_CALL)) {
160
+ const fn = call[1];
161
+ const args = callArguments(statement, call.index! + call[0].length - 1);
162
+ const target = resourcePath(args[0] ?? "", resourceContext);
163
+ if (!target) continue;
164
+ const [literal] = stringLiterals(args[1] ?? "");
165
+ if (fn === "set") {
166
+ if (target.key === undefined) out.push(`set replaces the whole resource attribute map`);
167
+ else if (isProtectedResourceAttribute(target.key)) out.push(`set replaces "${target.key}"`);
168
+ } else if (target.key !== undefined) {
169
+ continue;
170
+ } else if (fn === "delete_key" && literal !== undefined && isProtectedResourceAttribute(literal)) {
171
+ out.push(`delete_key removes "${literal}"`);
172
+ } else if (fn === "delete_matching_keys" && literal !== undefined) {
173
+ const hit = protectedMatching(literal);
174
+ if (hit.length > 0) out.push(`delete_matching_keys "${literal}" removes ${quoteAll(hit)}`);
175
+ } else if (fn === "keep_keys") {
176
+ const kept = new Set(stringLiterals(args[1] ?? ""));
177
+ const dropped = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !kept.has(k));
178
+ if (dropped.length > 0) out.push(`keep_keys drops ${quoteAll(dropped)}`);
179
+ } else if (fn === "keep_matching_keys" && literal !== undefined) {
180
+ const hit = new Set(protectedMatching(literal));
181
+ const dropped = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !hit.has(k));
182
+ if (dropped.length > 0) out.push(`keep_matching_keys "${literal}" drops ${quoteAll(dropped)}`);
183
+ }
184
+ }
185
+ return out;
186
+ }
187
+
188
+ function transformFindings(body: Record<string, unknown>): string[] {
189
+ const out: string[] = [];
190
+ for (const [field, groups] of Object.entries(body)) {
191
+ if (!field.endsWith("_statements") || !Array.isArray(groups)) continue;
192
+ for (const group of groups) {
193
+ // A plain string infers its context from its paths, so only `resource.` paths reach the resource.
194
+ const [statements, resourceContext] =
195
+ typeof group === "string"
196
+ ? [[group], false]
197
+ : typeof group === "object" && group !== null
198
+ ? [(group as { statements?: unknown }).statements, (group as { context?: unknown }).context === "resource"]
199
+ : [[], false];
200
+ if (!Array.isArray(statements)) continue;
201
+ for (const statement of statements) {
202
+ if (typeof statement !== "string") continue;
203
+ for (const finding of statementFindings(statement, resourceContext)) {
204
+ out.push(`the OTTL text \`${statement}\` matches a call where ${finding}`);
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return out;
210
+ }
211
+
212
+ // ── resourcedetection ────────────────────────────────────────────────
213
+
214
+ /** Detectors that can write a protected key, and which. */
215
+ const OVERRIDING_DETECTORS: Record<string, string> = {
216
+ env: "any key in the collector's own OTEL_RESOURCE_ATTRIBUTES",
217
+ dynatrace: "any key in the Dynatrace properties file",
218
+ heroku: '"service.name" and "service.version"',
219
+ elastic_beanstalk: '"service.version"',
220
+ };
221
+
222
+ function detectionFindings(body: Record<string, unknown>): string[] {
223
+ // `override` defaults to true, and `detectors` to [env], at contrib v0.130.0.
224
+ if (body.override === false) return [];
225
+ const detectors = Array.isArray(body.detectors) ? body.detectors : ["env"];
226
+ const how = body.override === true ? "override: true" : "override on by default";
227
+ return detectors
228
+ .filter((d): d is string => typeof d === "string" && d in OVERRIDING_DETECTORS)
229
+ .map((d) => `with ${how}, the ${d} detector writes ${OVERRIDING_DETECTORS[d]} over the service's own value; set override: false`);
230
+ }
231
+
232
+ // ── processors that rewrite the resource as a side effect ─────────────
233
+
234
+ function strings(value: unknown): string[] {
235
+ return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
236
+ }
237
+
238
+ function groupByAttrsFindings(body: Record<string, unknown>): string[] {
239
+ const keys = strings(body.keys).filter(isProtectedResourceAttribute);
240
+ return keys.length > 0 ? [`it groups by ${quoteAll(keys)}, so a record attribute with that key is copied over the resource's value`] : [];
241
+ }
242
+
243
+ function redactionFindings(body: Record<string, unknown>): string[] {
244
+ const out: string[] = [];
245
+ const ignored = new Set(strings(body.ignored_keys));
246
+ const allowed = new Set(strings(body.allowed_keys));
247
+ const exposed = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !ignored.has(k));
248
+ if (body.allow_all_keys !== true) {
249
+ const deleted = exposed.filter((k) => !allowed.has(k));
250
+ if (deleted.length > 0) {
251
+ const how = body.allow_all_keys === false ? "allow_all_keys: false" : "allow_all_keys off by default";
252
+ out.push(`with ${how}, it deletes resource keys missing from allowed_keys and ignored_keys: ${quoteAll(deleted)}`);
253
+ }
254
+ }
255
+ for (const pattern of strings(body.blocked_key_patterns)) {
256
+ const masked = exposed.filter((k) => protectedMatching(pattern).includes(k));
257
+ if (masked.length > 0) out.push(`its blocked_key_patterns entry "${pattern}" masks the value of ${quoteAll(masked)}`);
258
+ }
259
+ return out;
260
+ }
261
+
262
+ function sumologicFindings(body: Record<string, unknown>): string[] {
263
+ const out: string[] = [];
264
+ // `translate_attributes` defaults to true at contrib v0.130.0.
265
+ if (body.translate_attributes !== false) {
266
+ out.push(`translate_attributes${body.translate_attributes === true ? "" : " (on by default)"} renames "service.name" to "service" on log and metric resources`);
267
+ }
268
+ const nest = body.nest_attributes;
269
+ if (typeof nest === "object" && nest !== null && (nest as { enabled?: unknown }).enabled === true) {
270
+ const include = strings((nest as { include?: unknown }).include);
271
+ const exclude = strings((nest as { exclude?: unknown }).exclude);
272
+ const nested = PROTECTED_RESOURCE_ATTRIBUTES.filter(
273
+ (k) => (include.length === 0 || include.some((p) => k.startsWith(p))) && !exclude.some((p) => k.startsWith(p)),
274
+ );
275
+ if (nested.length > 0) out.push(`nest_attributes turns ${quoteAll(nested)} into nested maps`);
276
+ }
277
+ for (const pair of Array.isArray(body.aggregate_attributes) ? body.aggregate_attributes : []) {
278
+ const prefixes = strings((pair as { prefixes?: unknown } | null)?.prefixes);
279
+ const moved = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => prefixes.some((p) => k.startsWith(p)));
280
+ if (moved.length > 0) out.push(`aggregate_attributes prefixes ${quoteAll(prefixes)} move ${quoteAll(moved)} into one attribute`);
281
+ }
282
+ return out;
283
+ }
284
+
285
+ // Schema versions whose `all` changes rename a protected key, in semantic-conventions' schemas/1.37.0.
286
+ const SCHEMA_RENAMES: ReadonlyArray<{ version: [number, number, number]; from: string; to: string }> = [
287
+ { version: [1, 29, 0], from: "vcs.repository.ref.revision", to: "vcs.ref.head.revision" },
288
+ { version: [1, 27, 0], from: "deployment.environment", to: "deployment.environment.name" },
289
+ ];
290
+
291
+ function schemaFindings(body: Record<string, unknown>): string[] {
292
+ const out: string[] = [];
293
+ for (const target of strings(body.targets)) {
294
+ const m = /^https?:\/\/opentelemetry\.io\/schemas\/(\d+)\.(\d+)\.(\d+)$/.exec(target);
295
+ if (!m) continue;
296
+ const v = [Number(m[1]), Number(m[2]), Number(m[3])];
297
+ const older = (r: [number, number, number]) => v[0] < r[0] || (v[0] === r[0] && (v[1] < r[1] || (v[1] === r[1] && v[2] < r[2])));
298
+ for (const rename of SCHEMA_RENAMES) {
299
+ if (older(rename.version)) out.push(`its target ${target} renames "${rename.to}" back to "${rename.from}" on data with a newer schema URL`);
300
+ }
301
+ }
302
+ return out;
303
+ }
304
+
305
+ function metricsTransformFindings(body: Record<string, unknown>): string[] {
306
+ const out: string[] = [];
307
+ for (const t of Array.isArray(body.transforms) ? body.transforms : []) {
308
+ if (typeof t !== "object" || t === null || (t as { action?: unknown }).action !== "group") continue;
309
+ const labels = (t as { group_resource_labels?: unknown }).group_resource_labels;
310
+ const keys = typeof labels === "object" && labels !== null ? Object.keys(labels).filter(isProtectedResourceAttribute) : [];
311
+ if (keys.length > 0) out.push(`a group transform's group_resource_labels sets ${quoteAll(keys)} on the new resource`);
312
+ }
313
+ return out;
314
+ }
315
+
316
+ /** The key of a stanza `resource["<key>"]` field, or undefined. */
317
+ function resourceField(field: unknown): string | undefined {
318
+ if (typeof field !== "string") return undefined;
319
+ const m = /^resource\[\s*["']([^"']*)["']\s*\]$/.exec(field.trim());
320
+ return m?.[1];
321
+ }
322
+
323
+ function logsTransformFindings(body: Record<string, unknown>): string[] {
324
+ const out: string[] = [];
325
+ for (const raw of Array.isArray(body.operators) ? body.operators : []) {
326
+ if (typeof raw !== "object" || raw === null) continue;
327
+ const op = raw as Record<string, unknown>;
328
+ const type = String(op.type ?? "");
329
+ const touched = [
330
+ ...(type === "add" || type === "remove" ? [resourceField(op.field)] : []),
331
+ ...(type === "move" ? [resourceField(op.from), resourceField(op.to)] : []),
332
+ ...(type === "copy" ? [resourceField(op.to)] : []),
333
+ ].filter((k): k is string => k !== undefined && isProtectedResourceAttribute(k));
334
+ if (touched.length > 0) out.push(`its ${type} operator writes or removes ${quoteAll(touched)}`);
335
+ if (type === "retain") {
336
+ const kept = strings(op.fields).map(resourceField).filter((k): k is string => k !== undefined);
337
+ const dropped = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !kept.includes(k));
338
+ if (kept.length > 0 && dropped.length > 0) out.push(`its retain operator keeps only the resource fields it lists, dropping ${quoteAll(dropped)}`);
339
+ }
340
+ if (op.parse_to === "resource") out.push(`its ${type} operator parses into the resource, so a parsed key can replace any protected key`);
341
+ }
342
+ return out;
343
+ }
344
+
345
+ const FINDINGS: Record<string, (body: Record<string, unknown>) => string[]> = {
346
+ resource: resourceActionFindings,
347
+ transform: transformFindings,
348
+ resourcedetection: detectionFindings,
349
+ groupbyattrs: groupByAttrsFindings,
350
+ redaction: redactionFindings,
351
+ sumologic: sumologicFindings,
352
+ schema: schemaFindings,
353
+ metricstransform: metricsTransformFindings,
354
+ logstransform: logsTransformFindings,
355
+ };
356
+
357
+ /**
358
+ * OTEL118 over one collector config: each processor a pipeline lists that can
359
+ * remove or replace a protected resource attribute. Not part of
360
+ * `validateCollectorConfig`, because it applies only to a build that stamps
361
+ * the attribution: the post-synth check runs it when the context carries
362
+ * `telemetry`.
363
+ */
364
+ export function attributionIssues(config: CollectorConfig): CollectorIssue[] {
365
+ const pipelinesOf = new Map<string, string[]>();
366
+ for (const [pipeline, body] of Object.entries(config.service?.pipelines ?? {})) {
367
+ for (const id of body?.processors ?? []) pipelinesOf.set(String(id), [...(pipelinesOf.get(String(id)) ?? []), pipeline]);
368
+ }
369
+ const issues: CollectorIssue[] = [];
370
+ for (const [id, pipelines] of pipelinesOf) {
371
+ const raw = config.processors?.[id];
372
+ const body = typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
373
+ const type = canonicalTypeOf("processor", id);
374
+ const findings = Object.prototype.hasOwnProperty.call(FINDINGS, type) ? FINDINGS[type](body) : [];
375
+ for (const finding of findings) {
376
+ issues.push({
377
+ code: "OTEL118",
378
+ severity: "warning",
379
+ component: id,
380
+ 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`,
381
+ });
382
+ }
383
+ }
384
+ return issues;
385
+ }
@@ -55,7 +55,14 @@ connector id shared with a receiver or exporter (OTEL114), a \`routing\` route
55
55
  to a pipeline that doesn't receive from it (OTEL115), a connector
56
56
  splitting metrics by a per-request GenAI attribute such as
57
57
  \`gen_ai.conversation.id\` (OTEL116), two started components listening
58
- on the same address (OTEL117), and a literal credential in source (OTEL002).
58
+ on the same address (OTEL117), and a literal credential in source (OTEL002)
59
+ or in the config (OTEL120). Others report deprecated fields (OTEL119),
60
+ credentials sent in plaintext (OTEL121), zpages or pprof off loopback
61
+ (OTEL122), detailed debug output beside a backend (OTEL123), delivery and
62
+ batching settings (OTEL124, OTEL125), and k8sattributes fields or
63
+ resourcedetection detectors the collector doesn't know (OTEL126, OTEL127). In
64
+ a build that stamps telemetry attribution, OTEL118 reports a processor that can
65
+ drop or overwrite one of the attribution's resource attributes.
59
66
  `;
60
67
 
61
68
  const outputFormat = `The otel lexicon serializes every otel entity in a build into **one
@@ -5,7 +5,7 @@ import { otelSerializer } from "../serializer";
5
5
  import { collectorYaml } from "../collector";
6
6
  import { collectorTopology } from "../topology";
7
7
  import { validateCollectorEntities } from "../validate-config";
8
- import { COLLECTOR_PIN } from "../define";
8
+ import { COLLECTOR_PIN, SEMCONV_PIN } from "../define";
9
9
  import { DebugExporter } from "./exporters";
10
10
  import { K8sClusterReceiver, KubeletStatsReceiver } from "./k8s-receivers";
11
11
  import { Pipeline } from "../pipeline";
@@ -43,7 +43,10 @@ describe("k8s_cluster receiver", () => {
43
43
  metrics: { "k8s.pod.status_reason": { enabled: true } },
44
44
  });
45
45
  const out = primary(otelSerializer.serialize(new Map([["cluster", cluster as unknown as Declarable]])));
46
- expect(out).toBe(`receivers:
46
+ // The `k8s.pod.status_reason` metric name is a k8s key, so the header names the pin.
47
+ expect(out).toBe(`# chant: semconv k8s ${SEMCONV_PIN.source}@${SEMCONV_PIN.version} (k8s_cluster)
48
+
49
+ receivers:
47
50
  k8s_cluster:
48
51
  auth_type: serviceAccount
49
52
  collection_interval: 30s
@@ -175,7 +175,7 @@ export type ResourceDetector =
175
175
  | "ec2"
176
176
  | "ecs"
177
177
  | "eks"
178
- | "elasticbeanstalk"
178
+ | "elastic_beanstalk"
179
179
  | "lambda"
180
180
  | "azure"
181
181
  | "aks"
@@ -24,6 +24,7 @@ export interface NumericAttributePolicyConfig {
24
24
  key: string;
25
25
  min_value?: number;
26
26
  max_value?: number;
27
+ /** @deprecated Inverted decisions are deprecated since collector-contrib v0.126.0; use a `drop` policy (OTEL119). */
27
28
  invert_match?: boolean;
28
29
  }
29
30
 
@@ -46,6 +47,7 @@ export interface StringAttributePolicyConfig {
46
47
  values: string[];
47
48
  enabled_regex_matching?: boolean;
48
49
  cache_max_size?: number;
50
+ /** @deprecated Inverted decisions are deprecated since collector-contrib v0.126.0; use a `drop` policy (OTEL119). */
49
51
  invert_match?: boolean;
50
52
  }
51
53
 
@@ -70,6 +72,7 @@ export interface TraceStatePolicyConfig {
70
72
  export interface BooleanAttributePolicyConfig {
71
73
  key: string;
72
74
  value: boolean;
75
+ /** @deprecated Inverted decisions are deprecated since collector-contrib v0.126.0; use a `drop` policy (OTEL119). */
73
76
  invert_match?: boolean;
74
77
  }
75
78
 
@@ -10,6 +10,8 @@ import { OtlpExporter, PrometheusExporter } from "../components/exporters";
10
10
  import { collectorYaml } from "../collector";
11
11
  import { validateCollectorConfig } from "../validate-config";
12
12
  import type { CollectorConfig } from "../model";
13
+ import { SEMCONV_PIN } from "../define";
14
+ import { semconvUsage } from "../semconv";
13
15
 
14
16
  const gateway = new OtlpExporter({ name: "gateway", endpoint: "otel-gateway.observability.svc:4317", tls: { insecure: true } });
15
17
  const base: NodeAgentProps = { exporters: [gateway] };
@@ -136,3 +138,17 @@ describe("NodeAgent options", () => {
136
138
  expect((config.receivers?.filelog as Loose).exclude).toEqual(["/var/log/pods/*/agent/*.log"]);
137
139
  });
138
140
  });
141
+
142
+ describe("NodeAgent semconv", () => {
143
+ test("the YAML names the k8s semantic conventions its keys follow", () => {
144
+ const yaml = collectorYaml(Object.values(NodeAgent({ ...base, clusterName: "prod-eu-1" }).members) as Declarable[]);
145
+ expect(yaml.split("\n")[0]).toBe(`# chant: semconv k8s ${SEMCONV_PIN.source}@${SEMCONV_PIN.version} (k8sattributes, resource)`);
146
+ });
147
+
148
+ test("k8s_cluster and k8sattributes as names are not k8s keys", () => {
149
+ expect(semconvUsage({ receivers: { k8s_cluster: {} }, processors: { k8sattributes: {}, "resource/k8s": {} } })).toEqual([]);
150
+ expect(semconvUsage({ processors: { "transform/x": { statements: ['delete_matching_keys(resource.attributes, "^k8s\\\\.pod\\\\..*")'] } } })).toEqual([
151
+ { namespace: "k8s", ...SEMCONV_PIN, components: ["transform/x"] },
152
+ ]);
153
+ });
154
+ });