@intentius/chant-lexicon-otel 0.102.0 → 0.104.1

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
@@ -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
+ }
@@ -70,6 +70,41 @@ describe("collectorEndpoints", () => {
70
70
  ]);
71
71
  });
72
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
+
73
108
  test("port names are cut to 15 characters", () => {
74
109
  const config: CollectorConfig = {
75
110
  receivers: { "otlp/very_long_name": { protocols: { http: { endpoint: "0.0.0.0:4318" } } } },
package/src/platform.ts CHANGED
@@ -17,7 +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 { receiverListenerPaths } from "./validate-config";
20
+ import { listenerPaths } from "./validate-config";
21
21
 
22
22
  /** The contrib collector image at the version the built-in components are typed against. */
23
23
  export const COLLECTOR_IMAGE = `otel/opentelemetry-collector-contrib:${COLLECTOR_PIN.version.replace(/^v/, "")}`;
@@ -66,10 +66,12 @@ export interface CollectorPort {
66
66
  /** A name usable as a k8s port name: lowercase, `-` separated, at most 15 characters. */
67
67
  name: string;
68
68
  port: number;
69
+ /** Set when the listener is UDP. A port without it is TCP. */
70
+ protocol?: "UDP";
69
71
  }
70
72
 
71
73
  export interface CollectorEndpoints {
72
- /** 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. */
73
75
  ports: CollectorPort[];
74
76
  /**
75
77
  * The `health_check` extension, when the service enables one and it listens
@@ -109,26 +111,33 @@ function valueAt(value: unknown, path: string[]): unknown {
109
111
  const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
110
112
 
111
113
  /**
112
- * The ports a built collector config listens on: the endpoint of every
113
- * receiver that listens (otlp, zipkin, jaeger; the receivers OTEL117 knows),
114
- * named after the receiver and the protocol key above it (`otlp-grpc`,
115
- * `otlp-http`), and the `health_check` port if the service enables it. A
116
- * receiver whose `endpoint` is a server it connects to, such as
117
- * kubeletstats' kubelet, adds no port, and neither does a receiver type the
118
- * table doesn't list.
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"`.
119
122
  */
120
123
  export function collectorEndpoints(config: CollectorConfig): CollectorEndpoints {
121
124
  const ports: CollectorPort[] = [];
122
- const seen = new Set<number>();
123
- for (const [id, receiverConfig] of Object.entries(config.receivers ?? {})) {
124
- const parsed = parseComponentId(id);
125
- const base = parsed ? [parsed.type, ...(parsed.name ? [parsed.name] : [])] : [id];
126
- for (const path of receiverListenerPaths(parsed?.type ?? id)) {
127
- const p = portOf(valueAt(receiverConfig, path));
128
- if (!p || seen.has(p.port)) continue;
129
- seen.add(p.port);
130
- const protocol = path.slice(0, -1).filter((k) => k !== "protocols");
131
- 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
+ }
132
141
  }
133
142
  }
134
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. */
@@ -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,15 +566,32 @@ 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
+
522
585
  /**
523
- * The config key paths to the addresses a receiver of this type listens on,
524
- * from the table OTEL117 checks. Empty for a type not in the table, whose
525
- * `endpoint` may be a server the collector connects to. `collectorEndpoints`
526
- * reads the receiver ports from here, so the ports a platform composite
527
- * publishes and the listeners OTEL117 compares are the same set.
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.
528
592
  */
529
- export function receiverListenerPaths(type: string): string[][] {
530
- return (LISTENERS.receiver[type] ?? []).map((spec) => spec.path);
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) }));
531
595
  }
532
596
 
533
597
  /** The collector's own metrics endpoint when `service.telemetry.metrics` names no reader. */
@@ -551,7 +615,8 @@ function at(value: unknown, path: string[]): { found: boolean; value: unknown }
551
615
  return { found: true, value: cur };
552
616
  }
553
617
 
554
- 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[] {
555
620
  const type = parseComponentId(id)?.type ?? id;
556
621
  const out: Listener[] = [];
557
622
  for (const spec of LISTENERS[kind][type] ?? []) {
@@ -559,7 +624,7 @@ function componentListeners(kind: "receiver" | "exporter" | "extension", id: str
559
624
  const { found, value } = at(body, spec.path);
560
625
  const address = splitAddress(found && value !== undefined && value !== null ? value : spec.default);
561
626
  if (!address) continue;
562
- 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) });
563
628
  }
564
629
  return out;
565
630
  }