@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
@@ -1,9 +1,15 @@
1
1
  import { describe, expect, test } from "vitest";
2
- import { PrometheusExporter, SpanMetricsConnector } from "./components";
2
+ import { PrometheusExporter, ServiceGraphConnector, SpanMetricsConnector } from "./components";
3
3
  import { genAiComponents, genAiMetrics } from "./genai";
4
- import { prometheusLabel, prometheusMetricName, spanMetricsNames } from "./metric-names";
4
+ import { prometheusLabel, prometheusMetricName, serviceGraphNames, spanMetricsNames } from "./metric-names";
5
5
 
6
6
  describe("spanMetricsNames", () => {
7
+ test("takes a span_metrics declaration, the collector's newer name, as a spanmetrics one", () => {
8
+ const declared = { componentType: "span_metrics", props: { namespace: "spans" } } as never;
9
+ expect(spanMetricsNames(declared).namespace).toBe("spans");
10
+ expect(() => spanMetricsNames({ componentType: "count", props: {} } as never)).toThrow("expected a spanmetrics connector");
11
+ });
12
+
7
13
  test("defaults: the traces.span.metrics namespace and a millisecond histogram", () => {
8
14
  const n = spanMetricsNames(new SpanMetricsConnector({}));
9
15
  expect(n.namespace).toBe("traces.span.metrics");
@@ -78,3 +84,19 @@ describe("prometheus naming", () => {
78
84
  expect(prometheusMetricName("calls", "sum", "{call}")).toBe("calls_total");
79
85
  });
80
86
  });
87
+
88
+ describe("serviceGraphNames", () => {
89
+ test("the fixed names, in seconds, with the edge labels and the client_/server_ dimensions", () => {
90
+ const n = serviceGraphNames(new ServiceGraphConnector({ dimensions: ["http.method"], virtual_node_extra_label: true }));
91
+ expect(n.requests.prometheus).toBe("traces_service_graph_request_total");
92
+ expect(n.failed.prometheus).toBe("traces_service_graph_request_failed_total");
93
+ expect(n.serverDuration.prometheus).toBe("traces_service_graph_request_server_seconds");
94
+ expect(n.clientDuration.prometheus).toBe("traces_service_graph_request_client_seconds");
95
+ expect(n.requests.dimensions).toEqual(["client", "server", "connection_type", "failed", "client_http.method", "server_http.method", "virtual_node"]);
96
+ });
97
+
98
+ test("the exporter's namespace prefixes the names", () => {
99
+ expect(serviceGraphNames({}, new PrometheusExporter({ endpoint: "0.0.0.0:8889", namespace: "otel" })).requests.prometheus).toBe("otel_traces_service_graph_request_total");
100
+ expect(() => serviceGraphNames(new SpanMetricsConnector({}))).toThrow(/expected a servicegraph connector/);
101
+ });
102
+ });
@@ -19,6 +19,8 @@
19
19
  * builds queries from it does not load the collector components.
20
20
  */
21
21
 
22
+ import { canonicalComponentType } from "./model";
23
+
22
24
  /** A metric a collector component emits, as the collector names it and as Prometheus exposes it. */
23
25
  export interface CollectorMetric {
24
26
  /** The OTLP metric name. */
@@ -114,7 +116,7 @@ type Declared = { componentType?: unknown; props?: unknown };
114
116
  function configOf(connector: SpanMetricsNamingConfig | Declared): SpanMetricsNamingConfig {
115
117
  const d = connector as Declared;
116
118
  if (typeof d.componentType === "string") {
117
- if (d.componentType !== "spanmetrics") throw new Error(`spanMetricsNames: expected a spanmetrics connector, got ${d.componentType}`);
119
+ if (canonicalComponentType("connector", d.componentType) !== "spanmetrics") throw new Error(`spanMetricsNames: expected a spanmetrics connector, got ${d.componentType}`);
118
120
  return (d.props ?? {}) as SpanMetricsNamingConfig;
119
121
  }
120
122
  return connector as SpanMetricsNamingConfig;
@@ -174,3 +176,66 @@ export function spanMetricsNames(
174
176
  errorStatus: SPAN_STATUS_ERROR,
175
177
  };
176
178
  }
179
+
180
+ /** The prefix of every metric the `servicegraph` connector emits; the connector takes no namespace. */
181
+ export const SERVICEGRAPH_NAMESPACE = "traces_service_graph";
182
+
183
+ /** The parts of a servicegraph config the metric labels depend on. */
184
+ export interface ServiceGraphNamingConfig {
185
+ dimensions?: string[];
186
+ virtual_node_extra_label?: boolean;
187
+ }
188
+
189
+ /** The metrics one `servicegraph` connector emits. */
190
+ export interface ServiceGraphNames {
191
+ /** Requests per edge. */
192
+ requests: CollectorMetric;
193
+ /** Failed requests per edge. */
194
+ failed: CollectorMetric;
195
+ /** Server-side request duration histogram, in seconds. */
196
+ serverDuration: CollectorMetric;
197
+ /** Client-side request duration histogram, in seconds. */
198
+ clientDuration: CollectorMetric;
199
+ }
200
+
201
+ /**
202
+ * The Prometheus names of the metrics a `servicegraph` connector emits at
203
+ * `COLLECTOR_PIN`, with its default feature gates: latency in seconds, under
204
+ * `_request_server` and `_request_client`. Every metric carries `client`,
205
+ * `server`, `connection_type` and `failed`, `client_<d>` and `server_<d>` for
206
+ * each configured dimension, and `virtual_node` when
207
+ * `virtual_node_extra_label` is set (connector/servicegraphconnector/connector.go).
208
+ */
209
+ export function serviceGraphNames(
210
+ connector: ServiceGraphNamingConfig | Declared,
211
+ exporter?: PrometheusNaming | { props?: PrometheusNaming },
212
+ ): ServiceGraphNames {
213
+ const d = connector as Declared;
214
+ if (typeof d.componentType === "string" && d.componentType !== "servicegraph") {
215
+ throw new Error(`serviceGraphNames: expected a servicegraph connector, got ${d.componentType}`);
216
+ }
217
+ const c = (typeof d.componentType === "string" ? (d.props ?? {}) : connector) as ServiceGraphNamingConfig;
218
+ const naming: PrometheusNaming =
219
+ exporter && "props" in exporter && typeof exporter.props === "object" ? (exporter.props as PrometheusNaming) : ((exporter ?? {}) as PrometheusNaming);
220
+ const dimensions = [
221
+ "client",
222
+ "server",
223
+ "connection_type",
224
+ "failed",
225
+ ...(c.dimensions ?? []).flatMap((dim) => [`client_${dim}`, `server_${dim}`]),
226
+ ...(c.virtual_node_extra_label ? ["virtual_node"] : []),
227
+ ];
228
+ const metric = (n: string, type: "sum" | "histogram", unit?: string): CollectorMetric => ({
229
+ name: `${SERVICEGRAPH_NAMESPACE}_${n}`,
230
+ prometheus: prometheusMetricName(`${SERVICEGRAPH_NAMESPACE}_${n}`, type, unit, naming),
231
+ type,
232
+ ...(unit ? { unit } : {}),
233
+ dimensions,
234
+ });
235
+ return {
236
+ requests: metric("request_total", "sum"),
237
+ failed: metric("request_failed_total", "sum"),
238
+ serverDuration: metric("request_server", "histogram", "s"),
239
+ clientDuration: metric("request_client", "histogram", "s"),
240
+ };
241
+ }
package/src/model.ts CHANGED
@@ -108,3 +108,51 @@ export function looksLikeCollectorConfig(value: unknown): value is CollectorConf
108
108
  if (typeof pipelines !== "object" || pipelines === null) return false;
109
109
  return "receivers" in v || "exporters" in v;
110
110
  }
111
+
112
+ /**
113
+ * A built-in type the collector renamed, keeping the old name as a
114
+ * deprecated alias. `type` is the new name and `builtin` the name chant's
115
+ * class declares and emits. `since` is the first release of `repo` whose
116
+ * `metadata.yaml` for the component lists `builtin` as `deprecated_type`.
117
+ */
118
+ export interface ComponentTypeAlias {
119
+ kind: ComponentKind;
120
+ type: string;
121
+ builtin: string;
122
+ since: string;
123
+ repo: "core" | "contrib";
124
+ }
125
+
126
+ /**
127
+ * The renamed built-ins. chant emits `builtin`, which the pinned collector
128
+ * (`COLLECTOR_PIN`, v0.130.0) knows and newer ones still accept. A config
129
+ * written for a newer collector may use `type`, so code that reads a config
130
+ * (the importer, the checks, the k8s composites) accepts both.
131
+ */
132
+ export const COMPONENT_TYPE_ALIASES: readonly ComponentTypeAlias[] = [
133
+ { kind: "exporter", type: "otlp_grpc", builtin: "otlp", since: "v0.148.0", repo: "core" },
134
+ { kind: "exporter", type: "otlp_http", builtin: "otlphttp", since: "v0.148.0", repo: "core" },
135
+ { kind: "processor", type: "k8s_attributes", builtin: "k8sattributes", since: "v0.148.0", repo: "contrib" },
136
+ { kind: "connector", type: "signal_to_metrics", builtin: "signaltometrics", since: "v0.148.0", repo: "contrib" },
137
+ { kind: "receiver", type: "file_log", builtin: "filelog", since: "v0.149.0", repo: "contrib" },
138
+ { kind: "connector", type: "span_metrics", builtin: "spanmetrics", since: "v0.151.0", repo: "contrib" },
139
+ { kind: "connector", type: "service_graph", builtin: "servicegraph", since: "v0.151.0", repo: "contrib" },
140
+ { kind: "receiver", type: "host_metrics", builtin: "hostmetrics", since: "v0.151.0", repo: "contrib" },
141
+ { kind: "receiver", type: "kubelet_stats", builtin: "kubeletstats", since: "v0.152.0", repo: "contrib" },
142
+ { kind: "processor", type: "resource_detection", builtin: "resourcedetection", since: "v0.153.0", repo: "contrib" },
143
+ { kind: "exporter", type: "load_balancing", builtin: "loadbalancing", since: "v0.153.0", repo: "contrib" },
144
+ { kind: "processor", type: "delta_to_cumulative", builtin: "deltatocumulative", since: "v0.158.0", repo: "contrib" },
145
+ ];
146
+
147
+ const ALIAS_TO_BUILTIN: ReadonlyMap<string, string> = new Map(COMPONENT_TYPE_ALIASES.map((a) => [`${a.kind}:${a.type}`, a.builtin]));
148
+
149
+ /** The type chant declares for `kind` + `type`: the old name of a renamed built-in, otherwise `type` itself. */
150
+ export function canonicalComponentType(kind: ComponentKind, type: string): string {
151
+ return ALIAS_TO_BUILTIN.get(`${kind}:${type}`) ?? type;
152
+ }
153
+
154
+ /** The type of component id `id`, with a renamed built-in's new name read as the old one: `span_metrics/genai` is `spanmetrics`. */
155
+ export function canonicalTypeOf(kind: ComponentKind, id: string): string {
156
+ const slash = id.indexOf("/");
157
+ return canonicalComponentType(kind, slash === -1 ? id : id.slice(0, slash));
158
+ }
@@ -44,6 +44,67 @@ describe("collectorEndpoints", () => {
44
44
  expect(collectorEndpoints(config).ports).toEqual([{ name: "otlp-edge-grpc", port: 14317 }]);
45
45
  });
46
46
 
47
+ test("a receiver's client endpoint is not a port: kubeletstats and k8s_cluster beside otlp add none (#3102)", () => {
48
+ const config: CollectorConfig = {
49
+ receivers: {
50
+ otlp: { protocols: { grpc: { endpoint: "0.0.0.0:4317" } } },
51
+ kubeletstats: { auth_type: "serviceAccount", endpoint: "https://${env:K8S_NODE_NAME}:10250" },
52
+ k8s_cluster: { auth_type: "serviceAccount" },
53
+ prometheus: { config: { scrape_configs: [] }, target_allocator: { endpoint: "http://ta:8080" } },
54
+ },
55
+ };
56
+ expect(collectorEndpoints(config).ports).toEqual([{ name: "otlp-grpc", port: 4317 }]);
57
+ });
58
+
59
+ test("zipkin and jaeger listeners are ports", () => {
60
+ const config: CollectorConfig = {
61
+ receivers: {
62
+ zipkin: { endpoint: "0.0.0.0:9411" },
63
+ jaeger: { protocols: { grpc: { endpoint: "0.0.0.0:14250" }, thrift_http: { endpoint: "0.0.0.0:14268" } } },
64
+ },
65
+ };
66
+ expect(collectorEndpoints(config).ports).toEqual([
67
+ { name: "zipkin", port: 9411 },
68
+ { name: "jaeger-grpc", port: 14250 },
69
+ { name: "jaeger-thrift-h", port: 14268 },
70
+ ]);
71
+ });
72
+
73
+ test("the prometheus exporter's scrape endpoint is a port, after the receivers' (#3122)", () => {
74
+ const config: CollectorConfig = {
75
+ receivers: { otlp: { protocols: { grpc: { endpoint: "0.0.0.0:4317" } } } },
76
+ exporters: { prometheus: { endpoint: "0.0.0.0:8889" }, otlp: { endpoint: "tempo:4317" } },
77
+ };
78
+ expect(collectorEndpoints(config).ports).toEqual([
79
+ { name: "otlp-grpc", port: 4317 },
80
+ { name: "prometheus", port: 8889 },
81
+ ]);
82
+ });
83
+
84
+ test("contrib push receivers that listen are ports, and a UDP listener says so (#3122)", () => {
85
+ const config: CollectorConfig = {
86
+ receivers: {
87
+ statsd: { endpoint: "0.0.0.0:8125" },
88
+ "carbon/tcp": { endpoint: "0.0.0.0:2003" },
89
+ "carbon/udp": { endpoint: "0.0.0.0:2003", transport: "udp" },
90
+ syslog: { protocol: "rfc5424", tcp: { listen_address: "0.0.0.0:54526" }, udp: { listen_address: "0.0.0.0:54526" } },
91
+ fluentforward: { endpoint: "0.0.0.0:8006" },
92
+ splunk_hec: { endpoint: "0.0.0.0:8088" },
93
+ influxdb: { endpoint: "0.0.0.0:8086" },
94
+ },
95
+ };
96
+ expect(collectorEndpoints(config).ports).toEqual([
97
+ { name: "statsd", port: 8125, protocol: "UDP" },
98
+ { name: "carbon-tcp", port: 2003 },
99
+ { name: "carbon-udp", port: 2003, protocol: "UDP" },
100
+ { name: "syslog-tcp", port: 54526 },
101
+ { name: "syslog-udp", port: 54526, protocol: "UDP" },
102
+ { name: "fluentforward", port: 8006 },
103
+ { name: "splunk-hec", port: 8088 },
104
+ { name: "influxdb", port: 8086 },
105
+ ]);
106
+ });
107
+
47
108
  test("port names are cut to 15 characters", () => {
48
109
  const config: CollectorConfig = {
49
110
  receivers: { "otlp/very_long_name": { protocols: { http: { endpoint: "0.0.0.0:4318" } } } },
package/src/platform.ts CHANGED
@@ -17,6 +17,7 @@ import { DebugExporter } from "./components/exporters";
17
17
  import { HealthCheckExtension } from "./components/extensions";
18
18
  import { Pipeline } from "./pipeline";
19
19
  import { SIGNALS, parseComponentId, type CollectorConfig, type Signal } from "./model";
20
+ import { listenerPaths } from "./validate-config";
20
21
 
21
22
  /** The contrib collector image at the version the built-in components are typed against. */
22
23
  export const COLLECTOR_IMAGE = `otel/opentelemetry-collector-contrib:${COLLECTOR_PIN.version.replace(/^v/, "")}`;
@@ -65,10 +66,12 @@ export interface CollectorPort {
65
66
  /** A name usable as a k8s port name: lowercase, `-` separated, at most 15 characters. */
66
67
  name: string;
67
68
  port: number;
69
+ /** Set when the listener is UDP. A port without it is TCP. */
70
+ protocol?: "UDP";
68
71
  }
69
72
 
70
73
  export interface CollectorEndpoints {
71
- /** Ports the receivers listen on, in config order, one entry per port. */
74
+ /** Ports the receivers, then the exporters, listen on, in config order, one entry per port and protocol. */
72
75
  ports: CollectorPort[];
73
76
  /**
74
77
  * The `health_check` extension, when the service enables one and it listens
@@ -95,36 +98,46 @@ function portName(parts: string[]): string {
95
98
  return name.slice(0, 15).replace(/-$/, "");
96
99
  }
97
100
 
98
- /** Every `endpoint` value under a receiver's config, with the key path that led to it. */
99
- function receiverEndpoints(value: unknown, path: string[], out: Array<{ path: string[]; endpoint: unknown }>): void {
100
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
101
- for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
102
- if (key === "endpoint") out.push({ path, endpoint: child });
103
- else receiverEndpoints(child, [...path, key], out);
101
+ /** The value at a key path, if every key on the way is there. */
102
+ function valueAt(value: unknown, path: string[]): unknown {
103
+ let cur: unknown = value;
104
+ for (const key of path) {
105
+ if (!cur || typeof cur !== "object" || Array.isArray(cur)) return undefined;
106
+ cur = (cur as Record<string, unknown>)[key];
104
107
  }
108
+ return cur;
105
109
  }
106
110
 
107
111
  const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
108
112
 
109
113
  /**
110
- * The ports a built collector config listens on: every receiver `endpoint`,
111
- * named after the receiver and the protocol key above it (`otlp-grpc`,
112
- * `otlp-http`), and the `health_check` port if the service enables it.
114
+ * The ports a built collector config listens on: the address every receiver
115
+ * and exporter that listens sets (the types OTEL117 knows: otlp, jaeger,
116
+ * statsd, syslog, the `prometheus` exporter's scrape endpoint and the rest),
117
+ * named after the component and the key above the address (`otlp-grpc`,
118
+ * `syslog-udp`), and the `health_check` port if the service enables it. A
119
+ * component whose `endpoint` is a server it connects to, such as
120
+ * kubeletstats' kubelet, adds no port, and neither does a type the table
121
+ * doesn't list. A UDP listener gets `protocol: "UDP"`.
113
122
  */
114
123
  export function collectorEndpoints(config: CollectorConfig): CollectorEndpoints {
115
124
  const ports: CollectorPort[] = [];
116
- const seen = new Set<number>();
117
- for (const [id, receiverConfig] of Object.entries(config.receivers ?? {})) {
118
- const found: Array<{ path: string[]; endpoint: unknown }> = [];
119
- receiverEndpoints(receiverConfig, [], found);
120
- const parsed = parseComponentId(id);
121
- const base = parsed ? [parsed.type, ...(parsed.name ? [parsed.name] : [])] : [id];
122
- for (const { path, endpoint } of found) {
123
- const p = portOf(endpoint);
124
- if (!p || seen.has(p.port)) continue;
125
- seen.add(p.port);
126
- const protocol = path.filter((k) => k !== "protocols");
127
- ports.push({ name: portName([...base, ...protocol]), port: p.port });
125
+ const seen = new Set<string>();
126
+ const sections: Array<["receiver" | "exporter", Record<string, unknown> | undefined]> = [
127
+ ["receiver", config.receivers],
128
+ ["exporter", config.exporters],
129
+ ];
130
+ for (const [kind, section] of sections) {
131
+ for (const [id, body] of Object.entries(section ?? {})) {
132
+ const parsed = parseComponentId(id);
133
+ const base = parsed ? [parsed.type, ...(parsed.name ? [parsed.name] : [])] : [id];
134
+ for (const { path, transport } of listenerPaths(kind, parsed?.type ?? id, body)) {
135
+ const p = portOf(valueAt(body, path));
136
+ if (!p || seen.has(`${p.port}/${transport}`)) continue;
137
+ seen.add(`${p.port}/${transport}`);
138
+ const protocol = path.slice(0, -1).filter((k) => k !== "protocols");
139
+ ports.push({ name: portName([...base, ...protocol]), port: p.port, ...(transport === "udp" ? { protocol: "UDP" as const } : {}) });
140
+ }
128
141
  }
129
142
  }
130
143
 
@@ -34,6 +34,16 @@ describe("otel plugin", () => {
34
34
  "OTEL115",
35
35
  "OTEL116",
36
36
  "OTEL117",
37
+ "OTEL118",
38
+ "OTEL119",
39
+ "OTEL120",
40
+ "OTEL121",
41
+ "OTEL122",
42
+ "OTEL123",
43
+ "OTEL124",
44
+ "OTEL125",
45
+ "OTEL126",
46
+ "OTEL127",
37
47
  ]);
38
48
  for (const id of ids) expect(id.startsWith("OTEL")).toBe(true);
39
49
  });
package/src/semconv.ts CHANGED
@@ -5,14 +5,15 @@
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
12
13
  * declaration give the same answer.
13
14
  */
14
15
 
15
- import { GENAI_SEMCONV_PIN, type SchemaPin } from "./define";
16
+ import { GENAI_SEMCONV_PIN, SEMCONV_PIN, type SchemaPin } from "./define";
16
17
  import { SECTION_OF, type CollectorConfig, type ComponentKind } from "./model";
17
18
 
18
19
  /** One attribute vocabulary and the version of its conventions this package follows. */
@@ -26,9 +27,12 @@ export interface SemconvVocabulary {
26
27
 
27
28
  // `gen_ai.` as a key, in OTTL (`attributes["gen_ai.x"]`) or in an RE2 pattern (`gen_ai\.`).
28
29
  const GEN_AI_REF = /(^|[^A-Za-z0-9_])gen_ai(\\\\|\\)?\./;
30
+ // `k8s.` with the dot, so the component types `k8s_cluster` and `k8sattributes` don't count.
31
+ const K8S_REF = /(^|[^A-Za-z0-9_])k8s(\\\\|\\)?\./;
29
32
 
30
33
  export const SEMCONV_VOCABULARIES: ReadonlyArray<SemconvVocabulary> = Object.freeze([
31
34
  { namespace: "gen_ai", pin: GENAI_SEMCONV_PIN, matches: (text: string) => GEN_AI_REF.test(text) },
35
+ { namespace: "k8s", pin: SEMCONV_PIN, matches: (text: string) => K8S_REF.test(text) },
32
36
  ]);
33
37
 
34
38
  /** A vocabulary a config uses, with the components that use it. */
@@ -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.
@@ -11,11 +11,12 @@
11
11
  import type { Declarable } from "@intentius/chant/declarable";
12
12
  import { definitionFor, definitionOf, isOTelComponent, isUsablePin, runValidator } from "./define";
13
13
  import { componentConfig } from "./collector";
14
- import { isComponentId, parseComponentId, pipelineSignal, SIGNALS, type CollectorConfig, type ConnectorSignalPair } from "./model";
14
+ import { canonicalComponentType, isComponentId, parseComponentId, pipelineSignal, SIGNALS, type CollectorConfig, type ConnectorSignalPair } from "./model";
15
15
  import { isPipelineEntity } from "./pipeline";
16
16
  import { signalToMetricsEntries, type SignalToMetricsConnectorConfig } from "./components/connectors";
17
17
  import { genAiCardinalityRisk } from "./genai";
18
18
  import { collectorTopology, type TopologyEdge } from "./topology";
19
+ import { configHygieneIssues } from "./config-hygiene";
19
20
  // OTEL112 reads the built-in connectors' signal pairs from the registry.
20
21
  import "./components/connectors";
21
22
 
@@ -34,7 +35,17 @@ export type CollectorIssueCode =
34
35
  | "OTEL114"
35
36
  | "OTEL115"
36
37
  | "OTEL116"
37
- | "OTEL117";
38
+ | "OTEL117"
39
+ | "OTEL118"
40
+ | "OTEL119"
41
+ | "OTEL120"
42
+ | "OTEL121"
43
+ | "OTEL122"
44
+ | "OTEL123"
45
+ | "OTEL124"
46
+ | "OTEL125"
47
+ | "OTEL126"
48
+ | "OTEL127";
38
49
 
39
50
  export interface CollectorIssue {
40
51
  code: CollectorIssueCode;
@@ -67,8 +78,9 @@ function describePairs(pairs: ReadonlyArray<ConnectorSignalPair>): string {
67
78
  * each connector's signals against its definition (OTEL112), cycles through
68
79
  * connectors (OTEL113), connector ids shared with a receiver or exporter
69
80
  * (OTEL114), `routing` connector targets (OTEL115), the attributes
70
- * connectors split metrics by (OTEL116), and started components that listen
71
- * on the same address (OTEL117).
81
+ * connectors split metrics by (OTEL116), started components that listen
82
+ * on the same address (OTEL117), and the deprecation, exposure, delivery and
83
+ * name-list checks in `config-hygiene.ts` (OTEL119-OTEL127).
72
84
  */
73
85
  export function validateCollectorConfig(config: CollectorConfig): CollectorIssue[] {
74
86
  const issues: CollectorIssue[] = [];
@@ -229,6 +241,7 @@ export function validateCollectorConfig(config: CollectorConfig): CollectorIssue
229
241
  ...routingTargetIssues(config),
230
242
  ...metricAttributeIssues(config),
231
243
  ...listenerIssues(config),
244
+ ...configHygieneIssues(config),
232
245
  );
233
246
 
234
247
  return issues;
@@ -449,7 +462,7 @@ function metricAttributeUses(type: string, config: Record<string, unknown>): Met
449
462
  function metricAttributeIssues(config: CollectorConfig): CollectorIssue[] {
450
463
  const issues: CollectorIssue[] = [];
451
464
  for (const [id, raw] of Object.entries(config.connectors ?? {})) {
452
- const type = parseComponentId(id)?.type ?? id;
465
+ const type = canonicalComponentType("connector", parseComponentId(id)?.type ?? id);
453
466
  const body = typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
454
467
  for (const { key, field } of metricAttributeUses(type, body)) {
455
468
  const risk = genAiCardinalityRisk(key);
@@ -470,7 +483,7 @@ function metricAttributeIssues(config: CollectorConfig): CollectorIssue[] {
470
483
  }
471
484
 
472
485
  /** One address a started component listens on. */
473
- interface Listener {
486
+ export interface Listener {
474
487
  /** Who listens: `receiver "otlp" (protocols.grpc.endpoint)`. */
475
488
  owner: string;
476
489
  component?: string;
@@ -486,12 +499,14 @@ interface Listener {
486
499
  * default applies only when the key's parent is in the config: an otlp or
487
500
  * jaeger protocol that isn't listed doesn't start. A type not listed here is
488
501
  * not checked, since its `endpoint` may be a server it connects to (the
489
- * kubeletstats receiver's is the kubelet).
502
+ * kubeletstats receiver's is the kubelet). `transportKey` names a sibling key
503
+ * that switches the transport (`transport: udp` on carbon).
490
504
  */
491
505
  interface ListenerSpec {
492
506
  path: string[];
493
507
  default?: string;
494
508
  transport?: "udp";
509
+ transportKey?: string;
495
510
  present?: boolean;
496
511
  }
497
512
 
@@ -508,6 +523,38 @@ const LISTENERS: Record<"receiver" | "exporter" | "extension", Record<string, Li
508
523
  { path: ["protocols", "thrift_binary", "endpoint"], default: "localhost:6832", present: true, transport: "udp" },
509
524
  { path: ["protocols", "thrift_compact", "endpoint"], default: "localhost:6831", present: true, transport: "udp" },
510
525
  ],
526
+ opencensus: [{ path: ["endpoint"], default: "localhost:55678" }],
527
+ otelarrow: [{ path: ["protocols", "grpc", "endpoint"], default: "0.0.0.0:4317" }],
528
+ skywalking: [
529
+ { path: ["protocols", "grpc", "endpoint"], default: "localhost:11800", present: true },
530
+ { path: ["protocols", "http", "endpoint"], default: "localhost:12800", present: true },
531
+ ],
532
+ loki: [
533
+ { path: ["protocols", "grpc", "endpoint"], default: "localhost:3600", present: true },
534
+ { path: ["protocols", "http", "endpoint"], default: "localhost:3500", present: true },
535
+ ],
536
+ statsd: [{ path: ["endpoint"], default: "localhost:8125", transport: "udp", transportKey: "transport" }],
537
+ carbon: [{ path: ["endpoint"], default: "localhost:2003", transportKey: "transport" }],
538
+ awsxray: [{ path: ["endpoint"], default: "localhost:2000", transport: "udp", transportKey: "transport" }],
539
+ collectd: [{ path: ["endpoint"], default: "localhost:8081" }],
540
+ datadog: [{ path: ["endpoint"], default: "localhost:8126" }],
541
+ influxdb: [{ path: ["endpoint"], default: "localhost:8086" }],
542
+ signalfx: [{ path: ["endpoint"], default: "localhost:9943" }],
543
+ splunk_hec: [{ path: ["endpoint"], default: "localhost:8088" }],
544
+ sapm: [{ path: ["endpoint"], default: "localhost:7276" }],
545
+ wavefront: [{ path: ["endpoint"], default: "localhost:2003" }],
546
+ prometheusremotewrite: [{ path: ["endpoint"], default: "localhost:9090" }],
547
+ awsfirehose: [{ path: ["endpoint"], default: "localhost:4433" }],
548
+ faro: [{ path: ["endpoint"], default: "localhost:8080" }],
549
+ libhoney: [{ path: ["http", "endpoint"], default: "localhost:8080" }],
550
+ fluentforward: [{ path: ["endpoint"] }],
551
+ webhookevent: [{ path: ["endpoint"] }],
552
+ tcplog: [{ path: ["listen_address"] }],
553
+ udplog: [{ path: ["listen_address"], transport: "udp" }],
554
+ syslog: [
555
+ { path: ["tcp", "listen_address"], present: true },
556
+ { path: ["udp", "listen_address"], present: true, transport: "udp" },
557
+ ],
511
558
  },
512
559
  exporter: {
513
560
  prometheus: [{ path: ["endpoint"] }],
@@ -519,6 +566,34 @@ const LISTENERS: Record<"receiver" | "exporter" | "extension", Record<string, Li
519
566
  },
520
567
  };
521
568
 
569
+ /** A config key path a component listens on, and the transport it listens with. */
570
+ export interface ListenerPath {
571
+ path: string[];
572
+ transport: "tcp" | "udp";
573
+ }
574
+
575
+ /** The transport a spec listens with in this component config: its `transportKey` value, else its default. */
576
+ function specTransport(spec: ListenerSpec, body: unknown): "tcp" | "udp" {
577
+ if (spec.transportKey && typeof body === "object" && body !== null) {
578
+ const value = (body as Record<string, unknown>)[spec.transportKey];
579
+ if (typeof value === "string" && /^udp[46]?$/.test(value)) return "udp";
580
+ if (typeof value === "string" && /^tcp[46]?$/.test(value)) return "tcp";
581
+ }
582
+ return spec.transport ?? "tcp";
583
+ }
584
+
585
+ /**
586
+ * The config key paths to the addresses a receiver or exporter of this type
587
+ * listens on, from the table OTEL117 checks, with the transport each listens
588
+ * with in `body`. Empty for a type not in the table, whose `endpoint` may be
589
+ * a server it connects to. `collectorEndpoints` reads the ports from here, so
590
+ * the ports a platform composite publishes and the listeners OTEL117 compares
591
+ * are the same set.
592
+ */
593
+ export function listenerPaths(kind: "receiver" | "exporter", type: string, body: unknown): ListenerPath[] {
594
+ return (LISTENERS[kind][type] ?? []).map((spec) => ({ path: spec.path, transport: specTransport(spec, body) }));
595
+ }
596
+
522
597
  /** The collector's own metrics endpoint when `service.telemetry.metrics` names no reader. */
523
598
  const DEFAULT_TELEMETRY_METRICS = { host: "localhost", port: 8888 };
524
599
 
@@ -540,7 +615,8 @@ function at(value: unknown, path: string[]): { found: boolean; value: unknown }
540
615
  return { found: true, value: cur };
541
616
  }
542
617
 
543
- function componentListeners(kind: "receiver" | "exporter" | "extension", id: string, body: unknown): Listener[] {
618
+ /** The addresses a component of this kind listens on, from the OTEL117 table and its defaults. */
619
+ export function componentListeners(kind: "receiver" | "exporter" | "extension", id: string, body: unknown): Listener[] {
544
620
  const type = parseComponentId(id)?.type ?? id;
545
621
  const out: Listener[] = [];
546
622
  for (const spec of LISTENERS[kind][type] ?? []) {
@@ -548,7 +624,7 @@ function componentListeners(kind: "receiver" | "exporter" | "extension", id: str
548
624
  const { found, value } = at(body, spec.path);
549
625
  const address = splitAddress(found && value !== undefined && value !== null ? value : spec.default);
550
626
  if (!address) continue;
551
- out.push({ owner: `${kind} "${id}" (${spec.path.join(".")})`, component: id, ...address, transport: spec.transport ?? "tcp" });
627
+ out.push({ owner: `${kind} "${id}" (${spec.path.join(".")})`, component: id, ...address, transport: specTransport(spec, body) });
552
628
  }
553
629
  return out;
554
630
  }