@intentius/chant-lexicon-otel 0.95.1 → 0.96.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 (107) hide show
  1. package/README.md +30 -9
  2. package/dist/catalog.d.ts +2 -1
  3. package/dist/catalog.d.ts.map +1 -1
  4. package/dist/codegen/docs.d.ts.map +1 -1
  5. package/dist/collector.d.ts +4 -1
  6. package/dist/collector.d.ts.map +1 -1
  7. package/dist/components/connectors.d.ts +156 -0
  8. package/dist/components/connectors.d.ts.map +1 -0
  9. package/dist/components/filtering.d.ts +95 -0
  10. package/dist/components/filtering.d.ts.map +1 -0
  11. package/dist/components/index.d.ts +4 -0
  12. package/dist/components/index.d.ts.map +1 -1
  13. package/dist/components/k8s-receivers.d.ts +74 -0
  14. package/dist/components/k8s-receivers.d.ts.map +1 -0
  15. package/dist/components/sampling.d.ts +260 -0
  16. package/dist/components/sampling.d.ts.map +1 -0
  17. package/dist/define.d.ts +31 -4
  18. package/dist/define.d.ts.map +1 -1
  19. package/dist/genai.d.ts +165 -0
  20. package/dist/genai.d.ts.map +1 -0
  21. package/dist/index.d.ts +5 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/integrity.json +8 -7
  24. package/dist/lint/audit-catalog.d.ts +2 -1
  25. package/dist/lint/audit-catalog.d.ts.map +1 -1
  26. package/dist/lint/post-synth/index.d.ts.map +1 -1
  27. package/dist/lint/post-synth/otel-helpers.d.ts +1 -1
  28. package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
  29. package/dist/lint/post-synth/otel101.d.ts +1 -1
  30. package/dist/lint/post-synth/otel103.d.ts +1 -1
  31. package/dist/lint/post-synth/otel112.d.ts +8 -0
  32. package/dist/lint/post-synth/otel112.d.ts.map +1 -0
  33. package/dist/manifest.json +1 -1
  34. package/dist/meta.json +70 -0
  35. package/dist/metric-names.d.ts +121 -0
  36. package/dist/metric-names.d.ts.map +1 -0
  37. package/dist/model.d.ts +23 -4
  38. package/dist/model.d.ts.map +1 -1
  39. package/dist/okf/index.md +15 -0
  40. package/dist/okf/rules/OTEL112.md +11 -0
  41. package/dist/okf/types/CountConnector.md +9 -0
  42. package/dist/okf/types/FilterProcessor.md +9 -0
  43. package/dist/okf/types/ForwardConnector.md +9 -0
  44. package/dist/okf/types/K8sClusterReceiver.md +9 -0
  45. package/dist/okf/types/KubeletStatsReceiver.md +9 -0
  46. package/dist/okf/types/LoadBalancingExporter.md +9 -0
  47. package/dist/okf/types/ProbabilisticSamplerProcessor.md +9 -0
  48. package/dist/okf/types/RedactionProcessor.md +9 -0
  49. package/dist/okf/types/RoutingConnector.md +9 -0
  50. package/dist/okf/types/ServiceGraphConnector.md +9 -0
  51. package/dist/okf/types/SpanMetricsConnector.md +9 -0
  52. package/dist/okf/types/SumConnector.md +9 -0
  53. package/dist/okf/types/TailSamplingProcessor.md +9 -0
  54. package/dist/okf/types/TransformProcessor.md +9 -0
  55. package/dist/pipeline.d.ts +9 -3
  56. package/dist/pipeline.d.ts.map +1 -1
  57. package/dist/plugin.d.ts +1 -1
  58. package/dist/rules/otel-helpers.ts +1 -1
  59. package/dist/rules/otel101.ts +1 -1
  60. package/dist/rules/otel103.ts +1 -1
  61. package/dist/rules/otel112.ts +17 -0
  62. package/dist/semconv.d.ts +33 -0
  63. package/dist/semconv.d.ts.map +1 -0
  64. package/dist/serializer.d.ts +1 -1
  65. package/dist/skills/chant-otel.md +28 -5
  66. package/dist/topology.d.ts +34 -5
  67. package/dist/topology.d.ts.map +1 -1
  68. package/dist/validate-config.d.ts +6 -2
  69. package/dist/validate-config.d.ts.map +1 -1
  70. package/dist/validate.d.ts.map +1 -1
  71. package/package.json +3 -3
  72. package/src/catalog.ts +2 -1
  73. package/src/codegen/docs.ts +14 -8
  74. package/src/collector.ts +9 -1
  75. package/src/components/connectors.ts +276 -0
  76. package/src/components/filtering.test.ts +192 -0
  77. package/src/components/filtering.ts +192 -0
  78. package/src/components/index.ts +4 -0
  79. package/src/components/k8s-receivers.test.ts +123 -0
  80. package/src/components/k8s-receivers.ts +107 -0
  81. package/src/components/sampling.test.ts +464 -0
  82. package/src/components/sampling.ts +521 -0
  83. package/src/connectors.test.ts +388 -0
  84. package/src/define.ts +36 -4
  85. package/src/genai.test.ts +526 -0
  86. package/src/genai.ts +466 -0
  87. package/src/generated/lexicon-otel.json +70 -0
  88. package/src/index.ts +39 -0
  89. package/src/lint/audit-catalog.ts +11 -2
  90. package/src/lint/post-synth/index.ts +2 -0
  91. package/src/lint/post-synth/otel-helpers.ts +1 -1
  92. package/src/lint/post-synth/otel101.ts +1 -1
  93. package/src/lint/post-synth/otel103.ts +1 -1
  94. package/src/lint/post-synth/otel112.ts +17 -0
  95. package/src/metric-names.test.ts +80 -0
  96. package/src/metric-names.ts +176 -0
  97. package/src/model.ts +26 -4
  98. package/src/otelcol-validate.test.ts +143 -0
  99. package/src/pipeline.ts +9 -3
  100. package/src/plugin.test.ts +1 -0
  101. package/src/plugin.ts +1 -1
  102. package/src/semconv.ts +63 -0
  103. package/src/serializer.ts +1 -1
  104. package/src/skills/chant-otel.md +28 -5
  105. package/src/topology.ts +54 -8
  106. package/src/validate-config.ts +92 -4
  107. package/src/validate.ts +8 -0
@@ -0,0 +1,521 @@
1
+ /**
2
+ * Built-in sampling components: the tail_sampling and probabilistic_sampler
3
+ * processors, and the loadbalancing exporter that routes every span of a trace
4
+ * to the same collector so tail sampling sees whole traces.
5
+ *
6
+ * Field shapes follow `processor/tailsamplingprocessor`,
7
+ * `processor/probabilisticsamplerprocessor` and
8
+ * `exporter/loadbalancingexporter` at `COLLECTOR_PIN`.
9
+ */
10
+
11
+ import { defineBuiltin } from "../define";
12
+ import type { Duration, ExporterHelperSettings, GRPCClientSettings } from "./common";
13
+
14
+ // ── tail_sampling ────────────────────────────────────────────────────
15
+
16
+ /** Samples a trace whose duration is at least `threshold_ms` (and below `upper_threshold_ms`, when set). */
17
+ export interface LatencyPolicyConfig {
18
+ threshold_ms: number;
19
+ upper_threshold_ms?: number;
20
+ }
21
+
22
+ /** Samples a trace with a span whose integer attribute `key` falls within `min_value` to `max_value`. */
23
+ export interface NumericAttributePolicyConfig {
24
+ key: string;
25
+ min_value?: number;
26
+ max_value?: number;
27
+ invert_match?: boolean;
28
+ }
29
+
30
+ /** Samples a fixed percentage of traces by trace id hash. */
31
+ export interface ProbabilisticPolicyConfig {
32
+ sampling_percentage: number;
33
+ hash_salt?: string;
34
+ }
35
+
36
+ export type SpanStatusCode = "OK" | "ERROR" | "UNSET";
37
+
38
+ /** Samples a trace with a span whose status is one of `status_codes`. */
39
+ export interface StatusCodePolicyConfig {
40
+ status_codes: SpanStatusCode[];
41
+ }
42
+
43
+ /** Samples a trace with a span whose string attribute `key` matches one of `values`. */
44
+ export interface StringAttributePolicyConfig {
45
+ key: string;
46
+ values: string[];
47
+ enabled_regex_matching?: boolean;
48
+ cache_max_size?: number;
49
+ invert_match?: boolean;
50
+ }
51
+
52
+ /** Samples traces until `spans_per_second` is reached. */
53
+ export interface RateLimitingPolicyConfig {
54
+ spans_per_second: number;
55
+ }
56
+
57
+ /** Samples a trace whose span count falls within `min_spans` to `max_spans`. */
58
+ export interface SpanCountPolicyConfig {
59
+ min_spans: number;
60
+ max_spans?: number;
61
+ }
62
+
63
+ /** Samples a trace whose W3C trace state key `key` has one of `values`. */
64
+ export interface TraceStatePolicyConfig {
65
+ key: string;
66
+ values: string[];
67
+ }
68
+
69
+ /** Samples a trace with a span whose boolean attribute `key` equals `value`. */
70
+ export interface BooleanAttributePolicyConfig {
71
+ key: string;
72
+ value: boolean;
73
+ invert_match?: boolean;
74
+ }
75
+
76
+ /** Samples a trace when any OTTL condition on a span or span event is true. */
77
+ export interface OttlConditionPolicyConfig {
78
+ error_mode?: "ignore" | "silent" | "propagate";
79
+ span?: string[];
80
+ spanevent?: string[];
81
+ }
82
+
83
+ interface PolicyBase<T extends string> {
84
+ /** Unique within its list; the collector refuses an empty or repeated name. */
85
+ name: string;
86
+ type: T;
87
+ }
88
+
89
+ export interface AlwaysSamplePolicy extends PolicyBase<"always_sample"> {}
90
+ export interface LatencyPolicy extends PolicyBase<"latency"> {
91
+ latency: LatencyPolicyConfig;
92
+ }
93
+ export interface NumericAttributePolicy extends PolicyBase<"numeric_attribute"> {
94
+ numeric_attribute: NumericAttributePolicyConfig;
95
+ }
96
+ export interface ProbabilisticPolicy extends PolicyBase<"probabilistic"> {
97
+ probabilistic: ProbabilisticPolicyConfig;
98
+ }
99
+ export interface StatusCodePolicy extends PolicyBase<"status_code"> {
100
+ status_code: StatusCodePolicyConfig;
101
+ }
102
+ export interface StringAttributePolicy extends PolicyBase<"string_attribute"> {
103
+ string_attribute: StringAttributePolicyConfig;
104
+ }
105
+ export interface RateLimitingPolicy extends PolicyBase<"rate_limiting"> {
106
+ rate_limiting: RateLimitingPolicyConfig;
107
+ }
108
+ export interface SpanCountPolicy extends PolicyBase<"span_count"> {
109
+ span_count: SpanCountPolicyConfig;
110
+ }
111
+ export interface TraceStatePolicy extends PolicyBase<"trace_state"> {
112
+ trace_state: TraceStatePolicyConfig;
113
+ }
114
+ export interface BooleanAttributePolicy extends PolicyBase<"boolean_attribute"> {
115
+ boolean_attribute: BooleanAttributePolicyConfig;
116
+ }
117
+ export interface OttlConditionPolicy extends PolicyBase<"ottl_condition"> {
118
+ ottl_condition: OttlConditionPolicyConfig;
119
+ }
120
+
121
+ /** A policy that decides on its own: every type except `and`, `composite` and `drop`. Also the shape of an `and` sub-policy. */
122
+ export type TailSamplingLeafPolicy =
123
+ | AlwaysSamplePolicy
124
+ | LatencyPolicy
125
+ | NumericAttributePolicy
126
+ | ProbabilisticPolicy
127
+ | StatusCodePolicy
128
+ | StringAttributePolicy
129
+ | RateLimitingPolicy
130
+ | SpanCountPolicy
131
+ | TraceStatePolicy
132
+ | BooleanAttributePolicy
133
+ | OttlConditionPolicy;
134
+
135
+ /** Samples a trace only when every sub-policy samples it. */
136
+ export interface AndPolicy extends PolicyBase<"and"> {
137
+ and: { and_sub_policy: TailSamplingLeafPolicy[] };
138
+ }
139
+
140
+ /** Drops a trace when every sub-policy matches it, whatever the other policies decide. */
141
+ export interface DropPolicy extends PolicyBase<"drop"> {
142
+ drop: { drop_sub_policy: TailSamplingLeafPolicy[] };
143
+ }
144
+
145
+ /** A composite sub-policy: a leaf policy or an `and`. */
146
+ export type CompositeSubPolicy = TailSamplingLeafPolicy | AndPolicy;
147
+
148
+ /**
149
+ * Shares `max_total_spans_per_second` between sub-policies. Each
150
+ * `rate_allocation` entry names a sub-policy and its percent of the budget;
151
+ * `policy_order` is the order sub-policies are evaluated in.
152
+ */
153
+ export interface CompositePolicy extends PolicyBase<"composite"> {
154
+ composite: {
155
+ max_total_spans_per_second: number;
156
+ policy_order: string[];
157
+ composite_sub_policy: CompositeSubPolicy[];
158
+ rate_allocation?: Array<{ policy: string; percent: number }>;
159
+ };
160
+ }
161
+
162
+ /** One entry in `tail_sampling.policies`, discriminated by `type`. The config sits under the key named by `type`. */
163
+ export type TailSamplingPolicy = TailSamplingLeafPolicy | AndPolicy | DropPolicy | CompositePolicy;
164
+
165
+ export type TailSamplingPolicyType = TailSamplingPolicy["type"];
166
+
167
+ export interface TailSamplingProcessorConfig {
168
+ /** How long after a trace's first span the decision is made. The collector's default is 30s; chant asks for it explicitly. */
169
+ decision_wait: Duration;
170
+ /** Policies, evaluated for every trace. A trace is sampled when any policy samples it and no `drop` policy drops it. */
171
+ policies: TailSamplingPolicy[];
172
+ num_traces?: number;
173
+ expected_new_traces_per_sec?: number;
174
+ decision_cache?: {
175
+ sampled_cache_size?: number;
176
+ non_sampled_cache_size?: number;
177
+ };
178
+ sample_on_first_match?: boolean;
179
+ }
180
+
181
+ /**
182
+ * Holds each trace for `decision_wait`, then samples it when a policy says so.
183
+ * Tail sampling needs every span of a trace in one collector: run it on a
184
+ * gateway behind the `loadbalancing` exporter, not on a per-node agent.
185
+ */
186
+ export const TailSamplingProcessor = defineBuiltin<TailSamplingProcessorConfig, "processor", "tail_sampling">({
187
+ kind: "processor",
188
+ type: "tail_sampling",
189
+ description: "Samples whole traces by policy after waiting for their spans: errors, latency, attributes, rate",
190
+ validate: (c) => {
191
+ const problems: string[] = [];
192
+ if (!c.decision_wait) problems.push("decision_wait is not set; declare how long to wait for a trace's spans");
193
+ if (!Array.isArray(c.policies) || c.policies.length === 0) {
194
+ problems.push("policies is empty, so no trace is ever sampled");
195
+ } else {
196
+ policyListProblems(c.policies, "policies", problems);
197
+ }
198
+ return problems;
199
+ },
200
+ });
201
+
202
+ const STATUS_CODES: ReadonlySet<string> = new Set(["OK", "ERROR", "UNSET"]);
203
+
204
+ function policyListProblems(policies: ReadonlyArray<TailSamplingPolicy>, path: string, problems: string[]): void {
205
+ const seen = new Set<string>();
206
+ policies.forEach((p, i) => {
207
+ const at = `${path}[${i}]`;
208
+ if (!p || typeof p !== "object") {
209
+ problems.push(`${at} is not a policy`);
210
+ return;
211
+ }
212
+ if (!p.name) problems.push(`${at} has no name`);
213
+ else if (seen.has(p.name)) problems.push(`${at}: policy name "${p.name}" is repeated`);
214
+ else seen.add(p.name);
215
+ const label = p.name ? `${at} (${p.name})` : at;
216
+ policyProblems(p, label, problems);
217
+ });
218
+ }
219
+
220
+ function policyProblems(p: TailSamplingPolicy, at: string, problems: string[]): void {
221
+ const cfg = (p as unknown as Record<string, unknown>)[p.type];
222
+ if (p.type !== "always_sample" && (typeof cfg !== "object" || cfg === null)) {
223
+ problems.push(`${at}: type ${p.type} needs a ${p.type} block`);
224
+ return;
225
+ }
226
+ switch (p.type) {
227
+ case "latency":
228
+ if (!(p.latency.threshold_ms > 0) && !(p.latency.upper_threshold_ms && p.latency.upper_threshold_ms > 0)) {
229
+ problems.push(`${at}: latency needs threshold_ms or upper_threshold_ms above 0`);
230
+ }
231
+ if (p.latency.upper_threshold_ms !== undefined && p.latency.upper_threshold_ms <= p.latency.threshold_ms) {
232
+ problems.push(`${at}: latency upper_threshold_ms must be above threshold_ms`);
233
+ }
234
+ break;
235
+ case "numeric_attribute":
236
+ if (!p.numeric_attribute.key) problems.push(`${at}: numeric_attribute needs a key`);
237
+ if (
238
+ p.numeric_attribute.min_value !== undefined &&
239
+ p.numeric_attribute.max_value !== undefined &&
240
+ p.numeric_attribute.max_value < p.numeric_attribute.min_value
241
+ ) {
242
+ problems.push(`${at}: numeric_attribute max_value is below min_value`);
243
+ }
244
+ break;
245
+ case "probabilistic": {
246
+ const pct = p.probabilistic.sampling_percentage;
247
+ if (typeof pct !== "number" || pct <= 0 || pct > 100) {
248
+ problems.push(`${at}: probabilistic sampling_percentage must be above 0 and at most 100`);
249
+ }
250
+ break;
251
+ }
252
+ case "status_code": {
253
+ const codes = p.status_code.status_codes ?? [];
254
+ if (codes.length === 0) problems.push(`${at}: status_code needs at least one status code`);
255
+ for (const code of codes) {
256
+ if (!STATUS_CODES.has(code)) problems.push(`${at}: status code "${code}" is not OK, ERROR or UNSET`);
257
+ }
258
+ break;
259
+ }
260
+ case "string_attribute":
261
+ if (!p.string_attribute.key) problems.push(`${at}: string_attribute needs a key`);
262
+ if (!p.string_attribute.values?.length) problems.push(`${at}: string_attribute needs at least one value`);
263
+ break;
264
+ case "rate_limiting":
265
+ if (!(p.rate_limiting.spans_per_second > 0)) problems.push(`${at}: rate_limiting spans_per_second must be above 0`);
266
+ break;
267
+ case "span_count":
268
+ if (p.span_count.max_spans !== undefined && p.span_count.max_spans < p.span_count.min_spans) {
269
+ problems.push(`${at}: span_count max_spans is below min_spans`);
270
+ }
271
+ break;
272
+ case "trace_state":
273
+ if (!p.trace_state.key) problems.push(`${at}: trace_state needs a key`);
274
+ if (!p.trace_state.values?.length) problems.push(`${at}: trace_state needs at least one value`);
275
+ break;
276
+ case "boolean_attribute":
277
+ if (!p.boolean_attribute.key) problems.push(`${at}: boolean_attribute needs a key`);
278
+ break;
279
+ case "ottl_condition":
280
+ if (!p.ottl_condition.span?.length && !p.ottl_condition.spanevent?.length) {
281
+ problems.push(`${at}: ottl_condition needs a span or spanevent condition`);
282
+ }
283
+ break;
284
+ case "and":
285
+ subPolicyProblems(p.and.and_sub_policy, `${at}.and.and_sub_policy`, problems);
286
+ break;
287
+ case "drop":
288
+ subPolicyProblems(p.drop.drop_sub_policy, `${at}.drop.drop_sub_policy`, problems);
289
+ break;
290
+ case "composite": {
291
+ const comp = p.composite;
292
+ if (!(comp.max_total_spans_per_second > 0)) problems.push(`${at}: composite max_total_spans_per_second must be above 0`);
293
+ const subs = comp.composite_sub_policy ?? [];
294
+ if (subs.length === 0) {
295
+ problems.push(`${at}: composite has no composite_sub_policy`);
296
+ break;
297
+ }
298
+ policyListProblems(subs, `${at}.composite.composite_sub_policy`, problems);
299
+ const names = new Set(subs.map((s) => s.name));
300
+ for (const n of comp.policy_order ?? []) {
301
+ if (!names.has(n)) problems.push(`${at}: policy_order names "${n}", which is not a composite_sub_policy`);
302
+ }
303
+ let total = 0;
304
+ for (const r of comp.rate_allocation ?? []) {
305
+ if (!names.has(r.policy))
306
+ problems.push(`${at}: rate_allocation names "${r.policy}", which is not a composite_sub_policy`);
307
+ total += r.percent;
308
+ }
309
+ if (total > 100) problems.push(`${at}: rate_allocation percents add up to ${total}, above 100`);
310
+ break;
311
+ }
312
+ default:
313
+ break;
314
+ }
315
+ }
316
+
317
+ function subPolicyProblems(subs: ReadonlyArray<TailSamplingPolicy> | undefined, path: string, problems: string[]): void {
318
+ if (!subs || subs.length === 0) {
319
+ problems.push(`${path} is empty`);
320
+ return;
321
+ }
322
+ policyListProblems(subs, path, problems);
323
+ }
324
+
325
+ // ── probabilistic_sampler ────────────────────────────────────────────
326
+
327
+ export interface ProbabilisticSamplerProcessorConfig {
328
+ /** Percent of traces or logs kept, 0 to 100. The collector's default is 0, which keeps nothing, so chant asks for it. */
329
+ sampling_percentage: number;
330
+ /** Salt for `hash_seed` mode. Collectors that sample the same data must share it. */
331
+ hash_seed?: number;
332
+ /** `hash_seed` (the default for logs), `proportional` (the default for traces) or `equalizing`. */
333
+ mode?: "hash_seed" | "proportional" | "equalizing";
334
+ /** Default true: an item without usable randomness is dropped. */
335
+ fail_closed?: boolean;
336
+ /** Hex digits of threshold precision, 1 to 14; default 4. */
337
+ sampling_precision?: number;
338
+ /** Logs only: sample on the trace id (default) or on `from_attribute` of the record. */
339
+ attribute_source?: "traceID" | "record";
340
+ from_attribute?: string;
341
+ /** Logs only: an attribute whose value overrides the sampling percentage for its record. */
342
+ sampling_priority?: string;
343
+ }
344
+
345
+ /** Head sampling: keeps a fixed percentage of traces or logs, decided per item without waiting for the rest of the trace. */
346
+ export const ProbabilisticSamplerProcessor = defineBuiltin<
347
+ ProbabilisticSamplerProcessorConfig,
348
+ "processor",
349
+ "probabilistic_sampler"
350
+ >({
351
+ kind: "processor",
352
+ type: "probabilistic_sampler",
353
+ description: "Head sampling: keeps a fixed percentage of traces or logs",
354
+ validate: (c) => {
355
+ const problems: string[] = [];
356
+ const pct = c.sampling_percentage;
357
+ if (typeof pct !== "number" || Number.isNaN(pct)) {
358
+ problems.push("sampling_percentage is not set; the collector's default of 0 keeps nothing");
359
+ } else if (pct < 0 || pct > 100) {
360
+ problems.push(`sampling_percentage (${pct}) must be between 0 and 100`);
361
+ }
362
+ if (c.sampling_precision !== undefined && (c.sampling_precision < 1 || c.sampling_precision > 14)) {
363
+ problems.push(`sampling_precision (${c.sampling_precision}) must be between 1 and 14`);
364
+ }
365
+ if (c.attribute_source === "record" && !c.from_attribute) {
366
+ problems.push("attribute_source is record but from_attribute is not set");
367
+ }
368
+ if (c.hash_seed !== undefined && c.mode !== undefined && c.mode !== "hash_seed") {
369
+ problems.push(`hash_seed is only read in hash_seed mode, not ${c.mode}`);
370
+ }
371
+ return problems;
372
+ },
373
+ });
374
+
375
+ // ── loadbalancing ────────────────────────────────────────────────────
376
+
377
+ /** A fixed list of backends, each `host` or `host:port` (port 4317 when omitted). */
378
+ export interface StaticResolverConfig {
379
+ hostnames: string[];
380
+ }
381
+
382
+ /** Backends found by resolving a DNS name, e.g. a headless Service. */
383
+ export interface DnsResolverConfig {
384
+ hostname: string;
385
+ /** Default `4317`. */
386
+ port?: string;
387
+ interval?: Duration;
388
+ timeout?: Duration;
389
+ }
390
+
391
+ /** Backends found by watching a Kubernetes Service's endpoints. Needs RBAC to list and watch EndpointSlices. */
392
+ export interface K8sResolverConfig {
393
+ /** `name` or `name.namespace`. */
394
+ service: string;
395
+ /** Default `[4317]`. */
396
+ ports?: number[];
397
+ timeout?: Duration;
398
+ /** Use pod hostnames instead of IPs, e.g. for a StatefulSet behind a headless Service. */
399
+ return_hostnames?: boolean;
400
+ }
401
+
402
+ /** Backends found in AWS Cloud Map. */
403
+ export interface AwsCloudMapResolverConfig {
404
+ namespace: string;
405
+ service_name: string;
406
+ health_status?: "HEALTHY" | "UNHEALTHY" | "ALL" | "HEALTHY_OR_ELSE_ALL";
407
+ interval?: Duration;
408
+ timeout?: Duration;
409
+ port?: number;
410
+ }
411
+
412
+ /** Exactly one resolver; the collector refuses none or several. */
413
+ export type LoadBalancingResolver =
414
+ | {
415
+ static: StaticResolverConfig;
416
+ dns?: never;
417
+ k8s?: never;
418
+ aws_cloud_map?: never;
419
+ }
420
+ | {
421
+ dns: DnsResolverConfig;
422
+ static?: never;
423
+ k8s?: never;
424
+ aws_cloud_map?: never;
425
+ }
426
+ | {
427
+ k8s: K8sResolverConfig;
428
+ static?: never;
429
+ dns?: never;
430
+ aws_cloud_map?: never;
431
+ }
432
+ | {
433
+ aws_cloud_map: AwsCloudMapResolverConfig;
434
+ static?: never;
435
+ dns?: never;
436
+ k8s?: never;
437
+ };
438
+
439
+ /**
440
+ * `traceID` (the default for traces and logs), `service` or `attributes` for
441
+ * traces; `service` (the default for metrics), `resource`, `metric` or
442
+ * `streamID` for metrics. Logs always route by trace id.
443
+ */
444
+ export type LoadBalancingRoutingKey = "traceID" | "service" | "attributes" | "resource" | "metric" | "streamID";
445
+
446
+ /** The otlp exporter settings used for every backend. The endpoint comes from the resolver, so it is not set here. */
447
+ export type LoadBalancingOtlpConfig = Omit<GRPCClientSettings, "endpoint"> & ExporterHelperSettings;
448
+
449
+ export type LoadBalancingExporterConfig = ExporterHelperSettings & {
450
+ protocol?: { otlp: LoadBalancingOtlpConfig };
451
+ resolver: LoadBalancingResolver;
452
+ routing_key?: LoadBalancingRoutingKey;
453
+ /** With `routing_key: attributes`, the span attributes to route on. */
454
+ routing_attributes?: string[];
455
+ };
456
+
457
+ const RESOLVER_KEYS = ["static", "dns", "k8s", "aws_cloud_map"] as const;
458
+
459
+ /**
460
+ * Sends each trace (or service, or metric stream) to one backend chosen by a
461
+ * consistent hash over the resolver's backends. Put it on the agents in front
462
+ * of a multi-replica gateway that runs `tail_sampling`.
463
+ */
464
+ export const LoadBalancingExporter = defineBuiltin<LoadBalancingExporterConfig, "exporter", "loadbalancing">({
465
+ kind: "exporter",
466
+ type: "loadbalancing",
467
+ description: "Routes all spans of a trace to one backend collector by trace id, for tail sampling behind it",
468
+ validate: (c) => {
469
+ const problems: string[] = [];
470
+ const r = (c.resolver ?? {}) as Record<string, unknown>;
471
+ const set = RESOLVER_KEYS.filter((k) => r[k] !== undefined && r[k] !== null);
472
+ if (set.length === 0) problems.push("resolver has none of static, dns, k8s or aws_cloud_map");
473
+ if (set.length > 1) problems.push(`resolver sets ${set.join(" and ")}; the collector takes exactly one`);
474
+ const res = c.resolver as unknown as
475
+ | Partial<{
476
+ static: StaticResolverConfig;
477
+ dns: DnsResolverConfig;
478
+ k8s: K8sResolverConfig;
479
+ aws_cloud_map: AwsCloudMapResolverConfig;
480
+ }>
481
+ | undefined;
482
+ if (res?.static && !res.static.hostnames?.length) problems.push("resolver.static.hostnames is empty");
483
+ if (res?.dns && !res.dns.hostname) problems.push("resolver.dns.hostname is empty");
484
+ if (res?.k8s) {
485
+ if (!res.k8s.service) problems.push("resolver.k8s.service is empty");
486
+ if (res.k8s.ports && res.k8s.ports.length === 0) problems.push("resolver.k8s.ports is empty; omit it for the default 4317");
487
+ }
488
+ if (res?.aws_cloud_map && (!res.aws_cloud_map.namespace || !res.aws_cloud_map.service_name)) {
489
+ problems.push("resolver.aws_cloud_map needs namespace and service_name");
490
+ }
491
+ if (c.routing_key === "attributes" && !c.routing_attributes?.length) {
492
+ problems.push("routing_key is attributes but routing_attributes is empty");
493
+ }
494
+ if (c.routing_attributes?.length && c.routing_key !== "attributes") {
495
+ problems.push("routing_attributes is only read with routing_key: attributes");
496
+ }
497
+ const otlp = c.protocol?.otlp as Record<string, unknown> | undefined;
498
+ if (otlp && "endpoint" in otlp) problems.push("protocol.otlp.endpoint is ignored; backends come from the resolver");
499
+ return problems;
500
+ },
501
+ endpoints: (c) => loadBalancingEndpoints(c.resolver),
502
+ });
503
+
504
+ /**
505
+ * The backends a `loadbalancing` resolver names, as `host:port` where it can:
506
+ * static hostnames as given, a DNS name with its port, a Kubernetes Service
507
+ * once per port, a Cloud Map service as `aws-cloud-map://namespace/service`.
508
+ */
509
+ export function loadBalancingEndpoints(resolver: LoadBalancingResolver | undefined): string[] {
510
+ if (!resolver) return [];
511
+ if (resolver.static) return [...(resolver.static.hostnames ?? [])];
512
+ if (resolver.dns) return resolver.dns.hostname ? [`${resolver.dns.hostname}:${resolver.dns.port ?? "4317"}`] : [];
513
+ if (resolver.k8s) {
514
+ if (!resolver.k8s.service) return [];
515
+ return (resolver.k8s.ports?.length ? resolver.k8s.ports : [4317]).map((p) => `${resolver.k8s!.service}:${p}`);
516
+ }
517
+ if (resolver.aws_cloud_map) {
518
+ return [`aws-cloud-map://${resolver.aws_cloud_map.namespace}/${resolver.aws_cloud_map.service_name}`];
519
+ }
520
+ return [];
521
+ }