@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
package/src/genai.ts ADDED
@@ -0,0 +1,466 @@
1
+ /**
2
+ * A collector preset for workloads that emit OpenTelemetry GenAI spans:
3
+ * content removal, and agent RED and token metrics.
4
+ *
5
+ * Attribute keys follow `GENAI_SEMCONV_PIN`. Content (prompts, completions,
6
+ * system instructions, tool-call arguments and results, retrieval queries and
7
+ * documents) is deleted from spans, span events and log records unless the
8
+ * caller passes `keepContent: true`. A `transform` processor deletes the keys;
9
+ * `redaction` cannot delete one named key while keeping the rest, so it runs
10
+ * after the transform as a masking backstop on the same keys and for any
11
+ * value patterns the caller adds.
12
+ *
13
+ * Metrics come from a branch taken before any sampling: the traces pipeline
14
+ * forwards every span to `traces/genai`, which keeps only spans with a
15
+ * `gen_ai.operation.name`, and feeds `spanmetrics` (calls, errors and
16
+ * duration by operation, model, tool and error type) and `sum` (input and
17
+ * output tokens by model). `spanmetrics` counts spans and
18
+ * cannot sum an attribute, which is why token usage comes from the `sum`
19
+ * connector.
20
+ */
21
+
22
+ import type { Declarable } from "@intentius/chant/declarable";
23
+ import { GENAI_SEMCONV_PIN, type OTelComponent, type SchemaPin } from "./define";
24
+ import { OtlpReceiver } from "./components/receivers";
25
+ import { BatchProcessor, MemoryLimiterProcessor } from "./components/processors";
26
+ import { DebugExporter } from "./components/exporters";
27
+ import { HealthCheckExtension } from "./components/extensions";
28
+ import {
29
+ FilterProcessor,
30
+ RedactionProcessor,
31
+ TransformProcessor,
32
+ type FilterProcessorConfig,
33
+ type RedactionProcessorConfig,
34
+ type TransformProcessorConfig,
35
+ type TransformStatementGroup,
36
+ } from "./components/filtering";
37
+ import {
38
+ ForwardConnector,
39
+ SpanMetricsConnector,
40
+ SumConnector,
41
+ type ForwardConnectorConfig,
42
+ type SpanMetricsConnectorConfig,
43
+ type SpanMetricsDimension,
44
+ type SumConnectorConfig,
45
+ } from "./components/connectors";
46
+ import type { Duration } from "./components/common";
47
+ import { Pipeline } from "./pipeline";
48
+ import { prometheusMetricName, SPANMETRICS_DEFAULT_DIMENSIONS, type CollectorMetric } from "./metric-names";
49
+
50
+ // ── The GenAI attribute vocabulary, at GENAI_SEMCONV_PIN ─────────────
51
+
52
+ /** GenAI attributes the preset reads. */
53
+ export const GENAI_ATTRIBUTES = Object.freeze({
54
+ operationName: "gen_ai.operation.name",
55
+ providerName: "gen_ai.provider.name",
56
+ requestModel: "gen_ai.request.model",
57
+ responseModel: "gen_ai.response.model",
58
+ agentName: "gen_ai.agent.name",
59
+ toolName: "gen_ai.tool.name",
60
+ inputTokens: "gen_ai.usage.input_tokens",
61
+ outputTokens: "gen_ai.usage.output_tokens",
62
+ /** From the general conventions; GenAI spans set it on failure. */
63
+ errorType: "error.type",
64
+ } as const);
65
+
66
+ /**
67
+ * Attributes that carry content: what users and models said, and what tools
68
+ * were given and returned. The conventions mark each opt-in and sensitive.
69
+ * `gen_ai.prompt` and `gen_ai.completion` are deprecated, but older
70
+ * instrumentation still sets them.
71
+ */
72
+ export const GENAI_CONTENT_ATTRIBUTES: readonly string[] = Object.freeze([
73
+ "gen_ai.system_instructions",
74
+ "gen_ai.input.messages",
75
+ "gen_ai.output.messages",
76
+ "gen_ai.tool.call.arguments",
77
+ "gen_ai.tool.call.result",
78
+ "gen_ai.retrieval.query.text",
79
+ "gen_ai.retrieval.documents",
80
+ "gen_ai.prompt",
81
+ "gen_ai.completion",
82
+ ]);
83
+
84
+ /**
85
+ * Indexed content keys some instrumentation libraries write outside the
86
+ * conventions (`gen_ai.prompt.0.content`, `gen_ai.completion.1.role`). The
87
+ * index keeps `gen_ai.prompt.name`, a convention attribute, out of it.
88
+ */
89
+ export const GENAI_INDEXED_CONTENT_PATTERN = "^gen_ai\\.(prompt|completion)\\.[0-9]+\\..+$";
90
+
91
+ /**
92
+ * Deprecated content events. Their content is in the log record body, not in
93
+ * attributes, so the preset deletes `content`, `message` and `tool_calls`
94
+ * from the body of these events.
95
+ */
96
+ export const GENAI_CONTENT_EVENTS: readonly string[] = Object.freeze([
97
+ "gen_ai.system.message",
98
+ "gen_ai.user.message",
99
+ "gen_ai.assistant.message",
100
+ "gen_ai.tool.message",
101
+ "gen_ai.choice",
102
+ ]);
103
+
104
+ // ── Metrics ──────────────────────────────────────────────────────────
105
+
106
+ /** The spanmetrics dimensions the preset adds to service.name, span.name, span.kind and status.code. */
107
+ export const GENAI_SPAN_METRIC_DIMENSIONS: readonly string[] = Object.freeze([
108
+ GENAI_ATTRIBUTES.operationName,
109
+ GENAI_ATTRIBUTES.requestModel,
110
+ GENAI_ATTRIBUTES.toolName,
111
+ GENAI_ATTRIBUTES.errorType,
112
+ ]);
113
+
114
+ /**
115
+ * The attribute token sums are split by. One only: the pinned `sum`
116
+ * connector adds each value once per attribute, so a second one would double
117
+ * every sum.
118
+ */
119
+ export const GENAI_TOKEN_DIMENSIONS: readonly string[] = Object.freeze([GENAI_ATTRIBUTES.requestModel]);
120
+
121
+ /** The model label on token sums for a span that names no model, so its tokens are counted rather than skipped. */
122
+ export const GENAI_UNKNOWN_MODEL = "unknown";
123
+
124
+ /** Default duration buckets, sized for model and tool calls rather than HTTP handlers. */
125
+ export const GENAI_DURATION_BUCKETS: readonly Duration[] = Object.freeze([
126
+ "100ms",
127
+ "250ms",
128
+ "500ms",
129
+ "1s",
130
+ "2s",
131
+ "5s",
132
+ "10s",
133
+ "20s",
134
+ "40s",
135
+ "80s",
136
+ ]);
137
+
138
+ /** One metric the preset emits, as the collector names it and as Prometheus exposes it. */
139
+ export type GenAiMetric = CollectorMetric;
140
+
141
+ export interface GenAiMetrics {
142
+ /** Span count, with `status.code` = `STATUS_CODE_ERROR` for errors. */
143
+ calls: GenAiMetric;
144
+ duration: GenAiMetric;
145
+ inputTokens: GenAiMetric;
146
+ outputTokens: GenAiMetric;
147
+ }
148
+
149
+ export interface GenAiMetricsOptions {
150
+ /** Prefix of the emitted metric names. Default `genai`, outside the conventions' own `gen_ai` namespace. */
151
+ namespace?: string;
152
+ /** Dimensions added to the span metrics beyond the GenAI ones. */
153
+ dimensions?: SpanMetricsDimension[];
154
+ }
155
+
156
+ const DEFAULT_NAMESPACE = "genai";
157
+
158
+ /**
159
+ * The metrics `genAiPipeline()` emits for the given options: names,
160
+ * Prometheus names and dimensions. A dashboard reads this instead of
161
+ * repeating the names, so renaming the namespace moves the panels with it.
162
+ */
163
+ export function genAiMetrics(options: GenAiMetricsOptions = {}): GenAiMetrics {
164
+ const ns = options.namespace ?? DEFAULT_NAMESPACE;
165
+ const spanDims = [
166
+ ...SPANMETRICS_DEFAULT_DIMENSIONS,
167
+ ...GENAI_SPAN_METRIC_DIMENSIONS,
168
+ ...(options.dimensions ?? []).map((d) => d.name),
169
+ ];
170
+ const tokenDims = [...GENAI_TOKEN_DIMENSIONS];
171
+ return {
172
+ calls: { name: `${ns}.calls`, prometheus: prometheusMetricName(`${ns}.calls`, "sum"), type: "sum", dimensions: spanDims },
173
+ duration: {
174
+ name: `${ns}.duration`,
175
+ prometheus: prometheusMetricName(`${ns}.duration`, "histogram", "s"),
176
+ type: "histogram",
177
+ unit: "s",
178
+ dimensions: spanDims,
179
+ },
180
+ inputTokens: {
181
+ name: `${ns}.tokens.input`,
182
+ prometheus: prometheusMetricName(`${ns}.tokens.input`, "sum"),
183
+ type: "sum",
184
+ dimensions: tokenDims,
185
+ },
186
+ outputTokens: {
187
+ name: `${ns}.tokens.output`,
188
+ prometheus: prometheusMetricName(`${ns}.tokens.output`, "sum"),
189
+ type: "sum",
190
+ dimensions: tokenDims,
191
+ },
192
+ };
193
+ }
194
+
195
+ // ── Components ───────────────────────────────────────────────────────
196
+
197
+ type Exporter = OTelComponent<"exporter", string, any>;
198
+ type Processor = OTelComponent<"processor", string, any>;
199
+
200
+ export interface GenAiComponentsOptions extends GenAiMetricsOptions {
201
+ /**
202
+ * Keep content attributes and bodies. Default false: they are deleted.
203
+ * Setting this is the one way to keep prompts and completions, so a config
204
+ * that keeps them says so where it is declared.
205
+ */
206
+ keepContent?: boolean;
207
+ /** More attribute keys that hold content, deleted along with the convention ones. */
208
+ contentAttributes?: string[];
209
+ /** RE2 patterns masked in every attribute value (redaction `blocked_values`), whether content is kept or not. */
210
+ maskValues?: string[];
211
+ /** Hash masked values with this function instead of writing `****`. */
212
+ hashFunction?: RedactionProcessorConfig["hash_function"];
213
+ /** Duration histogram buckets. Default `GENAI_DURATION_BUCKETS`. */
214
+ buckets?: Duration[];
215
+ /** How often span and token metrics are flushed. Default 15s. */
216
+ metricsFlushInterval?: Duration;
217
+ }
218
+
219
+ export interface GenAiComponents {
220
+ /** Deletes content keys and event bodies (`transform/genai_content`). Absent when content is kept. */
221
+ contentRemoval?: OTelComponent<"processor", "transform", TransformProcessorConfig>;
222
+ /**
223
+ * Masks what is left (`redaction/genai_content`): the content keys, as a
224
+ * backstop behind the transform, and `maskValues`. Absent when content is
225
+ * kept and nothing is masked.
226
+ */
227
+ redaction?: OTelComponent<"processor", "redaction", RedactionProcessorConfig>;
228
+ /** Content removal then redaction, in the order a pipeline should run them. */
229
+ processors: Processor[];
230
+ /** Carries every span from the traces pipeline to `traces/genai` (`forward/genai`). */
231
+ forward: OTelComponent<"connector", "forward", ForwardConnectorConfig>;
232
+ /** Keeps only GenAI spans in `traces/genai` (`filter/genai_spans`). */
233
+ genAiSpans: OTelComponent<"processor", "filter", FilterProcessorConfig>;
234
+ /** Calls, errors and duration per GenAI dimension (`spanmetrics/genai`). */
235
+ spanMetrics: OTelComponent<"connector", "spanmetrics", SpanMetricsConnectorConfig>;
236
+ /** Input and output token sums (`sum/genai_tokens`). */
237
+ tokenUsage: OTelComponent<"connector", "sum", SumConnectorConfig>;
238
+ metrics: GenAiMetrics;
239
+ /** The semantic conventions the attribute keys follow. */
240
+ semconv: SchemaPin;
241
+ }
242
+
243
+ function quote(s: string): string {
244
+ return JSON.stringify(s);
245
+ }
246
+
247
+ function escapeRe2(s: string): string {
248
+ return s.replace(/[\\^$.|?*+()[\]{}]/g, "\\$&");
249
+ }
250
+
251
+ /** OTTL that deletes the content keys from one context's attributes. */
252
+ function deleteStatements(target: string, keys: string[]): string[] {
253
+ return [
254
+ ...keys.map((k) => `delete_key(${target}, ${quote(k)})`),
255
+ `delete_matching_keys(${target}, ${quote(GENAI_INDEXED_CONTENT_PATTERN)})`,
256
+ ];
257
+ }
258
+
259
+ /** The pieces of the GenAI preset, for wiring into pipelines you declare yourself. */
260
+ export function genAiComponents(options: GenAiComponentsOptions = {}): GenAiComponents {
261
+ const keepContent = options.keepContent === true;
262
+ const contentKeys = [...GENAI_CONTENT_ATTRIBUTES, ...(options.contentAttributes ?? [])];
263
+ const metrics = genAiMetrics(options);
264
+
265
+ let contentRemoval: GenAiComponents["contentRemoval"];
266
+ if (!keepContent) {
267
+ // Older SDKs name the event in an `event.name` attribute instead of the record's event_name.
268
+ const events = quote(`^(${GENAI_CONTENT_EVENTS.map(escapeRe2).join("|")})$`);
269
+ const eventMatch = `IsMatch(log.event_name, ${events}) or IsMatch(log.attributes["event.name"], ${events})`;
270
+ const traceGroups: TransformStatementGroup<"span" | "spanevent">[] = [
271
+ { context: "span", statements: deleteStatements("span.attributes", contentKeys) },
272
+ { context: "spanevent", statements: deleteStatements("spanevent.attributes", contentKeys) },
273
+ ];
274
+ const logGroups: TransformStatementGroup<"log">[] = [
275
+ {
276
+ context: "log",
277
+ statements: [
278
+ ...deleteStatements("log.attributes", contentKeys),
279
+ `delete_matching_keys(log.body, "^(content|message|tool_calls)$") where IsMap(log.body) and (${eventMatch})`,
280
+ `set(log.body, "") where IsString(log.body) and (${eventMatch})`,
281
+ ],
282
+ },
283
+ ];
284
+ contentRemoval = new TransformProcessor({
285
+ name: "genai_content",
286
+ error_mode: "ignore",
287
+ trace_statements: traceGroups,
288
+ log_statements: logGroups,
289
+ });
290
+ }
291
+
292
+ const blockedKeys = keepContent
293
+ ? []
294
+ : [`^(${contentKeys.map(escapeRe2).join("|")})$`, GENAI_INDEXED_CONTENT_PATTERN];
295
+ const maskValues = options.maskValues ?? [];
296
+ let redaction: GenAiComponents["redaction"];
297
+ if (blockedKeys.length > 0 || maskValues.length > 0) {
298
+ redaction = new RedactionProcessor({
299
+ name: "genai_content",
300
+ allow_all_keys: true,
301
+ ...(blockedKeys.length > 0 ? { blocked_key_patterns: blockedKeys } : {}),
302
+ ...(maskValues.length > 0 ? { blocked_values: maskValues } : {}),
303
+ ...(options.hashFunction ? { hash_function: options.hashFunction } : {}),
304
+ });
305
+ }
306
+
307
+ const forward = new ForwardConnector({ name: "genai" });
308
+ const genAiSpans = new FilterProcessor({
309
+ name: "genai_spans",
310
+ error_mode: "ignore",
311
+ traces: { span: [`attributes[${quote(GENAI_ATTRIBUTES.operationName)}] == nil`] },
312
+ });
313
+
314
+ const flush = options.metricsFlushInterval ?? "15s";
315
+ const spanMetrics = new SpanMetricsConnector({
316
+ name: "genai",
317
+ namespace: options.namespace ?? DEFAULT_NAMESPACE,
318
+ dimensions: [...GENAI_SPAN_METRIC_DIMENSIONS.map((name) => ({ name })), ...(options.dimensions ?? [])],
319
+ histogram: { unit: "s", explicit: { buckets: [...(options.buckets ?? GENAI_DURATION_BUCKETS)] } },
320
+ metrics_flush_interval: flush,
321
+ });
322
+
323
+ // An in-process invoke_agent or invoke_workflow span may carry the usage of
324
+ // the model calls under it, which are counted on their own spans.
325
+ const notAggregate = `not (kind == SPAN_KIND_INTERNAL and (attributes[${quote(GENAI_ATTRIBUTES.operationName)}] == "invoke_agent" or attributes[${quote(GENAI_ATTRIBUTES.operationName)}] == "invoke_workflow"))`;
326
+ const tokenAttributes = GENAI_TOKEN_DIMENSIONS.map((key) => ({ key, default_value: GENAI_UNKNOWN_MODEL }));
327
+ const tokenUsage = new SumConnector({
328
+ name: "genai_tokens",
329
+ spans: {
330
+ [metrics.inputTokens.name]: {
331
+ source_attribute: GENAI_ATTRIBUTES.inputTokens,
332
+ description: "Input tokens used by GenAI operations",
333
+ conditions: [`attributes[${quote(GENAI_ATTRIBUTES.inputTokens)}] != nil and ${notAggregate}`],
334
+ attributes: tokenAttributes,
335
+ },
336
+ [metrics.outputTokens.name]: {
337
+ source_attribute: GENAI_ATTRIBUTES.outputTokens,
338
+ description: "Output tokens used by GenAI operations",
339
+ conditions: [`attributes[${quote(GENAI_ATTRIBUTES.outputTokens)}] != nil and ${notAggregate}`],
340
+ attributes: tokenAttributes,
341
+ },
342
+ },
343
+ });
344
+
345
+ const processors: Processor[] = [];
346
+ if (contentRemoval) processors.push(contentRemoval);
347
+ if (redaction) processors.push(redaction);
348
+
349
+ return {
350
+ ...(contentRemoval ? { contentRemoval } : {}),
351
+ ...(redaction ? { redaction } : {}),
352
+ processors,
353
+ forward,
354
+ genAiSpans,
355
+ spanMetrics,
356
+ tokenUsage,
357
+ metrics,
358
+ semconv: GENAI_SEMCONV_PIN,
359
+ };
360
+ }
361
+
362
+ // ── The whole collector ──────────────────────────────────────────────
363
+
364
+ export interface GenAiPipelineOptions extends GenAiComponentsOptions {
365
+ /** Where traces go. Default: one `debug` exporter at `basic` verbosity. */
366
+ traceExporters?: Exporter[];
367
+ /** Where the span and token metrics go. Default: the same `debug` exporter. */
368
+ metricExporters?: Exporter[];
369
+ /** Where logs (and events sent as logs) go. Default: the same `debug` exporter. */
370
+ logExporters?: Exporter[];
371
+ /** Give logs a pipeline, so event-based content is removed too. Default: true. */
372
+ logs?: boolean;
373
+ /**
374
+ * Processors that thin exported traces, such as `tail_sampling`. They run
375
+ * in a `traces/sampled` pipeline after the metrics branch, so metrics still
376
+ * count every span.
377
+ */
378
+ sampling?: Processor[];
379
+ /** Serve `health_check` on 0.0.0.0:13133. Default: true. */
380
+ healthCheck?: boolean;
381
+ }
382
+
383
+ /**
384
+ * The entities of an OTLP collector for GenAI workloads: an `otlp` receiver
385
+ * on 4317 and 4318; `memory_limiter`, content removal and `batch` on traces
386
+ * and logs; a `traces/genai` branch that turns GenAI spans into metrics
387
+ * before any sampling; and a `metrics/genai` pipeline that exports them. Pass
388
+ * the result to `collectorYaml`, or use `genAiComponents()` to wire the
389
+ * pieces into pipelines of your own.
390
+ */
391
+ export function genAiPipeline(options: GenAiPipelineOptions = {}): Declarable[] {
392
+ const debug = new DebugExporter({ verbosity: "basic" });
393
+ const { traceExporters = [debug], metricExporters = [debug], logExporters = [debug], logs = true, sampling = [], healthCheck = true } =
394
+ options;
395
+ const parts = genAiComponents(options);
396
+
397
+ const otlp = new OtlpReceiver({
398
+ protocols: {
399
+ grpc: { endpoint: "0.0.0.0:4317" },
400
+ http: { endpoint: "0.0.0.0:4318" },
401
+ },
402
+ });
403
+ const memoryLimiter = new MemoryLimiterProcessor({
404
+ check_interval: "1s",
405
+ limit_percentage: 80,
406
+ spike_limit_percentage: 20,
407
+ });
408
+ const batch = new BatchProcessor({});
409
+ const entities: Declarable[] = [otlp];
410
+
411
+ if (sampling.length === 0) {
412
+ entities.push(
413
+ new Pipeline({
414
+ signal: "traces",
415
+ receivers: [otlp],
416
+ processors: [memoryLimiter, ...parts.processors, batch],
417
+ exporters: [...traceExporters, parts.forward],
418
+ }),
419
+ );
420
+ } else {
421
+ const sampled = new ForwardConnector({ name: "sampled" });
422
+ entities.push(
423
+ new Pipeline({
424
+ signal: "traces",
425
+ receivers: [otlp],
426
+ processors: [memoryLimiter, ...parts.processors],
427
+ exporters: [parts.forward, sampled],
428
+ }),
429
+ new Pipeline({
430
+ signal: "traces",
431
+ name: "sampled",
432
+ receivers: [sampled],
433
+ processors: [...sampling, batch],
434
+ exporters: traceExporters,
435
+ }),
436
+ );
437
+ }
438
+ entities.push(
439
+ new Pipeline({
440
+ signal: "traces",
441
+ name: "genai",
442
+ receivers: [parts.forward],
443
+ processors: [parts.genAiSpans],
444
+ exporters: [parts.spanMetrics, parts.tokenUsage],
445
+ }),
446
+ new Pipeline({
447
+ signal: "metrics",
448
+ name: "genai",
449
+ receivers: [parts.spanMetrics, parts.tokenUsage],
450
+ processors: [batch],
451
+ exporters: metricExporters,
452
+ }),
453
+ );
454
+ if (logs) {
455
+ entities.push(
456
+ new Pipeline({
457
+ signal: "logs",
458
+ receivers: [otlp],
459
+ processors: [memoryLimiter, ...parts.processors, batch],
460
+ exporters: logExporters,
461
+ }),
462
+ );
463
+ }
464
+ if (healthCheck) entities.push(new HealthCheckExtension({ endpoint: "0.0.0.0:13133" }));
465
+ return entities;
466
+ }
@@ -9,6 +9,11 @@
9
9
  "kind": "resource",
10
10
  "lexicon": "otel"
11
11
  },
12
+ "CountConnector": {
13
+ "resourceType": "OTel::Connector::count",
14
+ "kind": "resource",
15
+ "lexicon": "otel"
16
+ },
12
17
  "DebugExporter": {
13
18
  "resourceType": "OTel::Exporter::debug",
14
19
  "kind": "resource",
@@ -19,6 +24,16 @@
19
24
  "kind": "resource",
20
25
  "lexicon": "otel"
21
26
  },
27
+ "FilterProcessor": {
28
+ "resourceType": "OTel::Processor::filter",
29
+ "kind": "resource",
30
+ "lexicon": "otel"
31
+ },
32
+ "ForwardConnector": {
33
+ "resourceType": "OTel::Connector::forward",
34
+ "kind": "resource",
35
+ "lexicon": "otel"
36
+ },
22
37
  "GoogleCloudExporter": {
23
38
  "resourceType": "OTel::Exporter::googlecloud",
24
39
  "kind": "resource",
@@ -39,6 +54,21 @@
39
54
  "kind": "resource",
40
55
  "lexicon": "otel"
41
56
  },
57
+ "K8sClusterReceiver": {
58
+ "resourceType": "OTel::Receiver::k8s_cluster",
59
+ "kind": "resource",
60
+ "lexicon": "otel"
61
+ },
62
+ "KubeletStatsReceiver": {
63
+ "resourceType": "OTel::Receiver::kubeletstats",
64
+ "kind": "resource",
65
+ "lexicon": "otel"
66
+ },
67
+ "LoadBalancingExporter": {
68
+ "resourceType": "OTel::Exporter::loadbalancing",
69
+ "kind": "resource",
70
+ "lexicon": "otel"
71
+ },
42
72
  "MemoryLimiterProcessor": {
43
73
  "resourceType": "OTel::Processor::memory_limiter",
44
74
  "kind": "resource",
@@ -69,6 +99,11 @@
69
99
  "kind": "resource",
70
100
  "lexicon": "otel"
71
101
  },
102
+ "ProbabilisticSamplerProcessor": {
103
+ "resourceType": "OTel::Processor::probabilistic_sampler",
104
+ "kind": "resource",
105
+ "lexicon": "otel"
106
+ },
72
107
  "PrometheusExporter": {
73
108
  "resourceType": "OTel::Exporter::prometheus",
74
109
  "kind": "resource",
@@ -79,6 +114,11 @@
79
114
  "kind": "resource",
80
115
  "lexicon": "otel"
81
116
  },
117
+ "RedactionProcessor": {
118
+ "resourceType": "OTel::Processor::redaction",
119
+ "kind": "resource",
120
+ "lexicon": "otel"
121
+ },
82
122
  "ResourceDetectionProcessor": {
83
123
  "resourceType": "OTel::Processor::resourcedetection",
84
124
  "kind": "resource",
@@ -89,11 +129,41 @@
89
129
  "kind": "resource",
90
130
  "lexicon": "otel"
91
131
  },
132
+ "RoutingConnector": {
133
+ "resourceType": "OTel::Connector::routing",
134
+ "kind": "resource",
135
+ "lexicon": "otel"
136
+ },
92
137
  "Service": {
93
138
  "resourceType": "OTel::Service",
94
139
  "kind": "resource",
95
140
  "lexicon": "otel"
96
141
  },
142
+ "ServiceGraphConnector": {
143
+ "resourceType": "OTel::Connector::servicegraph",
144
+ "kind": "resource",
145
+ "lexicon": "otel"
146
+ },
147
+ "SpanMetricsConnector": {
148
+ "resourceType": "OTel::Connector::spanmetrics",
149
+ "kind": "resource",
150
+ "lexicon": "otel"
151
+ },
152
+ "SumConnector": {
153
+ "resourceType": "OTel::Connector::sum",
154
+ "kind": "resource",
155
+ "lexicon": "otel"
156
+ },
157
+ "TailSamplingProcessor": {
158
+ "resourceType": "OTel::Processor::tail_sampling",
159
+ "kind": "resource",
160
+ "lexicon": "otel"
161
+ },
162
+ "TransformProcessor": {
163
+ "resourceType": "OTel::Processor::transform",
164
+ "kind": "resource",
165
+ "lexicon": "otel"
166
+ },
97
167
  "ZPagesExtension": {
98
168
  "resourceType": "OTel::Extension::zpages",
99
169
  "kind": "resource",
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export {
32
32
  componentEntityType,
33
33
  isOTelComponent,
34
34
  COLLECTOR_PIN,
35
+ GENAI_SEMCONV_PIN,
35
36
  type SchemaPin,
36
37
  type SafeParseSchema,
37
38
  type ConfigValidator,
@@ -59,7 +60,10 @@ export {
59
60
  type TopologyPipeline,
60
61
  type TopologyComponent,
61
62
  type TopologyExporter,
63
+ type TopologyEdge,
64
+ type SemconvUsage,
62
65
  } from "./topology";
66
+ export { semconvUsage, SEMCONV_VOCABULARIES, type SemconvVocabulary } from "./semconv";
63
67
 
64
68
  // What the platform collector composites (docker, k8s, fly) share
65
69
  export {
@@ -71,3 +75,38 @@ export {
71
75
  type CollectorPort,
72
76
  type CollectorEndpoints,
73
77
  } from "./platform";
78
+
79
+ // The GenAI preset: content removal and agent RED and token metrics
80
+ export {
81
+ genAiPipeline,
82
+ genAiComponents,
83
+ genAiMetrics,
84
+ GENAI_ATTRIBUTES,
85
+ GENAI_CONTENT_ATTRIBUTES,
86
+ GENAI_CONTENT_EVENTS,
87
+ GENAI_INDEXED_CONTENT_PATTERN,
88
+ GENAI_SPAN_METRIC_DIMENSIONS,
89
+ GENAI_TOKEN_DIMENSIONS,
90
+ GENAI_DURATION_BUCKETS,
91
+ GENAI_UNKNOWN_MODEL,
92
+ type GenAiPipelineOptions,
93
+ type GenAiComponentsOptions,
94
+ type GenAiComponents,
95
+ type GenAiMetricsOptions,
96
+ type GenAiMetrics,
97
+ type GenAiMetric,
98
+ } from "./genai";
99
+
100
+ // Metric names as Prometheus serves them, for dashboards and SLOs built from a declaration
101
+ export {
102
+ spanMetricsNames,
103
+ prometheusMetricName,
104
+ prometheusLabel,
105
+ SPANMETRICS_DEFAULT_DIMENSIONS,
106
+ SPANMETRICS_DEFAULT_NAMESPACE,
107
+ SPAN_STATUS_ERROR,
108
+ type CollectorMetric,
109
+ type SpanMetricsNames,
110
+ type SpanMetricsNamingConfig,
111
+ type PrometheusNaming,
112
+ } from "./metric-names";
@@ -2,7 +2,8 @@
2
2
  * The otel lexicon's chant audit catalog, contributed via
3
3
  * `otelPlugin.auditCatalog()` (#687, #1346).
4
4
  *
5
- * OTEL101-OTEL106 read the emitted collector YAML, so they are `yamlBased`.
5
+ * OTEL101-OTEL106 and OTEL112 read the emitted collector YAML, so they are
6
+ * `yamlBased`.
6
7
  * OTEL107-OTEL109 read the declared entities (a component's definition, its
7
8
  * schema pin), which a standalone YAML file does not carry, so they are
8
9
  * constructed with `yamlBased: false`. The two source-level lint rules are
@@ -38,7 +39,7 @@ export const otelAuditCatalog: Record<string, RuleMeta> = {
38
39
  "merge-worthy",
39
40
  "guidance",
40
41
  "Pipeline uses an undeclared component",
41
- "Declare the receiver, processor or exporter under its section, or reference the declared entity instead of an id string.",
42
+ "Declare the receiver, processor or exporter under its section, or reference the declared entity instead of an id string. List a connector as an exporter in one pipeline and a receiver in another.",
42
43
  { category: "correctness" },
43
44
  ),
44
45
  OTEL102: auditRule(
@@ -99,4 +100,12 @@ export const otelAuditCatalog: Record<string, RuleMeta> = {
99
100
  "Custom collector component has no schema pin",
100
101
  "Pass defineComponent a pin with the source and version the component's config type follows.",
101
102
  ),
103
+ OTEL112: auditRule(
104
+ "OTEL112",
105
+ "merge-worthy",
106
+ "guidance",
107
+ "Connector joins pipelines whose signals it does not convert",
108
+ "Feed the connector from, and receive from it into, pipelines of the signals it supports (spanmetrics: traces in, metrics out).",
109
+ { category: "correctness" },
110
+ ),
102
111
  };
@@ -9,6 +9,7 @@ import { otel106 } from "./otel106";
9
9
  import { otel107 } from "./otel107";
10
10
  import { otel108 } from "./otel108";
11
11
  import { otel109 } from "./otel109";
12
+ import { otel112 } from "./otel112";
12
13
 
13
14
  export const postSynthChecks: PostSynthCheck[] = [
14
15
  otel101,
@@ -20,4 +21,5 @@ export const postSynthChecks: PostSynthCheck[] = [
20
21
  otel107,
21
22
  otel108,
22
23
  otel109,
24
+ otel112,
23
25
  ];
@@ -52,7 +52,7 @@ function toDiagnostic(issue: CollectorIssue, source?: string): PostSynthDiagnost
52
52
  };
53
53
  }
54
54
 
55
- /** Diagnostics for one config-level code (OTEL101-OTEL106) across every collector config in the output. */
55
+ /** Diagnostics for one config-level code (OTEL101-OTEL106, OTEL112) across every collector config in the output. */
56
56
  export function configDiagnostics(ctx: PostSynthContext, code: CollectorIssueCode): PostSynthDiagnostic[] {
57
57
  return collectorConfigs(ctx).flatMap(({ source, config }) =>
58
58
  validateCollectorConfig(config)