@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.
- package/README.md +1 -1
- package/dist/attribution.d.ts +69 -0
- package/dist/attribution.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/processors.d.ts +1 -1
- package/dist/components/processors.d.ts.map +1 -1
- package/dist/components/sampling.d.ts +3 -0
- package/dist/components/sampling.d.ts.map +1 -1
- package/dist/config-hygiene.d.ts +33 -0
- package/dist/config-hygiene.d.ts.map +1 -0
- package/dist/define.d.ts +18 -2
- package/dist/define.d.ts.map +1 -1
- package/dist/import/generator.d.ts +5 -1
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +15 -5
- package/dist/lint/audit-catalog.d.ts +3 -2
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/otel-helpers.d.ts +2 -2
- package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/otel118.d.ts +8 -0
- package/dist/lint/post-synth/otel118.d.ts.map +1 -0
- package/dist/lint/post-synth/otel119.d.ts +8 -0
- package/dist/lint/post-synth/otel119.d.ts.map +1 -0
- package/dist/lint/post-synth/otel120.d.ts +8 -0
- package/dist/lint/post-synth/otel120.d.ts.map +1 -0
- package/dist/lint/post-synth/otel121.d.ts +8 -0
- package/dist/lint/post-synth/otel121.d.ts.map +1 -0
- package/dist/lint/post-synth/otel122.d.ts +8 -0
- package/dist/lint/post-synth/otel122.d.ts.map +1 -0
- package/dist/lint/post-synth/otel123.d.ts +8 -0
- package/dist/lint/post-synth/otel123.d.ts.map +1 -0
- package/dist/lint/post-synth/otel124.d.ts +8 -0
- package/dist/lint/post-synth/otel124.d.ts.map +1 -0
- package/dist/lint/post-synth/otel125.d.ts +8 -0
- package/dist/lint/post-synth/otel125.d.ts.map +1 -0
- package/dist/lint/post-synth/otel126.d.ts +8 -0
- package/dist/lint/post-synth/otel126.d.ts.map +1 -0
- package/dist/lint/post-synth/otel127.d.ts +8 -0
- package/dist/lint/post-synth/otel127.d.ts.map +1 -0
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/metric-names.d.ts +29 -0
- package/dist/metric-names.d.ts.map +1 -1
- package/dist/model.d.ts +24 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/okf/index.md +10 -0
- package/dist/okf/rules/OTEL118.md +11 -0
- package/dist/okf/rules/OTEL119.md +11 -0
- package/dist/okf/rules/OTEL120.md +11 -0
- package/dist/okf/rules/OTEL121.md +11 -0
- package/dist/okf/rules/OTEL122.md +11 -0
- package/dist/okf/rules/OTEL123.md +11 -0
- package/dist/okf/rules/OTEL124.md +11 -0
- package/dist/okf/rules/OTEL125.md +11 -0
- package/dist/okf/rules/OTEL126.md +11 -0
- package/dist/okf/rules/OTEL127.md +11 -0
- package/dist/platform.d.ts +11 -8
- package/dist/platform.d.ts.map +1 -1
- package/dist/rules/literal-credential.ts +2 -3
- package/dist/rules/otel-helpers.ts +15 -4
- package/dist/rules/otel118.ts +17 -0
- package/dist/rules/otel119.ts +17 -0
- package/dist/rules/otel120.ts +17 -0
- package/dist/rules/otel121.ts +17 -0
- package/dist/rules/otel122.ts +17 -0
- package/dist/rules/otel123.ts +17 -0
- package/dist/rules/otel124.ts +17 -0
- package/dist/rules/otel125.ts +17 -0
- package/dist/rules/otel126.ts +17 -0
- package/dist/rules/otel127.ts +17 -0
- package/dist/semconv.d.ts +2 -1
- package/dist/semconv.d.ts.map +1 -1
- package/dist/skills/chant-otel.md +6 -2
- package/dist/validate-config.d.ts +27 -9
- package/dist/validate-config.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/attribution.ts +385 -0
- package/src/codegen/docs.ts +8 -1
- package/src/components/k8s-receivers.test.ts +5 -2
- package/src/components/processors.ts +1 -1
- package/src/components/sampling.ts +3 -0
- package/src/composites/composites.test.ts +16 -0
- package/src/config-hygiene.ts +470 -0
- package/src/connectors.test.ts +3 -0
- package/src/define.ts +25 -4
- package/src/import/generator.test.ts +42 -4
- package/src/import/generator.ts +7 -2
- package/src/import/parser.test.ts +11 -0
- package/src/import/parser.ts +16 -4
- package/src/import/roundtrip.test.ts +90 -14
- package/src/index.ts +7 -0
- package/src/lint/audit-catalog.ts +87 -2
- package/src/lint/post-synth/hygiene.test.ts +334 -0
- package/src/lint/post-synth/index.ts +20 -0
- package/src/lint/post-synth/otel-helpers.ts +15 -4
- package/src/lint/post-synth/otel118.test.ts +308 -0
- package/src/lint/post-synth/otel118.ts +17 -0
- package/src/lint/post-synth/otel119.ts +17 -0
- package/src/lint/post-synth/otel120.ts +17 -0
- package/src/lint/post-synth/otel121.ts +17 -0
- package/src/lint/post-synth/otel122.ts +17 -0
- package/src/lint/post-synth/otel123.ts +17 -0
- package/src/lint/post-synth/otel124.ts +17 -0
- package/src/lint/post-synth/otel125.ts +17 -0
- package/src/lint/post-synth/otel126.ts +17 -0
- package/src/lint/post-synth/otel127.ts +17 -0
- package/src/lint/post-synth/post-synth.test.ts +18 -0
- package/src/lint/rules/literal-credential.ts +2 -3
- package/src/metric-names.test.ts +24 -2
- package/src/metric-names.ts +66 -1
- package/src/model.ts +48 -0
- package/src/platform.test.ts +35 -0
- package/src/platform.ts +28 -19
- package/src/plugin.test.ts +10 -0
- package/src/semconv.ts +6 -2
- package/src/skills/chant-otel.md +6 -2
- package/src/validate-config.ts +81 -16
package/src/metric-names.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/metric-names.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/platform.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
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
|
|
package/src/plugin.test.ts
CHANGED
|
@@ -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
|
|
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. */
|
package/src/skills/chant-otel.md
CHANGED
|
@@ -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
|
|
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.
|
package/src/validate-config.ts
CHANGED
|
@@ -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),
|
|
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
|
|
524
|
-
* from the table OTEL117 checks
|
|
525
|
-
* `
|
|
526
|
-
*
|
|
527
|
-
* publishes and the listeners OTEL117 compares
|
|
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
|
|
530
|
-
return (LISTENERS
|
|
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
|
-
|
|
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
|
|
627
|
+
out.push({ owner: `${kind} "${id}" (${spec.path.join(".")})`, component: id, ...address, transport: specTransport(spec, body) });
|
|
563
628
|
}
|
|
564
629
|
return out;
|
|
565
630
|
}
|