@intentius/chant-lexicon-otel 0.100.0 → 0.101.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 (122) hide show
  1. package/README.md +8 -6
  2. package/dist/codegen/docs.d.ts.map +1 -1
  3. package/dist/components/common.d.ts +2 -0
  4. package/dist/components/common.d.ts.map +1 -1
  5. package/dist/components/connectors.d.ts +109 -1
  6. package/dist/components/connectors.d.ts.map +1 -1
  7. package/dist/components/exporters.d.ts.map +1 -1
  8. package/dist/components/extensions.d.ts +26 -2
  9. package/dist/components/extensions.d.ts.map +1 -1
  10. package/dist/components/processors.d.ts +15 -2
  11. package/dist/components/processors.d.ts.map +1 -1
  12. package/dist/components/receivers.d.ts.map +1 -1
  13. package/dist/composites/catalog.d.ts +3 -0
  14. package/dist/composites/catalog.d.ts.map +1 -0
  15. package/dist/composites/index.d.ts +6 -0
  16. package/dist/composites/index.d.ts.map +1 -0
  17. package/dist/composites/node-agent.d.ts +110 -0
  18. package/dist/composites/node-agent.d.ts.map +1 -0
  19. package/dist/configmap.d.ts +35 -0
  20. package/dist/configmap.d.ts.map +1 -0
  21. package/dist/define.d.ts +9 -0
  22. package/dist/define.d.ts.map +1 -1
  23. package/dist/genai.d.ts +145 -2
  24. package/dist/genai.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/init-templates.d.ts +24 -0
  28. package/dist/init-templates.d.ts.map +1 -0
  29. package/dist/integrity.json +10 -5
  30. package/dist/lint/audit-catalog.d.ts +1 -1
  31. package/dist/lint/audit-catalog.d.ts.map +1 -1
  32. package/dist/lint/post-synth/index.d.ts.map +1 -1
  33. package/dist/lint/post-synth/otel-helpers.d.ts +32 -9
  34. package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
  35. package/dist/lint/post-synth/otel113.d.ts +8 -0
  36. package/dist/lint/post-synth/otel113.d.ts.map +1 -0
  37. package/dist/lint/post-synth/otel114.d.ts +8 -0
  38. package/dist/lint/post-synth/otel114.d.ts.map +1 -0
  39. package/dist/lint/post-synth/otel115.d.ts +8 -0
  40. package/dist/lint/post-synth/otel115.d.ts.map +1 -0
  41. package/dist/lint/post-synth/otel116.d.ts +8 -0
  42. package/dist/lint/post-synth/otel116.d.ts.map +1 -0
  43. package/dist/lint/post-synth/otel117.d.ts +8 -0
  44. package/dist/lint/post-synth/otel117.d.ts.map +1 -0
  45. package/dist/manifest.json +1 -1
  46. package/dist/meta.json +15 -0
  47. package/dist/okf/index.md +8 -0
  48. package/dist/okf/rules/OTEL113.md +11 -0
  49. package/dist/okf/rules/OTEL114.md +11 -0
  50. package/dist/okf/rules/OTEL115.md +11 -0
  51. package/dist/okf/rules/OTEL116.md +11 -0
  52. package/dist/okf/rules/OTEL117.md +11 -0
  53. package/dist/okf/types/DeltaToCumulativeProcessor.md +9 -0
  54. package/dist/okf/types/K8sLeaderElectorExtension.md +9 -0
  55. package/dist/okf/types/SignalToMetricsConnector.md +9 -0
  56. package/dist/plugin.d.ts.map +1 -1
  57. package/dist/rules/otel-helpers.ts +43 -11
  58. package/dist/rules/otel113.ts +17 -0
  59. package/dist/rules/otel114.ts +17 -0
  60. package/dist/rules/otel115.ts +17 -0
  61. package/dist/rules/otel116.ts +17 -0
  62. package/dist/rules/otel117.ts +17 -0
  63. package/dist/skills/chant-otel.md +11 -5
  64. package/dist/topology.d.ts +5 -0
  65. package/dist/topology.d.ts.map +1 -1
  66. package/dist/validate-config.d.ts +6 -2
  67. package/dist/validate-config.d.ts.map +1 -1
  68. package/dist/validate.d.ts.map +1 -1
  69. package/package.json +2 -2
  70. package/src/codegen/docs.ts +9 -4
  71. package/src/components/common.ts +17 -0
  72. package/src/components/connectors.ts +209 -1
  73. package/src/components/exporters.ts +2 -0
  74. package/src/components/extensions.test.ts +70 -0
  75. package/src/components/extensions.ts +60 -2
  76. package/src/components/processors.test.ts +54 -0
  77. package/src/components/processors.ts +41 -2
  78. package/src/components/receivers.ts +5 -0
  79. package/src/composites/catalog.test.ts +38 -0
  80. package/src/composites/catalog.ts +81 -0
  81. package/src/composites/composites.test.ts +138 -0
  82. package/src/composites/index.ts +6 -0
  83. package/src/composites/node-agent.ts +219 -0
  84. package/src/configmap.ts +67 -0
  85. package/src/connectors.test.ts +197 -1
  86. package/src/define.ts +9 -0
  87. package/src/genai.test.ts +301 -6
  88. package/src/genai.ts +323 -8
  89. package/src/generated/lexicon-otel.json +15 -0
  90. package/src/import/roundtrip.test.ts +21 -1
  91. package/src/import/testdata/fixtures.ts +37 -5
  92. package/src/import/testdata/k8s-leader-elector.yaml +35 -0
  93. package/src/import/testdata/signaltometrics.yaml +83 -0
  94. package/src/index.ts +16 -0
  95. package/src/init-templates.test.ts +72 -0
  96. package/src/init-templates.ts +146 -0
  97. package/src/lint/audit-catalog.ts +41 -1
  98. package/src/lint/post-synth/connector-graph.test.ts +207 -0
  99. package/src/lint/post-synth/index.ts +10 -0
  100. package/src/lint/post-synth/otel-helpers.ts +43 -11
  101. package/src/lint/post-synth/otel113.ts +17 -0
  102. package/src/lint/post-synth/otel114.ts +17 -0
  103. package/src/lint/post-synth/otel115.ts +17 -0
  104. package/src/lint/post-synth/otel116.ts +17 -0
  105. package/src/lint/post-synth/otel117.ts +17 -0
  106. package/src/lint/post-synth/post-synth.test.ts +223 -1
  107. package/src/lint/post-synth/testdata/otel113-fail.yaml +23 -0
  108. package/src/lint/post-synth/testdata/otel113-pass.yaml +21 -0
  109. package/src/lint/post-synth/testdata/otel114-fail.yaml +25 -0
  110. package/src/lint/post-synth/testdata/otel114-pass.yaml +23 -0
  111. package/src/lint/post-synth/testdata/otel115-fail.yaml +29 -0
  112. package/src/lint/post-synth/testdata/otel115-pass.yaml +29 -0
  113. package/src/plugin.test.ts +18 -0
  114. package/src/plugin.ts +22 -0
  115. package/src/skills/chant-otel.md +11 -5
  116. package/src/testdata/genai-configured.yaml +108 -0
  117. package/src/testdata/genai-default.yaml +92 -0
  118. package/src/topology.test.ts +12 -0
  119. package/src/topology.ts +16 -0
  120. package/src/typecheck.test.ts +112 -0
  121. package/src/validate-config.ts +415 -2
  122. package/src/validate.ts +17 -0
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: resource-type
3
+ title: DeltaToCumulativeProcessor
4
+ description: otel resource type OTel::Processor::deltatocumulative
5
+ name: DeltaToCumulativeProcessor
6
+ lexicon: otel
7
+ resource_type: OTel::Processor::deltatocumulative
8
+ ---
9
+ `OTel::Processor::deltatocumulative`, a resource type of the otel lexicon.
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: resource-type
3
+ title: K8sLeaderElectorExtension
4
+ description: otel resource type OTel::Extension::k8s_leader_elector
5
+ name: K8sLeaderElectorExtension
6
+ lexicon: otel
7
+ resource_type: OTel::Extension::k8s_leader_elector
8
+ ---
9
+ `OTel::Extension::k8s_leader_elector`, a resource type of the otel lexicon.
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: resource-type
3
+ title: SignalToMetricsConnector
4
+ description: otel resource type OTel::Connector::signaltometrics
5
+ name: SignalToMetricsConnector
6
+ lexicon: otel
7
+ resource_type: OTel::Connector::signaltometrics
8
+ ---
9
+ `OTel::Connector::signaltometrics`, a resource type of the otel lexicon.
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AA4B9D;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,EAAE,aAyFxB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AA+B9D;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,EAAE,aA4GxB,CAAC"}
@@ -8,16 +8,32 @@ import type { SerializerResult } from "@intentius/chant/serializer";
8
8
  import { loadAll } from "js-yaml";
9
9
  import type { Declarable } from "@intentius/chant/declarable";
10
10
  import { looksLikeCollectorConfig, type CollectorConfig } from "../../model";
11
+ import { configMapCollectorConfigs, describeConfigMapConfig } from "../../configmap";
11
12
  import { validateCollectorConfig, validateCollectorEntities, type CollectorIssueCode, type CollectorIssue } from "../../validate-config";
12
13
 
14
+ /** Where a collector config was found in the build output. */
15
+ export interface FoundCollectorConfig {
16
+ /** The lexicon output, or the output file, the config came from. */
17
+ source: string;
18
+ config: CollectorConfig;
19
+ /** Set when the config is a value in a Kubernetes ConfigMap (chant #2930). */
20
+ configMap?: { namespace: string; name: string; key: string };
21
+ }
22
+
13
23
  /**
14
- * Every collector config in the output: the otel lexicon's own, and any YAML
15
- * document shaped like one. Parsed with js-yaml rather than `ctx.docs`,
16
- * because collector configs are written with flow lists (`[otlp, batch]`)
17
- * that core's small YAML reader keeps as strings.
24
+ * Every collector config in the output: the otel lexicon's own, any YAML
25
+ * document shaped like one, and any ConfigMap data value that parses as one
26
+ * (how `OtelCollector`, `OtelCollectorGateway` and `GkeOtelCollector` carry
27
+ * theirs). Parsed with js-yaml rather than `ctx.docs`, because collector
28
+ * configs are written with flow lists (`[otlp, batch]`) that core's small
29
+ * YAML reader keeps as strings.
30
+ *
31
+ * `chant build` hands each lexicon's checks only that lexicon's output, so
32
+ * the otel checks see ConfigMaps only in a context that includes k8s output.
33
+ * The k8s lexicon's WK8604 runs the same config checks over its own output.
18
34
  */
19
- export function collectorConfigs(ctx: PostSynthContext): Array<{ source: string; config: CollectorConfig }> {
20
- const out: Array<{ source: string; config: CollectorConfig }> = [];
35
+ export function collectorConfigs(ctx: PostSynthContext): FoundCollectorConfig[] {
36
+ const out: FoundCollectorConfig[] = [];
21
37
  for (const [lexicon, output] of ctx.outputs) {
22
38
  const texts: Array<[string, string]> =
23
39
  typeof output === "string"
@@ -35,6 +51,10 @@ export function collectorConfigs(ctx: PostSynthContext): Array<{ source: string;
35
51
  if (typeof doc !== "object" || doc === null || Array.isArray(doc)) continue;
36
52
  if ((lexicon === "otel" && source === lexicon) || looksLikeCollectorConfig(doc)) {
37
53
  out.push({ source, config: doc as CollectorConfig });
54
+ continue;
55
+ }
56
+ for (const { config, ...configMap } of configMapCollectorConfigs(doc)) {
57
+ out.push({ source, config, configMap });
38
58
  }
39
59
  }
40
60
  }
@@ -42,22 +62,34 @@ export function collectorConfigs(ctx: PostSynthContext): Array<{ source: string;
42
62
  return out;
43
63
  }
44
64
 
45
- function toDiagnostic(issue: CollectorIssue, source?: string): PostSynthDiagnostic {
65
+ function toDiagnostic(issue: CollectorIssue, found?: Omit<FoundCollectorConfig, "config">): PostSynthDiagnostic {
66
+ const where = found?.configMap ? describeConfigMapConfig(found.configMap) : found && found.source !== "otel" ? found.source : undefined;
46
67
  return {
47
68
  checkId: issue.code,
48
69
  severity: issue.severity,
49
- message: source && source !== "otel" ? `${source}: ${issue.message}` : issue.message,
70
+ message: where ? `${where}: ${issue.message}` : issue.message,
50
71
  ...(issue.component ? { entity: issue.component } : issue.pipeline ? { entity: issue.pipeline } : {}),
51
72
  lexicon: "otel",
52
73
  };
53
74
  }
54
75
 
55
- /** Diagnostics for one config-level code (OTEL101-OTEL106, OTEL112) across every collector config in the output. */
76
+ /**
77
+ * Every config-level diagnostic (OTEL101-OTEL106, OTEL112-OTEL117 and any later
78
+ * config check) over the collector configs in the output. `configMapsOnly`
79
+ * keeps the configs held in ConfigMaps, which is what WK8604 reads.
80
+ */
81
+ export function collectorConfigDiagnostics(ctx: PostSynthContext, opts: { configMapsOnly?: boolean } = {}): PostSynthDiagnostic[] {
82
+ return collectorConfigs(ctx)
83
+ .filter((found) => !opts.configMapsOnly || found.configMap)
84
+ .flatMap(({ config, ...found }) => validateCollectorConfig(config).map((i) => toDiagnostic(i, found)));
85
+ }
86
+
87
+ /** Diagnostics for one config-level code (OTEL101-OTEL106, OTEL112-OTEL117) across every collector config in the output. */
56
88
  export function configDiagnostics(ctx: PostSynthContext, code: CollectorIssueCode): PostSynthDiagnostic[] {
57
- return collectorConfigs(ctx).flatMap(({ source, config }) =>
89
+ return collectorConfigs(ctx).flatMap(({ config, ...found }) =>
58
90
  validateCollectorConfig(config)
59
91
  .filter((i) => i.code === code)
60
- .map((i) => toDiagnostic(i, source)),
92
+ .map((i) => toDiagnostic(i, found)),
61
93
  );
62
94
  }
63
95
 
@@ -0,0 +1,17 @@
1
+ /**
2
+ * OTEL113: Pipelines form a cycle through connectors
3
+ *
4
+ * A connector carries data from the pipeline that lists it as an exporter to the pipeline that lists it as a receiver. When those hops lead back to a pipeline already on the path (traces/a feeds traces/b through one connector and traces/b feeds traces/a through another, or one pipeline lists the same `forward` connector on both sides), the collector refuses to start. The check walks the `edges` `collectorTopology()` returns and names one cycle per group of pipelines caught in it.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { configDiagnostics } from "./otel-helpers";
9
+
10
+ export const otel113: PostSynthCheck = {
11
+ id: "OTEL113",
12
+ description: "Pipelines form a cycle through connectors",
13
+
14
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ return configDiagnostics(ctx, "OTEL113");
16
+ },
17
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * OTEL114: A connector id is also declared as a receiver or exporter
3
+ *
4
+ * A pipeline names a connector the same way it names a receiver or an exporter, so the collector refuses a config where a connector id is also declared under `receivers` or `exporters`, whether or not any pipeline uses it.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { configDiagnostics } from "./otel-helpers";
9
+
10
+ export const otel114: PostSynthCheck = {
11
+ id: "OTEL114",
12
+ description: "A connector id is also declared as a receiver or exporter",
13
+
14
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ return configDiagnostics(ctx, "OTEL114");
16
+ },
17
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * OTEL115: A routing connector routes to a pipeline that does not receive from it
3
+ *
4
+ * Every pipeline a `routing` connector names in `table[].pipelines` or `default_pipelines` must list that connector in its receivers. The connector can only hand data to the pipelines it feeds, and the collector refuses to start when a route names any other pipeline or one that does not exist.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { configDiagnostics } from "./otel-helpers";
9
+
10
+ export const otel115: PostSynthCheck = {
11
+ id: "OTEL115",
12
+ description: "A routing connector routes to a pipeline that does not receive from it",
13
+
14
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ return configDiagnostics(ctx, "OTEL115");
16
+ },
17
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * OTEL116: A connector splits metrics by a high-cardinality GenAI attribute
3
+ *
4
+ * `gen_ai.conversation.id`, `gen_ai.response.id`, `gen_ai.tool.call.id`, `session.id`, `user.id`, `enduser.id` and the other keys in `GENAI_HIGH_CARDINALITY_ATTRIBUTES` take a new value per response, conversation or user, and the content keys (`gen_ai.input.messages` and the rest of `GENAI_CONTENT_ATTRIBUTES`) are unbounded. Used as a metric attribute in `spanmetrics` or `servicegraph` dimensions, `sum` or `count` attributes, or `signaltometrics` attributes or `include_resource_attributes`, each value starts new time series. The collector accepts such a config without a warning.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { configDiagnostics } from "./otel-helpers";
9
+
10
+ export const otel116: PostSynthCheck = {
11
+ id: "OTEL116",
12
+ description: "A connector splits metrics by a high-cardinality GenAI attribute",
13
+
14
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ return configDiagnostics(ctx, "OTEL116");
16
+ },
17
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * OTEL117: Two components the collector starts listen on the same address
3
+ *
4
+ * A receiver or exporter a pipeline lists, an extension in `service.extensions`, and the collector's own metrics endpoint (`localhost:8888` unless `service.telemetry.metrics` says otherwise) each bind an address when the collector starts. Two on the same port, on the same host or with either on a wildcard host (`0.0.0.0`, `::`), make the second bind fail with "address already in use", and the collector exits. `otelcol validate` does not bind, so it accepts such a config. The listeners checked are the otlp, zipkin and jaeger receivers, the prometheus exporter, and the health_check, zpages and pprof extensions, with their defaults at the pinned collector release.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { configDiagnostics } from "./otel-helpers";
9
+
10
+ export const otel117: PostSynthCheck = {
11
+ id: "OTEL117",
12
+ description: "Two components the collector starts listen on the same address",
13
+
14
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ return configDiagnostics(ctx, "OTEL117");
16
+ },
17
+ };
@@ -15,6 +15,10 @@ The otel lexicon (`@intentius/chant-lexicon-otel`) types collector config. Each
15
15
  export default { lexicons: ["otel"] };
16
16
  ```
17
17
 
18
+ `chant init --lexicon otel` scaffolds a collector; `--template k8s-agent` scaffolds a per-node Kubernetes agent and `--template genai` a GenAI collector.
19
+
20
+ For a per-node Kubernetes agent, `NodeAgent({ exporters, metricExporters?, clusterName?, kubeletStats? })` declares the whole config: OTLP, host metrics, container logs, `k8sattributes` filtered to the node, and `memory_limiter` first and `batch` last on every pipeline. Pass `Object.values(agent.members)` wherever an entity list is taken, such as the k8s lexicon's `OtelCollector` `config`.
21
+
18
22
  ## Declaring components
19
23
 
20
24
  Every component class takes the collector's own config keys (snake_case, as in the collector docs) plus an optional `name`. The id is `type`, or `type/name` when `name` is set.
@@ -41,10 +45,10 @@ Emits `receivers.otlp`, `processors.memory_limiter` and `processors.batch`, `exp
41
45
  | Kind | Types |
42
46
  |---|---|
43
47
  | receivers | otlp, prometheus, hostmetrics, filelog, k8s_cluster, kubeletstats |
44
- | processors | batch, memory_limiter, resource, attributes, k8sattributes, resourcedetection, filter, transform, redaction, tail_sampling, probabilistic_sampler |
48
+ | processors | batch, memory_limiter, resource, attributes, k8sattributes, resourcedetection, filter, transform, redaction, tail_sampling, probabilistic_sampler, deltatocumulative |
45
49
  | exporters | otlp, otlphttp, debug, prometheus, googlecloud, loadbalancing |
46
- | connectors | spanmetrics, servicegraph, routing, forward, count, sum |
47
- | extensions | health_check, pprof, zpages |
50
+ | connectors | spanmetrics, servicegraph, routing, forward, count, sum, signaltometrics |
51
+ | extensions | health_check, pprof, zpages, k8s_leader_elector |
48
52
 
49
53
  Anything else goes through `defineComponent`; see the `chant-otel-custom-components` skill.
50
54
 
@@ -58,7 +62,7 @@ export const traces = new Pipeline({ signal: "traces", receivers: [otlp], export
58
62
  export const red = new Pipeline({ signal: "metrics", name: "red", receivers: [spanmetrics], exporters: [prom] });
59
63
  ```
60
64
 
61
- `spanmetrics` and `servicegraph` turn traces into metrics, `count` and `sum` turn any signal into metrics (`sum` adds up a numeric attribute), `routing` and `forward` keep the signal. A connector on one side only fails OTEL101; a pipeline whose signal the connector can't pair fails OTEL112.
65
+ `spanmetrics` and `servicegraph` turn traces into metrics, `count` and `sum` turn any signal into metrics (`sum` adds up a numeric attribute), `signaltometrics` builds metrics you name, of any type, from any signal with OTTL values, `routing` and `forward` keep the signal. A connector on one side only fails OTEL101; a pipeline whose signal the connector can't pair fails OTEL112. Pipelines that feed each other in a loop through connectors fail OTEL113, a connector id that is also a receiver or exporter id fails OTEL114, and every pipeline a `routing` connector routes to must list it in `receivers` (OTEL115).
62
66
 
63
67
  ## GenAI workloads
64
68
 
@@ -68,7 +72,7 @@ For services that emit GenAI spans, start from the preset instead of writing the
68
72
  export const collector = genAiPipeline({ traceExporters: [tempo], metricExporters: [prom] });
69
73
  ```
70
74
 
71
- It deletes prompt, completion, system-instruction and tool-call content from spans, span events and logs, and derives call, error, duration and token metrics (`genai_calls_total`, `genai_duration_seconds`, `genai_tokens_input_total`, `genai_tokens_output_total`) from every GenAI span before sampling. Keep content only when asked, with `keepContent: true`. `genAiComponents()` returns the pieces for hand-built pipelines.
75
+ It deletes prompt, completion, system-instruction and tool-call content from spans, span events and logs, and derives call, error, duration and token metrics (`genai_calls_total`, `genai_duration_seconds`, `genai_tokens_input_total`, `genai_tokens_output_total`) from every GenAI span before sampling. Keep content only when asked, with `keepContent: true`. When a dashboard or backend expects the semantic conventions' names, add `clientMetrics: "derive"` for `gen_ai.client.operation.duration` and `gen_ai.client.token.usage` from spans, or `"passthrough"` when the SDK already records them; never both. Read metric names from `genAiMetrics(options)` (`.client` for the conventions' ones) instead of writing them out. `genAiComponents()` returns the pieces for hand-built pipelines. The token sums and derived client metrics are deltas: `deltaToCumulative: "auto"` puts a `deltatocumulative` processor in front of any metric exporter other than `prometheus` or `debug` (the default with `clientMetrics: "derive"`), `true` always, `false` never.
72
76
 
73
77
  ## Rules
74
78
 
@@ -76,6 +80,8 @@ It deletes prompt, completion, system-instruction and tool-call content from spa
76
80
  - Put `memory_limiter` first in `processors` (OTEL105).
77
81
  - Never write a credential literally. Use `"${env:NAME}"` and the collector reads it at start-up (OTEL002).
78
82
  - Every pipeline needs at least one receiver and one exporter (OTEL102). A declared component no pipeline uses is a warning (OTEL103).
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
+ - 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).
79
85
  - Declared extensions are enabled in declaration order unless a `Service` lists `extensions` itself.
80
86
 
81
87
  ## Starting from an existing config
@@ -36,6 +36,11 @@ export interface TopologyComponent {
36
36
  schema?: SchemaPin;
37
37
  /** Addresses it listens on or sends to, as the config states them. Empty when the config names none. */
38
38
  endpoints: string[];
39
+ /**
40
+ * The wire protocols it speaks, as OTLP SDKs name them (`grpc`, `http/protobuf`, `http/json`).
41
+ * Empty when its definition states none, which a protocol check reads as unknown.
42
+ */
43
+ protocols: string[];
39
44
  /** The pipelines that use it. For a connector, those on either side; for an extension, empty. */
40
45
  pipelines: string[];
41
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"topology.d.ts","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAgD,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AACjH,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AAE5D,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kHAAkH;IAClH,OAAO,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,wGAAwG;IACxG,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iGAAiG;IACjG,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,kEAAkE;IAClE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,oFAAoF;IACpF,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B;;;;OAIG;IACH,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB;;;;OAIG;IACH,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAeD,0CAA0C;AAC1C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,iBAAiB,CA2D5E;AAED,gGAAgG;AAChG,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,iBAAiB,CAE/G"}
1
+ {"version":3,"file":"topology.d.ts","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAgD,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AACjH,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AAE5D,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kHAAkH;IAClH,OAAO,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,wGAAwG;IACxG,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;OAGG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iGAAiG;IACjG,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,kEAAkE;IAClE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,oFAAoF;IACpF,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B;;;;OAIG;IACH,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB;;;;OAIG;IACH,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAyBD,0CAA0C;AAC1C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,iBAAiB,CA4D5E;AAED,gGAAgG;AAChG,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,iBAAiB,CAE/G"}
@@ -10,7 +10,7 @@
10
10
  import type { Declarable } from "@intentius/chant/declarable";
11
11
  import { type CollectorConfig } from "./model.js";
12
12
  import "./components/connectors.js";
13
- export type CollectorIssueCode = "OTEL101" | "OTEL102" | "OTEL103" | "OTEL104" | "OTEL105" | "OTEL106" | "OTEL107" | "OTEL108" | "OTEL109" | "OTEL112";
13
+ export type CollectorIssueCode = "OTEL101" | "OTEL102" | "OTEL103" | "OTEL104" | "OTEL105" | "OTEL106" | "OTEL107" | "OTEL108" | "OTEL109" | "OTEL112" | "OTEL113" | "OTEL114" | "OTEL115" | "OTEL116" | "OTEL117";
14
14
  export interface CollectorIssue {
15
15
  code: CollectorIssueCode;
16
16
  severity: "error" | "warning";
@@ -22,7 +22,11 @@ export interface CollectorIssue {
22
22
  }
23
23
  /**
24
24
  * Check a collector config's references and pipeline shape (OTEL101-OTEL106),
25
- * and each connector's signals against its definition (OTEL112).
25
+ * each connector's signals against its definition (OTEL112), cycles through
26
+ * connectors (OTEL113), connector ids shared with a receiver or exporter
27
+ * (OTEL114), `routing` connector targets (OTEL115), the attributes
28
+ * connectors split metrics by (OTEL116), and started components that listen
29
+ * on the same address (OTEL117).
26
30
  */
27
31
  export declare function validateCollectorConfig(config: CollectorConfig): CollectorIssue[];
28
32
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"validate-config.d.ts","sourceRoot":"","sources":["../src/validate-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,OAAO,EAA4D,KAAK,eAAe,EAA4B,MAAM,SAAS,CAAC;AAGnI,OAAO,yBAAyB,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,EAAE,CA0JjF;AA2CD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,cAAc,EAAE,CAyDpH"}
1
+ {"version":3,"file":"validate-config.d.ts","sourceRoot":"","sources":["../src/validate-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,OAAO,EAA4D,KAAK,eAAe,EAA4B,MAAM,SAAS,CAAC;AAMnI,OAAO,yBAAyB,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,EAAE,CAkKjF;AAobD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,cAAc,EAAE,CAyDpH"}
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAMvF,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAEvF,0DAA0D;AAC1D,eAAO,MAAM,cAAc,UA6B1B,CAAC;AAEF,wBAAsB,QAAQ,IAAI,OAAO,CAAC,cAAc,CAAC,CA2CxD"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAMvF,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAEvF,0DAA0D;AAC1D,eAAO,MAAM,cAAc,UAsC1B,CAAC;AAEF,wBAAsB,QAAQ,IAAI,OAAO,CAAC,cAAc,CAAC,CAmDxD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-otel",
3
- "version": "0.100.0",
3
+ "version": "0.101.0",
4
4
  "description": "OpenTelemetry Collector lexicon for chant — typed receivers, processors, exporters, connectors, extensions and pipelines, serialized to collector YAML",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -68,7 +68,7 @@
68
68
  "typescript": "^5.9.3"
69
69
  },
70
70
  "peerDependencies": {
71
- "@intentius/chant": "^0.100.0",
71
+ "@intentius/chant": "^0.101.0",
72
72
  "typescript": "^5.9.3"
73
73
  }
74
74
  }
@@ -35,10 +35,10 @@ export const traces = new Pipeline({
35
35
  The built-in component set is otlp, prometheus, hostmetrics, filelog,
36
36
  k8s_cluster and kubeletstats receivers; batch, memory_limiter, resource,
37
37
  attributes, k8sattributes, resourcedetection, filter, transform, redaction,
38
- tail_sampling and probabilistic_sampler processors; otlp, otlphttp, debug,
38
+ tail_sampling, probabilistic_sampler and deltatocumulative processors; otlp, otlphttp, debug,
39
39
  prometheus, googlecloud and loadbalancing exporters; spanmetrics,
40
- servicegraph, routing, forward, count and sum connectors; and the
41
- health_check, pprof and zpages extensions. \`genAiPipeline()\` is a preset for
40
+ servicegraph, routing, forward, count, sum and signaltometrics connectors; and the
41
+ health_check, pprof, zpages and k8s_leader_elector extensions. \`genAiPipeline()\` is a preset for
42
42
  GenAI workloads: it removes prompt and completion content and derives agent
43
43
  RED and token metrics.
44
44
  Their config types follow the collector-contrib release named on the
@@ -50,7 +50,12 @@ TypeScript (see Importing a Collector Config).
50
50
  Checks catch a pipeline that uses a component nobody declared (OTEL101), a
51
51
  pipeline with no receivers or exporters (OTEL102), a declared component no
52
52
  pipeline uses (OTEL103), a connector joining pipelines of signals it can't
53
- convert (OTEL112), and a literal credential in source (OTEL002).
53
+ convert (OTEL112), pipelines feeding each other in a cycle (OTEL113), a
54
+ connector id shared with a receiver or exporter (OTEL114), a \`routing\` route
55
+ to a pipeline that doesn't receive from it (OTEL115), a connector
56
+ splitting metrics by a per-request GenAI attribute such as
57
+ \`gen_ai.conversation.id\` (OTEL116), two started components listening
58
+ on the same address (OTEL117), and a literal credential in source (OTEL002).
54
59
  `;
55
60
 
56
61
  const outputFormat = `The otel lexicon serializes every otel entity in a build into **one
@@ -13,6 +13,23 @@
13
13
  */
14
14
  export type Duration = string | number;
15
15
 
16
+ const GO_DURATION_UNITS: Record<string, number> = { ns: 1e-6, us: 1e-3, "µs": 1e-3, "μs": 1e-3, ms: 1, s: 1e3, m: 60e3, h: 3600e3 };
17
+
18
+ /** A Go duration in milliseconds, `fallback` when unset, or undefined when it isn't one this can read. */
19
+ export function goDurationMs(d: Duration | undefined, fallback: number): number | undefined {
20
+ if (d === undefined) return fallback;
21
+ if (typeof d === "number") return d / 1e6;
22
+ // time.ParseDuration takes a bare "0" without a unit.
23
+ if (/^[+-]?0$/.test(d.trim())) return 0;
24
+ const m = d.trim().match(/^([+-]?)((?:\d+(?:\.\d*)?|\.\d+)(?:ns|us|µs|μs|ms|s|m|h))+$/);
25
+ if (!m) return undefined;
26
+ let total = 0;
27
+ for (const part of d.trim().matchAll(/(\d+(?:\.\d*)?|\.\d+)(ns|us|µs|μs|ms|s|m|h)/g)) {
28
+ total += Number(part[1]) * GO_DURATION_UNITS[part[2]];
29
+ }
30
+ return m[1] === "-" ? -total : total;
31
+ }
32
+
16
33
  export interface TLSClientSettings {
17
34
  insecure?: boolean;
18
35
  insecure_skip_verify?: boolean;
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Built-in connectors: spanmetrics, servicegraph, routing, forward, count, sum.
2
+ * Built-in connectors: spanmetrics, servicegraph, routing, forward, count, sum,
3
+ * signaltometrics.
3
4
  *
4
5
  * A connector is an exporter in one pipeline and a receiver in another. Put
5
6
  * the same entity in `exporters` of the pipeline that feeds it and in
@@ -274,3 +275,210 @@ export const SumConnector = defineBuiltin<SumConnectorConfig, "connector", "sum"
274
275
  return problems;
275
276
  },
276
277
  });
278
+
279
+ // ── signaltometrics ──────────────────────────────────────────────────
280
+ //
281
+ // Typed against connector/signaltometricsconnector at COLLECTOR_PIN
282
+ // (collector-contrib v0.130.0): config/config.go and README.md at that tag,
283
+ // https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.130.0/connector/signaltometricsconnector
284
+ // The collector type is `signaltometrics`. Upstream marks every signal pair
285
+ // alpha. At v0.130.0 there is no span event section; spans, data points,
286
+ // logs and profiles each take a list of metrics.
287
+
288
+ /** The input sections of a `signaltometrics` connector, one per signal it reads. */
289
+ export const SIGNAL_TO_METRICS_SIGNALS = ["spans", "datapoints", "logs", "profiles"] as const;
290
+ export type SignalToMetricsSignal = (typeof SIGNAL_TO_METRICS_SIGNALS)[number];
291
+
292
+ /** The metric types a `signaltometrics` entry can produce. Each entry names exactly one. */
293
+ export const SIGNAL_TO_METRICS_TYPES = ["sum", "gauge", "histogram", "exponential_histogram"] as const;
294
+ export type SignalToMetricsType = (typeof SIGNAL_TO_METRICS_TYPES)[number];
295
+
296
+ /**
297
+ * The connector's default explicit histogram bounds, used when `histogram`
298
+ * sets no `buckets`. They are plain numbers in whatever unit `value` returns,
299
+ * not durations.
300
+ */
301
+ export const SIGNAL_TO_METRICS_DEFAULT_BUCKETS: readonly number[] = Object.freeze([
302
+ 2, 4, 6, 8, 10, 50, 100, 200, 400, 800, 1000, 1400, 2000, 5000, 10_000, 15_000,
303
+ ]);
304
+
305
+ /**
306
+ * An attribute of the produced metric.
307
+ *
308
+ * In `attributes`: without `default_value` or `optional`, an item lacking the
309
+ * attribute is not counted at all; with `default_value`, it is counted under
310
+ * that value; with `optional: true`, it is counted without the attribute.
311
+ * Set at most one of the two.
312
+ *
313
+ * In `include_resource_attributes`: an include list. `default_value` fills a
314
+ * missing resource attribute; `optional` changes nothing.
315
+ */
316
+ export interface SignalToMetricsAttribute {
317
+ key: string;
318
+ default_value?: string | number | boolean;
319
+ optional?: boolean;
320
+ }
321
+
322
+ /** A delta sum of `value` over the items in each batch. */
323
+ export interface SignalToMetricsSum {
324
+ /** OTTL value expression; its result type (int or double) is the sum's. A constant is a string: `"1"`. */
325
+ value: string;
326
+ }
327
+
328
+ /** The last `value` seen in each batch. */
329
+ export interface SignalToMetricsGauge {
330
+ /** OTTL value expression. With `ExtractGrokPatterns`, select one key: `ExtractGrokPatterns(...)["key"]`. */
331
+ value: string;
332
+ }
333
+
334
+ /** An explicit-bucket histogram of `value`. */
335
+ export interface SignalToMetricsHistogram {
336
+ /** Upper bounds, increasing, in the unit `value` returns. Default `SIGNAL_TO_METRICS_DEFAULT_BUCKETS`. */
337
+ buckets?: number[];
338
+ /** OTTL value expression for how many observations each item counts as. Default: one per item. */
339
+ count?: string;
340
+ /** OTTL value expression for the observed value, e.g. `Seconds(end_time - start_time)` or an attribute. */
341
+ value: string;
342
+ }
343
+
344
+ /** A base-2 exponential histogram of `value`. */
345
+ export interface SignalToMetricsExponentialHistogram {
346
+ /** Buckets per positive or negative range, 2 to 16384. Default 160. */
347
+ max_size?: number;
348
+ /** OTTL value expression for how many observations each item counts as. Default: one per item. */
349
+ count?: string;
350
+ /** OTTL value expression for the observed value. */
351
+ value: string;
352
+ }
353
+
354
+ /** What every `signaltometrics` entry has, whatever metric type it produces. */
355
+ export interface SignalToMetricsMetricBase {
356
+ /** The OTLP metric name, used as written. */
357
+ name: string;
358
+ description?: string;
359
+ /** The metric's unit, e.g. `s` or `{token}`. */
360
+ unit?: string;
361
+ /** Resource attributes the metric keeps. Unset or empty keeps them all. */
362
+ include_resource_attributes?: SignalToMetricsAttribute[];
363
+ /** Attributes of the item (span, data point, log record) the metric is split by. Unset: no attributes. */
364
+ attributes?: SignalToMetricsAttribute[];
365
+ /** OTTL conditions, ORed; an item counts when any matches. Unset counts every item. */
366
+ conditions?: string[];
367
+ }
368
+
369
+ type OneMetricType<K extends SignalToMetricsType, V> = { [P in K]: V } & { [P in Exclude<SignalToMetricsType, K>]?: never };
370
+
371
+ /** One metric a `signaltometrics` connector produces: a name and exactly one metric type. */
372
+ export type SignalToMetricsMetric = SignalToMetricsMetricBase &
373
+ (
374
+ | OneMetricType<"sum", SignalToMetricsSum>
375
+ | OneMetricType<"gauge", SignalToMetricsGauge>
376
+ | OneMetricType<"histogram", SignalToMetricsHistogram>
377
+ | OneMetricType<"exponential_histogram", SignalToMetricsExponentialHistogram>
378
+ );
379
+
380
+ /**
381
+ * The metrics to produce, per input signal. The connector emits delta
382
+ * temporality and aggregates only within each batch it receives, so an
383
+ * exporter that needs cumulative input (Prometheus remote write) wants a
384
+ * `deltatocumulative` processor in front of it.
385
+ */
386
+ export interface SignalToMetricsConnectorConfig {
387
+ spans?: SignalToMetricsMetric[];
388
+ datapoints?: SignalToMetricsMetric[];
389
+ logs?: SignalToMetricsMetric[];
390
+ profiles?: SignalToMetricsMetric[];
391
+ }
392
+
393
+ /** One entry of a `signaltometrics` config, with where it sits. */
394
+ export interface SignalToMetricsEntry {
395
+ signal: SignalToMetricsSignal;
396
+ index: number;
397
+ metric: SignalToMetricsMetric;
398
+ /** The metric types the entry names. A valid entry names exactly one. */
399
+ types: SignalToMetricsType[];
400
+ }
401
+
402
+ /** Every metric entry of a `signaltometrics` config, in section order. Reads untyped config too. */
403
+ export function signalToMetricsEntries(config: SignalToMetricsConnectorConfig): SignalToMetricsEntry[] {
404
+ const out: SignalToMetricsEntry[] = [];
405
+ for (const signal of SIGNAL_TO_METRICS_SIGNALS) {
406
+ const list = (config as Record<string, unknown>)?.[signal];
407
+ if (!Array.isArray(list)) continue;
408
+ list.forEach((metric, index) => {
409
+ const m = (metric ?? {}) as Record<string, unknown>;
410
+ const types = SIGNAL_TO_METRICS_TYPES.filter((t) => m[t] !== undefined && m[t] !== null);
411
+ out.push({ signal, index, metric: m as unknown as SignalToMetricsMetric, types });
412
+ });
413
+ }
414
+ return out;
415
+ }
416
+
417
+ const GROK_KEY_SELECTOR = /ExtractGrokPatterns\([^)]*\)\s*\[[^\]]+\]/;
418
+
419
+ /** The checks config/config.go makes at v0.130.0 that need no OTTL parser, plus increasing buckets. */
420
+ function validateSignalToMetrics(c: SignalToMetricsConnectorConfig): string[] {
421
+ const problems: string[] = [];
422
+ const entries = signalToMetricsEntries(c);
423
+ if (entries.length === 0) {
424
+ problems.push("no metric is configured under spans, datapoints, logs or profiles; the collector refuses the connector");
425
+ }
426
+ for (const { signal, index, metric, types } of entries) {
427
+ const m = metric as unknown as SignalToMetricsMetricBase & Partial<Record<SignalToMetricsType, Record<string, unknown>>>;
428
+ const at = m.name ? `${signal}[${index}] (${m.name})` : `${signal}[${index}]`;
429
+ if (!m.name) problems.push(`${at}: name is missing`);
430
+ if (types.length !== 1) {
431
+ problems.push(
432
+ `${at}: name exactly one metric type (sum, gauge, histogram or exponential_histogram); ${types.length === 0 ? "found none" : `found ${types.join(" and ")}`}`,
433
+ );
434
+ }
435
+ for (const t of types) {
436
+ const body = m[t];
437
+ if (typeof body?.value !== "string" || body.value === "") problems.push(`${at}: ${t}.value is missing`);
438
+ }
439
+ const buckets = m.histogram?.buckets;
440
+ if (Array.isArray(buckets) && buckets.some((b, i) => i > 0 && !((b as number) > (buckets[i - 1] as number)))) {
441
+ problems.push(`${at}: histogram.buckets must increase`);
442
+ }
443
+ const maxSize = m.exponential_histogram?.max_size;
444
+ if (typeof maxSize === "number" && maxSize !== 0 && (maxSize < 2 || maxSize > 16384)) {
445
+ problems.push(`${at}: exponential_histogram.max_size must be between 2 and 16384`);
446
+ }
447
+ const gauge = m.gauge?.value;
448
+ if (typeof gauge === "string" && gauge.includes("ExtractGrokPatterns") && !GROK_KEY_SELECTOR.test(gauge)) {
449
+ problems.push(`${at}: gauge.value with ExtractGrokPatterns needs one key selector, ExtractGrokPatterns(...)["key"]`);
450
+ }
451
+ const seen = new Set<string>();
452
+ (m.attributes ?? []).forEach((a, i) => {
453
+ if (!a?.key) {
454
+ problems.push(`${at}: attributes[${i}]: key is missing`);
455
+ return;
456
+ }
457
+ if (a.default_value !== undefined && a.optional) {
458
+ problems.push(`${at}: attributes "${a.key}": set default_value or optional, not both`);
459
+ }
460
+ if (seen.has(a.key)) problems.push(`${at}: attributes "${a.key}" is listed twice`);
461
+ seen.add(a.key);
462
+ });
463
+ }
464
+ return problems;
465
+ }
466
+
467
+ /**
468
+ * Builds metrics from spans, data points, logs or profiles, each with the
469
+ * name, type, unit, attributes and conditions you choose. A histogram, sum or
470
+ * gauge takes its value from an OTTL expression, such as an attribute or the
471
+ * span's duration.
472
+ */
473
+ export const SignalToMetricsConnector = defineBuiltin<SignalToMetricsConnectorConfig, "connector", "signaltometrics">({
474
+ kind: "connector",
475
+ type: "signaltometrics",
476
+ description: "Builds named sum, gauge and histogram metrics from spans, data points, logs or profiles, with values and conditions in OTTL",
477
+ connects: [
478
+ { from: "traces", to: "metrics" },
479
+ { from: "metrics", to: "metrics" },
480
+ { from: "logs", to: "metrics" },
481
+ { from: "profiles", to: "metrics" },
482
+ ],
483
+ validate: validateSignalToMetrics,
484
+ });
@@ -16,6 +16,7 @@ export const OtlpExporter = defineBuiltin<OtlpExporterConfig, "exporter", "otlp"
16
16
  description: "Sends traces, metrics and logs over OTLP gRPC",
17
17
  validate: (c) => (c.endpoint ? [] : ["endpoint is empty"]),
18
18
  endpoints: (c) => (c.endpoint ? [c.endpoint] : []),
19
+ protocols: () => ["grpc"],
19
20
  });
20
21
 
21
22
  // ── otlphttp ─────────────────────────────────────────────────────────
@@ -39,6 +40,7 @@ export const OtlpHttpExporter = defineBuiltin<OtlpHttpExporterConfig, "exporter"
39
40
  : ["set endpoint, or at least one of traces_endpoint, metrics_endpoint and logs_endpoint"],
40
41
  endpoints: (c) =>
41
42
  [c.endpoint, c.traces_endpoint, c.metrics_endpoint, c.logs_endpoint].filter((e): e is string => typeof e === "string" && e !== ""),
43
+ protocols: (c) => [c.encoding === "json" ? "http/json" : "http/protobuf"],
42
44
  });
43
45
 
44
46
  // ── debug ────────────────────────────────────────────────────────────