@intentius/chant-lexicon-otel 0.101.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) 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/connectors.d.ts.map +1 -1
  6. package/dist/components/processors.d.ts +1 -1
  7. package/dist/components/processors.d.ts.map +1 -1
  8. package/dist/components/sampling.d.ts +3 -0
  9. package/dist/components/sampling.d.ts.map +1 -1
  10. package/dist/composites/node-agent.d.ts +4 -3
  11. package/dist/composites/node-agent.d.ts.map +1 -1
  12. package/dist/config-hygiene.d.ts +33 -0
  13. package/dist/config-hygiene.d.ts.map +1 -0
  14. package/dist/define.d.ts +18 -2
  15. package/dist/define.d.ts.map +1 -1
  16. package/dist/genai.d.ts.map +1 -1
  17. package/dist/import/generator.d.ts +5 -1
  18. package/dist/import/generator.d.ts.map +1 -1
  19. package/dist/import/parser.d.ts +1 -1
  20. package/dist/import/parser.d.ts.map +1 -1
  21. package/dist/index.d.ts +4 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/init-templates.d.ts.map +1 -1
  24. package/dist/integrity.json +15 -5
  25. package/dist/lint/audit-catalog.d.ts +3 -2
  26. package/dist/lint/audit-catalog.d.ts.map +1 -1
  27. package/dist/lint/post-synth/index.d.ts.map +1 -1
  28. package/dist/lint/post-synth/otel-helpers.d.ts +2 -2
  29. package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
  30. package/dist/lint/post-synth/otel118.d.ts +8 -0
  31. package/dist/lint/post-synth/otel118.d.ts.map +1 -0
  32. package/dist/lint/post-synth/otel119.d.ts +8 -0
  33. package/dist/lint/post-synth/otel119.d.ts.map +1 -0
  34. package/dist/lint/post-synth/otel120.d.ts +8 -0
  35. package/dist/lint/post-synth/otel120.d.ts.map +1 -0
  36. package/dist/lint/post-synth/otel121.d.ts +8 -0
  37. package/dist/lint/post-synth/otel121.d.ts.map +1 -0
  38. package/dist/lint/post-synth/otel122.d.ts +8 -0
  39. package/dist/lint/post-synth/otel122.d.ts.map +1 -0
  40. package/dist/lint/post-synth/otel123.d.ts +8 -0
  41. package/dist/lint/post-synth/otel123.d.ts.map +1 -0
  42. package/dist/lint/post-synth/otel124.d.ts +8 -0
  43. package/dist/lint/post-synth/otel124.d.ts.map +1 -0
  44. package/dist/lint/post-synth/otel125.d.ts +8 -0
  45. package/dist/lint/post-synth/otel125.d.ts.map +1 -0
  46. package/dist/lint/post-synth/otel126.d.ts +8 -0
  47. package/dist/lint/post-synth/otel126.d.ts.map +1 -0
  48. package/dist/lint/post-synth/otel127.d.ts +8 -0
  49. package/dist/lint/post-synth/otel127.d.ts.map +1 -0
  50. package/dist/lint/rules/literal-credential.d.ts.map +1 -1
  51. package/dist/manifest.json +1 -1
  52. package/dist/metric-names.d.ts +29 -0
  53. package/dist/metric-names.d.ts.map +1 -1
  54. package/dist/model.d.ts +24 -0
  55. package/dist/model.d.ts.map +1 -1
  56. package/dist/okf/index.md +10 -0
  57. package/dist/okf/rules/OTEL118.md +11 -0
  58. package/dist/okf/rules/OTEL119.md +11 -0
  59. package/dist/okf/rules/OTEL120.md +11 -0
  60. package/dist/okf/rules/OTEL121.md +11 -0
  61. package/dist/okf/rules/OTEL122.md +11 -0
  62. package/dist/okf/rules/OTEL123.md +11 -0
  63. package/dist/okf/rules/OTEL124.md +11 -0
  64. package/dist/okf/rules/OTEL125.md +11 -0
  65. package/dist/okf/rules/OTEL126.md +11 -0
  66. package/dist/okf/rules/OTEL127.md +11 -0
  67. package/dist/platform.d.ts +11 -4
  68. package/dist/platform.d.ts.map +1 -1
  69. package/dist/rules/literal-credential.ts +2 -3
  70. package/dist/rules/otel-helpers.ts +15 -4
  71. package/dist/rules/otel118.ts +17 -0
  72. package/dist/rules/otel119.ts +17 -0
  73. package/dist/rules/otel120.ts +17 -0
  74. package/dist/rules/otel121.ts +17 -0
  75. package/dist/rules/otel122.ts +17 -0
  76. package/dist/rules/otel123.ts +17 -0
  77. package/dist/rules/otel124.ts +17 -0
  78. package/dist/rules/otel125.ts +17 -0
  79. package/dist/rules/otel126.ts +17 -0
  80. package/dist/rules/otel127.ts +17 -0
  81. package/dist/semconv.d.ts +2 -1
  82. package/dist/semconv.d.ts.map +1 -1
  83. package/dist/skill-defs.d.ts +1 -1
  84. package/dist/skill-defs.d.ts.map +1 -1
  85. package/dist/skills/chant-otel.md +7 -3
  86. package/dist/validate-config.d.ts +29 -3
  87. package/dist/validate-config.d.ts.map +1 -1
  88. package/package.json +3 -3
  89. package/src/attribution.ts +385 -0
  90. package/src/codegen/docs.ts +8 -1
  91. package/src/components/k8s-receivers.test.ts +5 -2
  92. package/src/components/processors.ts +1 -1
  93. package/src/components/sampling.ts +3 -0
  94. package/src/composites/composites.test.ts +16 -0
  95. package/src/composites/node-agent.ts +3 -2
  96. package/src/config-hygiene.ts +470 -0
  97. package/src/connectors.test.ts +3 -0
  98. package/src/define.ts +25 -4
  99. package/src/import/generator.test.ts +42 -4
  100. package/src/import/generator.ts +7 -2
  101. package/src/import/parser.test.ts +11 -0
  102. package/src/import/parser.ts +16 -4
  103. package/src/import/roundtrip.test.ts +90 -14
  104. package/src/index.ts +7 -0
  105. package/src/init-templates.ts +4 -2
  106. package/src/lint/audit-catalog.ts +87 -2
  107. package/src/lint/post-synth/hygiene.test.ts +334 -0
  108. package/src/lint/post-synth/index.ts +20 -0
  109. package/src/lint/post-synth/otel-helpers.ts +15 -4
  110. package/src/lint/post-synth/otel118.test.ts +308 -0
  111. package/src/lint/post-synth/otel118.ts +17 -0
  112. package/src/lint/post-synth/otel119.ts +17 -0
  113. package/src/lint/post-synth/otel120.ts +17 -0
  114. package/src/lint/post-synth/otel121.ts +17 -0
  115. package/src/lint/post-synth/otel122.ts +17 -0
  116. package/src/lint/post-synth/otel123.ts +17 -0
  117. package/src/lint/post-synth/otel124.ts +17 -0
  118. package/src/lint/post-synth/otel125.ts +17 -0
  119. package/src/lint/post-synth/otel126.ts +17 -0
  120. package/src/lint/post-synth/otel127.ts +17 -0
  121. package/src/lint/post-synth/post-synth.test.ts +31 -0
  122. package/src/lint/rules/literal-credential.ts +2 -3
  123. package/src/metric-names.test.ts +24 -2
  124. package/src/metric-names.ts +66 -1
  125. package/src/model.ts +48 -0
  126. package/src/platform.test.ts +61 -0
  127. package/src/platform.ts +35 -22
  128. package/src/plugin.test.ts +10 -0
  129. package/src/semconv.ts +6 -2
  130. package/src/skills/chant-otel.md +7 -3
  131. package/src/validate-config.ts +85 -9
@@ -0,0 +1,17 @@
1
+ /**
2
+ * OTEL127: resourcedetection lists a detector that does not exist
3
+ *
4
+ * Each name in a started `resourcedetection` processor's `detectors` must be one the processor registers at collector-contrib v0.130.0 (`RESOURCE_DETECTORS`); any other fails the processor's build and the collector exits.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { configDiagnostics } from "./otel-helpers";
9
+
10
+ export const otel127: PostSynthCheck = {
11
+ id: "OTEL127",
12
+ description: "resourcedetection lists a detector that does not exist",
13
+
14
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ return configDiagnostics(ctx, "OTEL127");
16
+ },
17
+ };
package/dist/semconv.d.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * A collector config has no metadata channel, so a config cannot say which
6
6
  * semconv version its attribute keys follow. What it can show is that it uses
7
7
  * a vocabulary: a `spanmetrics` dimension named `gen_ai.request.model`, an
8
- * OTTL statement deleting `gen_ai.input.messages`. `semconvUsage()` finds
8
+ * OTTL statement deleting `gen_ai.input.messages`, a `k8sattributes`
9
+ * processor extracting `k8s.pod.name`. `semconvUsage()` finds
9
10
  * those and pairs each vocabulary with the pin this package's keys follow, the
10
11
  * same way `collectorTopology()` pairs a component type with the pin of the
11
12
  * definition this process has. Reading a parsed YAML file and reading the
@@ -1 +1 @@
1
- {"version":3,"file":"semconv.d.ts","sourceRoot":"","sources":["../src/semconv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EAAc,KAAK,eAAe,EAAsB,MAAM,SAAS,CAAC;AAE/E,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,SAAS,CAAC;IACf,2EAA2E;IAC3E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACpC;AAKD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,iBAAiB,CAEhE,CAAC;AAEH,mEAAmE;AACnE,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAWD,0GAA0G;AAC1G,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,YAAY,EAAE,CAYpE"}
1
+ {"version":3,"file":"semconv.d.ts","sourceRoot":"","sources":["../src/semconv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAc,KAAK,eAAe,EAAsB,MAAM,SAAS,CAAC;AAE/E,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,SAAS,CAAC;IACf,2EAA2E;IAC3E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACpC;AAOD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,iBAAiB,CAGhE,CAAC;AAEH,mEAAmE;AACnE,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAWD,0GAA0G;AAC1G,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,YAAY,EAAE,CAYpE"}
@@ -1,3 +1,3 @@
1
1
  /** The otel lexicon's AI skills, read from src/skills/. */
2
- export declare const otelSkills: () => import("packages/core/src").SkillDefinition[];
2
+ export declare const otelSkills: () => import("@intentius/chant").SkillDefinition[];
3
3
  //# sourceMappingURL=skill-defs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"skill-defs.d.ts","sourceRoot":"","sources":["../src/skill-defs.ts"],"names":[],"mappings":"AAEA,2DAA2D;AAC3D,eAAO,MAAM,UAAU,qDAgCrB,CAAC"}
1
+ {"version":3,"file":"skill-defs.d.ts","sourceRoot":"","sources":["../src/skill-defs.ts"],"names":[],"mappings":"AAEA,2DAA2D;AAC3D,eAAO,MAAM,UAAU,oDAgCrB,CAAC"}
@@ -17,7 +17,7 @@ export default { lexicons: ["otel"] };
17
17
 
18
18
  `chant init --lexicon otel` scaffolds a collector; `--template k8s-agent` scaffolds a per-node Kubernetes agent and `--template genai` a GenAI collector.
19
19
 
20
- For a per-node Kubernetes agent, `NodeAgent({ exporters, metricExporters?, clusterName?, kubeletStats? })` declares the whole config: OTLP, host metrics, container logs, `k8sattributes` filtered to the node, and `memory_limiter` first and `batch` last on every pipeline. Pass `Object.values(agent.members)` wherever an entity list is taken, such as the k8s lexicon's `OtelCollector` `config`.
20
+ For a per-node Kubernetes agent, `NodeAgent({ exporters, metricExporters?, clusterName?, kubeletStats? })` declares the whole config: OTLP, host metrics, container logs, `k8sattributes` filtered to the node, and `memory_limiter` first and `batch` last on every pipeline. Pass `Object.values(agent.members)` wherever an entity list is taken, such as the k8s lexicon's `OtelCollector` `config`, which adds what the config reads from the node: `K8S_NODE_NAME` from `spec.nodeName`, the host root at `/hostfs` and `/var/log/pods` read-only, group 0 for the log files, and the RBAC.
21
21
 
22
22
  ## Declaring components
23
23
 
@@ -78,10 +78,14 @@ It deletes prompt, completion, system-instruction and tool-call content from spa
78
78
 
79
79
  - Reference components by entity where you can. A string id (`"otlp/backend"`) is allowed for a component declared elsewhere, and OTEL101 fails the build if nothing declares it.
80
80
  - Put `memory_limiter` first in `processors` (OTEL105).
81
- - Never write a credential literally. Use `"${env:NAME}"` and the collector reads it at start-up (OTEL002).
81
+ - Never write a credential literally. Use `"${env:NAME}"` and the collector reads it at start-up (OTEL002 in source, OTEL120 in the config). Don't send one over `http://` or `tls.insecure: true` (OTEL121).
82
82
  - Every pipeline needs at least one receiver and one exporter (OTEL102). A declared component no pipeline uses is a warning (OTEL103).
83
83
  - Don't split metrics by a per-request id (`gen_ai.conversation.id`, `gen_ai.response.id`, `gen_ai.tool.call.id`, `session.id`, `user.id`, `enduser.id`) or a content key: each value starts new time series (OTEL116).
84
84
  - Give every listener its own port. The collector's own metrics already use localhost:8888, so a `prometheus` exporter there makes it exit with "address already in use" (OTEL117).
85
+ - Keep `zpages` and `pprof` on localhost (OTEL122). `health_check` may listen on 0.0.0.0 for probes.
86
+ - Put `batch` in every pipeline that sends to a remote `otlp` or `otlphttp` exporter (OTEL125), and leave the exporter's `sending_queue` and `retry_on_failure` on (OTEL124).
87
+ - Use a `drop` policy rather than `invert_match`, and `readers` rather than `service.telemetry.metrics.address` (OTEL119).
88
+ - In a workspace, or with `telemetry.attribution: true`, leave `service.name`, `service.version`, `deployment.environment.name`, `vcs.ref.head.revision` and `chant.*` resource attributes alone: `insert` rather than `upsert`, and `override: false` on `resourcedetection` (OTEL118).
85
89
  - Declared extensions are enabled in declaration order unless a `Service` lists `extensions` itself.
86
90
 
87
91
  ## Starting from an existing config
@@ -97,4 +101,4 @@ The importer writes `receivers.ts`, `processors.ts`, `exporters.ts`, `connectors
97
101
 
98
102
  ## Reading the result
99
103
 
100
- `collectorTopologyOf(entities)` returns the pipelines, each component's endpoints and schema pin, for each exporter the signals it carries, the connector `edges` between pipelines, and under `semconv` the semantic-conventions version (`GENAI_SEMCONV_PIN`) the config's `gen_ai.` keys follow, as plain data.
104
+ `collectorTopologyOf(entities)` returns the pipelines, each component's endpoints and schema pin, for each exporter the signals it carries, the connector `edges` between pipelines, and under `semconv` the semantic-conventions version the config's `gen_ai.` keys (`GENAI_SEMCONV_PIN`) and `k8s.` keys (`SEMCONV_PIN`) follow, as plain data.
@@ -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,10 +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
+ }
47
+ /**
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.
54
+ */
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[];
32
58
  /**
33
59
  * Check declared entities for what only the declaration knows (OTEL107-OTEL109):
34
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;AAobD;;;;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.101.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",
@@ -56,7 +56,7 @@
56
56
  "validate": "tsx src/validate-cli.ts",
57
57
  "docs": "tsx src/codegen/docs-cli.ts",
58
58
  "prepack": "npm run generate && npm run bundle && npm run validate && npm run build",
59
- "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && find dist -type f \\( -name \"*.js\" -o -name \"*.js.map\" \\) -delete"
59
+ "build": "../../scripts/tsc7.sh -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && find dist -type f \\( -name \"*.js\" -o -name \"*.js.map\" \\) -delete"
60
60
  },
61
61
  "dependencies": {
62
62
  "js-yaml": "^4.3.1"
@@ -68,7 +68,7 @@
68
68
  "typescript": "^5.9.3"
69
69
  },
70
70
  "peerDependencies": {
71
- "@intentius/chant": "^0.101.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
+ });