@intentius/chant-lexicon-otel 0.106.4 → 0.108.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 (86) hide show
  1. package/README.md +1 -1
  2. package/dist/collector.d.ts +6 -0
  3. package/dist/collector.d.ts.map +1 -1
  4. package/dist/composites/catalog.d.ts.map +1 -1
  5. package/dist/composites/genai-pipeline.d.ts +33 -0
  6. package/dist/composites/genai-pipeline.d.ts.map +1 -0
  7. package/dist/composites/index.d.ts +8 -0
  8. package/dist/composites/index.d.ts.map +1 -1
  9. package/dist/composites/otlp-collector.d.ts +43 -0
  10. package/dist/composites/otlp-collector.d.ts.map +1 -0
  11. package/dist/composites/red-metrics.d.ts +77 -0
  12. package/dist/composites/red-metrics.d.ts.map +1 -0
  13. package/dist/composites/tail-sampling-tier.d.ts +90 -0
  14. package/dist/composites/tail-sampling-tier.d.ts.map +1 -0
  15. package/dist/configmap.d.ts +16 -0
  16. package/dist/configmap.d.ts.map +1 -1
  17. package/dist/genai.d.ts +19 -0
  18. package/dist/genai.d.ts.map +1 -1
  19. package/dist/import/embedded.d.ts +8 -0
  20. package/dist/import/embedded.d.ts.map +1 -1
  21. package/dist/index.d.ts +8 -5
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/integrity.json +3 -3
  24. package/dist/lint/post-synth/otel-helpers.d.ts +9 -2
  25. package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
  26. package/dist/manifest.json +1 -1
  27. package/dist/metric-names.d.ts +68 -0
  28. package/dist/metric-names.d.ts.map +1 -1
  29. package/dist/op/activities/collector-audit.d.ts +95 -0
  30. package/dist/op/activities/collector-audit.d.ts.map +1 -0
  31. package/dist/op/activities/collector-health.d.ts +74 -0
  32. package/dist/op/activities/collector-health.d.ts.map +1 -0
  33. package/dist/op/activities/exec.d.ts +19 -0
  34. package/dist/op/activities/exec.d.ts.map +1 -0
  35. package/dist/op/activities/github.d.ts +55 -0
  36. package/dist/op/activities/github.d.ts.map +1 -0
  37. package/dist/op/activities/index.d.ts +20 -0
  38. package/dist/op/activities/index.d.ts.map +1 -0
  39. package/dist/op/activities/otelcol.d.ts +96 -0
  40. package/dist/op/activities/otelcol.d.ts.map +1 -0
  41. package/dist/op/activity-contracts.d.ts +96 -0
  42. package/dist/op/activity-contracts.d.ts.map +1 -0
  43. package/dist/op/audit-op.d.ts +35 -0
  44. package/dist/op/audit-op.d.ts.map +1 -0
  45. package/dist/op/builders.d.ts +43 -0
  46. package/dist/op/builders.d.ts.map +1 -0
  47. package/dist/platform.d.ts +14 -0
  48. package/dist/platform.d.ts.map +1 -1
  49. package/dist/rules/otel-helpers.ts +18 -5
  50. package/package.json +14 -3
  51. package/src/collector.ts +9 -0
  52. package/src/composites/catalog.test.ts +1 -1
  53. package/src/composites/catalog.ts +292 -0
  54. package/src/composites/genai-pipeline.test.ts +53 -0
  55. package/src/composites/genai-pipeline.ts +40 -0
  56. package/src/composites/index.ts +8 -0
  57. package/src/composites/otlp-collector.test.ts +43 -0
  58. package/src/composites/otlp-collector.ts +70 -0
  59. package/src/composites/red-metrics.test.ts +131 -0
  60. package/src/composites/red-metrics.ts +148 -0
  61. package/src/composites/tail-sampling-tier.test.ts +100 -0
  62. package/src/composites/tail-sampling-tier.ts +182 -0
  63. package/src/configmap-operator.test.ts +44 -0
  64. package/src/configmap.ts +35 -0
  65. package/src/detect.ts +13 -1
  66. package/src/genai.ts +72 -59
  67. package/src/import/embedded.test.ts +36 -0
  68. package/src/import/embedded.ts +28 -4
  69. package/src/index.ts +57 -2
  70. package/src/lint/post-synth/otel-helpers.ts +18 -5
  71. package/src/metric-names.ts +141 -0
  72. package/src/op/activities/collector-audit.test.ts +166 -0
  73. package/src/op/activities/collector-audit.ts +265 -0
  74. package/src/op/activities/collector-health.test.ts +115 -0
  75. package/src/op/activities/collector-health.ts +211 -0
  76. package/src/op/activities/exec.ts +33 -0
  77. package/src/op/activities/github.ts +127 -0
  78. package/src/op/activities/index.ts +19 -0
  79. package/src/op/activities/otelcol.test.ts +162 -0
  80. package/src/op/activities/otelcol.ts +233 -0
  81. package/src/op/activity-contracts.test.ts +103 -0
  82. package/src/op/activity-contracts.ts +92 -0
  83. package/src/op/audit-op.ts +53 -0
  84. package/src/op/builders.ts +52 -0
  85. package/src/op/testdata/otelcol-components-v0.130.0.yaml +82 -0
  86. package/src/platform.ts +45 -8
@@ -0,0 +1,182 @@
1
+ /**
2
+ * `TailSamplingTier`: the collector config of a tail sampling tier on any
3
+ * host (VMs, Docker, Fly, ECS), for where the k8s lexicon's
4
+ * `OtelCollectorGateway` does not run.
5
+ *
6
+ * Tail sampling needs every span of a trace in one collector. The agents in
7
+ * front send to this tier through a `loadbalancing` exporter that routes by
8
+ * trace id (`tailSamplingLoadBalancer()` declares it), and each replica of
9
+ * the tier holds a trace for `decisionWait`, then keeps it when a policy
10
+ * says so:
11
+ *
12
+ * - `errors`: any span ended in error (default: on);
13
+ * - `slowerThanMs`: the trace took at least this long (default 1000 ms);
14
+ * - `percentage`: a baseline share of everything else (default 10%);
15
+ * - `policies`: more `tail_sampling` policies of your own, after those.
16
+ *
17
+ * The pipeline is `otlp` in, `memory_limiter`, `tail_sampling`, `batch`, the
18
+ * given exporters out. Compute span metrics before this tier (the agents, or
19
+ * `RedMetrics`), or they count only the traces kept.
20
+ */
21
+
22
+ import { Composite, type CompositeInstance } from "@intentius/chant/composite";
23
+ import { goDurationMs, type Duration } from "../components/common";
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
+ LoadBalancingExporter,
30
+ TailSamplingProcessor,
31
+ type LoadBalancingExporterConfig,
32
+ type LoadBalancingResolver,
33
+ type TailSamplingPolicy,
34
+ type TailSamplingProcessorConfig,
35
+ } from "../components/sampling";
36
+ import type { OTelComponent } from "../define";
37
+ import { Pipeline, type PipelineEntity } from "../pipeline";
38
+
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ type Exporter = OTelComponent<"exporter", string, any>;
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ type TraceSource = OTelComponent<"receiver" | "connector", string, any>;
43
+ type Component<K extends "receiver" | "processor" | "exporter" | "extension"> = OTelComponent<K, string, object>;
44
+
45
+ export interface TailSamplingTierProps {
46
+ /** Where the kept traces go. Default: one `debug` exporter at `basic` verbosity. */
47
+ exporters?: Exporter[];
48
+ /** Where spans come from. Default: an `otlp` receiver on 4317 (gRPC) and 4318 (HTTP). */
49
+ receivers?: TraceSource[];
50
+ /** How long after a trace's first span the decision is made (default `10s`). */
51
+ decisionWait?: Duration;
52
+ /** Keep every trace with a span ended in error (default: on). */
53
+ errors?: boolean;
54
+ /** Keep every trace that took at least this many milliseconds (default 1000; `false` for none). */
55
+ slowerThanMs?: number | false;
56
+ /** Keep this percentage of all other traces, by trace id hash (default 10; `false` for none). */
57
+ percentage?: number | false;
58
+ /** More policies, evaluated after the ones above. */
59
+ policies?: TailSamplingPolicy[];
60
+ /** How many traces to hold in memory at once (default: the collector's 50000). */
61
+ numTraces?: number;
62
+ /** `memory_limiter`'s hard limit in MiB (default: 80% of the container's memory, with a 20% spike limit). */
63
+ memoryLimitMib?: number;
64
+ /** Serve `health_check` on 0.0.0.0:13133 (default: on). */
65
+ healthCheck?: boolean;
66
+ }
67
+
68
+ // A type alias, not an interface: a composite's members type needs the implicit index signature.
69
+ export type TailSamplingTierMembers = {
70
+ otlp?: Component<"receiver">;
71
+ memoryLimiter: Component<"processor">;
72
+ tailSampling: OTelComponent<"processor", "tail_sampling", TailSamplingProcessorConfig>;
73
+ batch: Component<"processor">;
74
+ /** The default `debug` exporter, when no `exporters` are given. */
75
+ debug?: Component<"exporter">;
76
+ health?: Component<"extension">;
77
+ traces: PipelineEntity;
78
+ };
79
+
80
+ export type TailSamplingTierInstance = CompositeInstance<TailSamplingTierMembers> & TailSamplingTierMembers;
81
+
82
+ /** Why a set of `TailSamplingTier` props can't build, or undefined when they can. */
83
+ export function tailSamplingTierPropsProblem(props: TailSamplingTierProps = {}): string | undefined {
84
+ if (props.exporters !== undefined && props.exporters.length === 0) return "exporters must name at least one exporter when set";
85
+ if (props.receivers !== undefined && props.receivers.length === 0) return "receivers must name at least one receiver when set";
86
+ if (props.decisionWait !== undefined) {
87
+ const ms = goDurationMs(props.decisionWait, 0);
88
+ if (ms === undefined || ms <= 0) return `decisionWait ${JSON.stringify(props.decisionWait)} is not a positive duration`;
89
+ }
90
+ const s = props.slowerThanMs;
91
+ if (s !== undefined && s !== false && !(Number.isInteger(s) && s > 0)) return "slowerThanMs must be a positive whole number of milliseconds, or false";
92
+ const pc = props.percentage;
93
+ if (pc !== undefined && pc !== false && !(typeof pc === "number" && pc > 0 && pc <= 100)) return "percentage must be above 0 and at most 100, or false";
94
+ if (props.numTraces !== undefined && !(Number.isInteger(props.numTraces) && props.numTraces > 0)) return "numTraces must be a positive whole number";
95
+ if (props.memoryLimitMib !== undefined && !(Number.isInteger(props.memoryLimitMib) && props.memoryLimitMib > 0)) {
96
+ return "memoryLimitMib must be a positive whole number";
97
+ }
98
+ if (tierPolicies(props).length === 0) return "every policy is off, so no trace would ever be kept";
99
+ return undefined;
100
+ }
101
+
102
+ function tierPolicies(props: TailSamplingTierProps): TailSamplingPolicy[] {
103
+ const out: TailSamplingPolicy[] = [];
104
+ if (props.errors !== false) out.push({ name: "errors", type: "status_code", status_code: { status_codes: ["ERROR"] } });
105
+ const slow = props.slowerThanMs ?? 1000;
106
+ if (slow !== false) out.push({ name: "slow", type: "latency", latency: { threshold_ms: slow } });
107
+ const pc = props.percentage ?? 10;
108
+ if (pc !== false) out.push({ name: "baseline", type: "probabilistic", probabilistic: { sampling_percentage: pc } });
109
+ return [...out, ...(props.policies ?? [])];
110
+ }
111
+
112
+ /**
113
+ * The collector config of a tail sampling tier: whole traces in, the ones
114
+ * a policy keeps out.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * import { TailSamplingTier, OtlpExporter } from "@intentius/chant-lexicon-otel";
119
+ *
120
+ * const tempo = new OtlpExporter({ name: "tempo", endpoint: "tempo:4317", tls: { insecure: true } });
121
+ * export const sampler = TailSamplingTier({ exporters: [tempo], slowerThanMs: 500, percentage: 5 });
122
+ * ```
123
+ */
124
+ export const TailSamplingTier = Composite<TailSamplingTierProps, TailSamplingTierMembers>((input) => {
125
+ const props = input ?? {};
126
+ const problem = tailSamplingTierPropsProblem(props);
127
+ if (problem) throw new Error(`TailSamplingTier: ${problem}`);
128
+
129
+ const otlp = props.receivers
130
+ ? undefined
131
+ : new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" }, http: { endpoint: "0.0.0.0:4318" } } });
132
+ const memoryLimiter = new MemoryLimiterProcessor(
133
+ props.memoryLimitMib !== undefined
134
+ ? { check_interval: "1s", limit_mib: props.memoryLimitMib, spike_limit_mib: Math.ceil(props.memoryLimitMib / 4) }
135
+ : { check_interval: "1s", limit_percentage: 80, spike_limit_percentage: 20 },
136
+ );
137
+ const tailSampling = new TailSamplingProcessor({
138
+ decision_wait: props.decisionWait ?? "10s",
139
+ ...(props.numTraces !== undefined ? { num_traces: props.numTraces } : {}),
140
+ policies: tierPolicies(props),
141
+ });
142
+ const batch = new BatchProcessor({});
143
+ const debug = props.exporters ? undefined : new DebugExporter({ verbosity: "basic" });
144
+ const traces = new Pipeline({
145
+ signal: "traces",
146
+ receivers: props.receivers ?? [otlp!],
147
+ processors: [memoryLimiter, tailSampling, batch],
148
+ exporters: props.exporters ?? [debug!],
149
+ });
150
+ const health = props.healthCheck === false ? undefined : new HealthCheckExtension({ endpoint: "0.0.0.0:13133" });
151
+
152
+ // A composite member must be a declarable, so the parts that are off are left out rather than undefined.
153
+ const members: TailSamplingTierMembers = { memoryLimiter, tailSampling, batch, traces };
154
+ if (otlp) members.otlp = otlp;
155
+ if (debug) members.debug = debug;
156
+ if (health) members.health = health;
157
+ return members;
158
+ }, "TailSamplingTier");
159
+
160
+ /**
161
+ * The exporter the agents in front of a `TailSamplingTier` send traces with:
162
+ * a `loadbalancing` exporter that routes every span of a trace to the same
163
+ * replica of the tier, by trace id.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * // TLS by default; pass `{ otlp: { tls: { insecure: true } } }` as `protocol` on a private network.
168
+ * const toSampler = tailSamplingLoadBalancer({ dns: { hostname: "sampler.internal" } }, "sampler");
169
+ * ```
170
+ */
171
+ export function tailSamplingLoadBalancer(
172
+ resolver: LoadBalancingResolver,
173
+ name?: string,
174
+ protocol?: LoadBalancingExporterConfig["protocol"],
175
+ ): OTelComponent<"exporter", "loadbalancing", LoadBalancingExporterConfig> {
176
+ return new LoadBalancingExporter({
177
+ ...(name !== undefined ? { name } : {}),
178
+ routing_key: "traceID",
179
+ ...(protocol !== undefined ? { protocol } : {}),
180
+ resolver,
181
+ });
182
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { collectorConfig, describeOperatorCollectorConfig, operatorCollectorConfig } from "./index";
3
+ import { OtlpReceiver, DebugExporter, Pipeline } from "./index";
4
+
5
+ const config = {
6
+ receivers: { otlp: { protocols: { grpc: {} } } },
7
+ exporters: { debug: {} },
8
+ service: { pipelines: { traces: { receivers: ["otlp"], exporters: ["debug"] } } },
9
+ };
10
+
11
+ const cr = (spec: unknown, apiVersion = "opentelemetry.io/v1beta1") => ({
12
+ apiVersion,
13
+ kind: "OpenTelemetryCollector",
14
+ metadata: { name: "gw", namespace: "obs" },
15
+ spec,
16
+ });
17
+
18
+ describe("operatorCollectorConfig (#3367)", () => {
19
+ test("reads spec.config as an object (v1beta1) or as YAML text (v1alpha1)", () => {
20
+ expect(operatorCollectorConfig(cr({ config }))).toEqual({ namespace: "obs", name: "gw", config });
21
+ const text = "receivers: { otlp: {} }\nexporters: { debug: {} }\nservice:\n pipelines:\n traces: { receivers: [otlp], exporters: [debug] }\n";
22
+ const old = operatorCollectorConfig(cr({ config: text }, "opentelemetry.io/v1alpha1"));
23
+ expect(old?.config.service?.pipelines?.traces).toEqual({ receivers: ["otlp"], exporters: ["debug"] });
24
+ });
25
+
26
+ test("defaults the namespace and skips other kinds, other groups and configs without pipelines", () => {
27
+ expect(operatorCollectorConfig({ ...cr({ config }), metadata: { name: "x" } })?.namespace).toBe("default");
28
+ expect(operatorCollectorConfig({ ...cr({ config }), kind: "Deployment" })).toBeUndefined();
29
+ expect(operatorCollectorConfig(cr({ config }, "example.com/v1"))).toBeUndefined();
30
+ expect(operatorCollectorConfig(cr({ config: { receivers: {} } }))).toBeUndefined();
31
+ expect(operatorCollectorConfig(cr({}))).toBeUndefined();
32
+ });
33
+
34
+ test("describes the CR in a message", () => {
35
+ expect(describeOperatorCollectorConfig({ namespace: "obs", name: "gw" })).toBe("OpenTelemetryCollector obs/gw, spec.config");
36
+ });
37
+ });
38
+
39
+ describe("collectorConfig", () => {
40
+ test("is the config object collectorYaml prints", () => {
41
+ const entities = [new Pipeline({ signal: "traces", receivers: [new OtlpReceiver({ protocols: { grpc: {} } })], exporters: [new DebugExporter({})] })];
42
+ expect(collectorConfig(entities).service?.pipelines?.traces).toEqual({ receivers: ["otlp"], exporters: ["debug"] });
43
+ });
44
+ });
package/src/configmap.ts CHANGED
@@ -65,3 +65,38 @@ export function configMapCollectorConfigs(doc: unknown): ConfigMapCollectorConfi
65
65
  export function describeConfigMapConfig(c: Pick<ConfigMapCollectorConfig, "namespace" | "name" | "key">): string {
66
66
  return `ConfigMap ${c.namespace}/${c.name}, key ${c.key}`;
67
67
  }
68
+
69
+ /** One collector config found in an OpenTelemetry Operator `OpenTelemetryCollector`. */
70
+ export interface OperatorCollectorConfig {
71
+ /** `metadata.namespace`, or `default` when unset. */
72
+ namespace: string;
73
+ name: string;
74
+ config: CollectorConfig;
75
+ }
76
+
77
+ /**
78
+ * The collector config in one parsed manifest when it is an OpenTelemetry
79
+ * Operator `OpenTelemetryCollector`: `spec.config`, an object in v1beta1 and
80
+ * YAML text in v1alpha1. Undefined for any other document, and for a config
81
+ * without a `service.pipelines` map.
82
+ */
83
+ export function operatorCollectorConfig(doc: unknown): OperatorCollectorConfig | undefined {
84
+ if (!isRecord(doc) || doc.kind !== "OpenTelemetryCollector") return undefined;
85
+ if (typeof doc.apiVersion !== "string" || !doc.apiVersion.startsWith("opentelemetry.io/")) return undefined;
86
+ const metadata = isRecord(doc.metadata) ? doc.metadata : {};
87
+ if (typeof metadata.name !== "string") return undefined;
88
+ const raw = isRecord(doc.spec) ? doc.spec.config : undefined;
89
+ const config = isRecord(raw)
90
+ ? isRecord(raw.service) && isRecord(raw.service.pipelines)
91
+ ? (raw as unknown as CollectorConfig)
92
+ : undefined
93
+ : parseCollectorConfig(raw);
94
+ if (!config) return undefined;
95
+ const namespace = typeof metadata.namespace === "string" && metadata.namespace.length > 0 ? metadata.namespace : "default";
96
+ return { namespace, name: metadata.name, config };
97
+ }
98
+
99
+ /** How an operator CR's config is named in a message: `OpenTelemetryCollector observability/gateway, spec.config`. */
100
+ export function describeOperatorCollectorConfig(c: Pick<OperatorCollectorConfig, "namespace" | "name">): string {
101
+ return `OpenTelemetryCollector ${c.namespace}/${c.name}, spec.config`;
102
+ }
package/src/detect.ts CHANGED
@@ -7,5 +7,17 @@
7
7
  import { looksLikeCollectorConfig } from "./model";
8
8
 
9
9
  export function detectTemplate(data: unknown): boolean {
10
- return looksLikeCollectorConfig(data);
10
+ return looksLikeCollectorConfig(data) || isObjectConfigOffer(data);
11
+ }
12
+
13
+ /**
14
+ * The `{ config, header }` document a host offers for a config it holds as an
15
+ * object, such as an OpenTelemetryCollector's `spec.config` (#3367). `chant
16
+ * import` asks this module which lexicons own what a host offered, so the
17
+ * wrapper has to be recognised here as well as by the importer.
18
+ */
19
+ function isObjectConfigOffer(data: unknown): boolean {
20
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
21
+ const keys = Object.keys(data);
22
+ return keys.includes("config") && keys.every((k) => k === "config" || k === "header") && looksLikeCollectorConfig((data as { config?: unknown }).config);
11
23
  }
package/src/genai.ts CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  type SumConnectorConfig,
64
64
  } from "./components/connectors";
65
65
  import type { Duration } from "./components/common";
66
- import { Pipeline } from "./pipeline";
66
+ import { Pipeline, type PipelineEntity } from "./pipeline";
67
67
  import { prometheusMetricName, SPANMETRICS_DEFAULT_DIMENSIONS, type CollectorMetric } from "./metric-names";
68
68
 
69
69
  // ── The GenAI attribute vocabulary, at GENAI_SEMCONV_PIN ─────────────
@@ -677,6 +677,23 @@ export interface GenAiPipelineOptions extends GenAiComponentsOptions {
677
677
  deltaToCumulative?: boolean | "auto";
678
678
  }
679
679
 
680
+ /**
681
+ * The entities `genAiPipeline()` returns, by name: what the `GenAiPipeline`
682
+ * composite's members are. A key is absent when its part is off: `sampled`
683
+ * without `sampling`, `sdkMetrics` without `clientMetrics`, `logs` with
684
+ * `logs: false`, `health` with `healthCheck: false`.
685
+ */
686
+ export type GenAiPipelineParts = {
687
+ otlp: Declarable;
688
+ traces: PipelineEntity;
689
+ sampled?: PipelineEntity;
690
+ genAiTraces: PipelineEntity;
691
+ genAiMetrics: PipelineEntity;
692
+ sdkMetrics?: PipelineEntity;
693
+ logs?: PipelineEntity;
694
+ health?: Declarable;
695
+ };
696
+
680
697
  /**
681
698
  * The entities of an OTLP collector for GenAI workloads: an `otlp` receiver
682
699
  * on 4317 and 4318; `memory_limiter`, content removal and `batch` on traces
@@ -686,6 +703,11 @@ export interface GenAiPipelineOptions extends GenAiComponentsOptions {
686
703
  * pieces into pipelines of your own.
687
704
  */
688
705
  export function genAiPipeline(options: GenAiPipelineOptions = {}): Declarable[] {
706
+ return Object.values(genAiPipelineParts(options)) as Declarable[];
707
+ }
708
+
709
+ /** `genAiPipeline()`'s entities by name, in the order it returns them. */
710
+ export function genAiPipelineParts(options: GenAiPipelineOptions = {}): GenAiPipelineParts {
689
711
  const debug = new DebugExporter({ verbosity: "basic" });
690
712
  const { traceExporters = [debug], metricExporters = [debug], logExporters = [debug], logs = true, sampling = [], healthCheck = true } =
691
713
  options;
@@ -709,73 +731,64 @@ export function genAiPipeline(options: GenAiPipelineOptions = {}): Declarable[]
709
731
  spike_limit_percentage: 20,
710
732
  });
711
733
  const batch = new BatchProcessor({});
712
- const entities: Declarable[] = [otlp];
734
+ // Insertion order is the order genAiPipeline() has always returned.
735
+ const out: Partial<GenAiPipelineParts> = { otlp };
713
736
 
714
737
  if (sampling.length === 0) {
715
- entities.push(
716
- new Pipeline({
717
- signal: "traces",
718
- receivers: [otlp],
719
- processors: [memoryLimiter, ...parts.processors, batch],
720
- exporters: [...traceExporters, parts.forward],
721
- }),
722
- );
738
+ out.traces = new Pipeline({
739
+ signal: "traces",
740
+ receivers: [otlp],
741
+ processors: [memoryLimiter, ...parts.processors, batch],
742
+ exporters: [...traceExporters, parts.forward],
743
+ });
723
744
  } else {
724
745
  const sampled = new ForwardConnector({ name: "sampled" });
725
- entities.push(
726
- new Pipeline({
727
- signal: "traces",
728
- receivers: [otlp],
729
- processors: [memoryLimiter, ...parts.processors],
730
- exporters: [parts.forward, sampled],
731
- }),
732
- new Pipeline({
733
- signal: "traces",
734
- name: "sampled",
735
- receivers: [sampled],
736
- processors: [...sampling, batch],
737
- exporters: traceExporters,
738
- }),
739
- );
740
- }
741
- entities.push(
742
- new Pipeline({
746
+ out.traces = new Pipeline({
743
747
  signal: "traces",
744
- name: "genai",
745
- receivers: [parts.forward],
746
- processors: [parts.genAiSpans],
747
- exporters: [parts.spanMetrics, parts.tokenUsage, ...(parts.clientMetrics ? [parts.clientMetrics] : [])],
748
- }),
749
- new Pipeline({
750
- signal: "metrics",
751
- name: "genai",
752
- receivers: [parts.spanMetrics, parts.tokenUsage, ...(parts.clientMetrics ? [parts.clientMetrics] : [])],
753
- processors: [...cumulative, batch],
754
- exporters: metricExporters,
755
- }),
756
- );
748
+ receivers: [otlp],
749
+ processors: [memoryLimiter, ...parts.processors],
750
+ exporters: [parts.forward, sampled],
751
+ });
752
+ out.sampled = new Pipeline({
753
+ signal: "traces",
754
+ name: "sampled",
755
+ receivers: [sampled],
756
+ processors: [...sampling, batch],
757
+ exporters: traceExporters,
758
+ });
759
+ }
760
+ out.genAiTraces = new Pipeline({
761
+ signal: "traces",
762
+ name: "genai",
763
+ receivers: [parts.forward],
764
+ processors: [parts.genAiSpans],
765
+ exporters: [parts.spanMetrics, parts.tokenUsage, ...(parts.clientMetrics ? [parts.clientMetrics] : [])],
766
+ });
767
+ out.genAiMetrics = new Pipeline({
768
+ signal: "metrics",
769
+ name: "genai",
770
+ receivers: [parts.spanMetrics, parts.tokenUsage, ...(parts.clientMetrics ? [parts.clientMetrics] : [])],
771
+ processors: [...cumulative, batch],
772
+ exporters: metricExporters,
773
+ });
757
774
  if (client) {
758
775
  // The SDK's own metrics: the client metrics in passthrough, and the
759
776
  // ones no span carries (time to first chunk, gen_ai.server.*) either way.
760
- entities.push(
761
- new Pipeline({
762
- signal: "metrics",
763
- receivers: [otlp],
764
- processors: [memoryLimiter, ...(parts.sdkClientMetricsFilter ? [parts.sdkClientMetricsFilter] : []), batch],
765
- exporters: metricExporters,
766
- }),
767
- );
777
+ out.sdkMetrics = new Pipeline({
778
+ signal: "metrics",
779
+ receivers: [otlp],
780
+ processors: [memoryLimiter, ...(parts.sdkClientMetricsFilter ? [parts.sdkClientMetricsFilter] : []), batch],
781
+ exporters: metricExporters,
782
+ });
768
783
  }
769
784
  if (logs) {
770
- entities.push(
771
- new Pipeline({
772
- signal: "logs",
773
- receivers: [otlp],
774
- processors: [memoryLimiter, ...parts.processors, batch],
775
- exporters: logExporters,
776
- }),
777
- );
785
+ out.logs = new Pipeline({
786
+ signal: "logs",
787
+ receivers: [otlp],
788
+ processors: [memoryLimiter, ...parts.processors, batch],
789
+ exporters: logExporters,
790
+ });
778
791
  }
779
- if (healthCheck) entities.push(new HealthCheckExtension({ endpoint: "0.0.0.0:13133" }));
780
- return entities;
792
+ if (healthCheck) out.health = new HealthCheckExtension({ endpoint: "0.0.0.0:13133" });
793
+ return out as GenAiPipelineParts;
781
794
  }
@@ -53,4 +53,40 @@ describe("a collector config embedded in another lexicon's resource (#2962)", ()
53
53
  through: { from: "@intentius/chant-lexicon-otel", name: "collectorYaml" },
54
54
  });
55
55
  });
56
+
57
+ describe("an object config, from an OpenTelemetryCollector's spec.config (#3367)", () => {
58
+ const object = (header?: string[]): EmbeddedContent => {
59
+ const config = embeddedDocument(CONFIG);
60
+ return site(CONFIG, {
61
+ hostType: "K8s::OpenTelemetry::OpenTelemetryCollector",
62
+ location: "OpenTelemetryCollector agent spec.config",
63
+ text: undefined,
64
+ document: { config, header },
65
+ select: "config",
66
+ });
67
+ };
68
+
69
+ test("matches a { config } document selected as config, and nothing without a collector config in it", () => {
70
+ expect(collectorConfigImporter.matches(object())).toBe(true);
71
+ expect(collectorConfigImporter.matches(site(CONFIG, { text: undefined, document: { config: { groups: [] } }, select: "config" }))).toBe(false);
72
+ expect(collectorConfigImporter.matches(site(CONFIG, { text: undefined, document: { nope: 1 }, select: "config" }))).toBe(false);
73
+ });
74
+
75
+ test("is referenced through collectorConfig, which gives an object", () => {
76
+ const out = collectorConfigImporter.import(object());
77
+ expect(out.value.through).toEqual({ from: "@intentius/chant-lexicon-otel", name: "collectorConfig" });
78
+ expect(out.value.bindings.map((b) => b.name)).toEqual(["otlp", "mycorp", "debug", "traces"]);
79
+ });
80
+
81
+ test("puts the header lines back, so the custom component keeps its pin", () => {
82
+ const pin = "chant: receiver mycorp schema github.com/mycorp/otel@v1.2.3";
83
+ const out = collectorConfigImporter.import(object([pin]));
84
+ const custom = out.files.find((f) => f.path === "custom-components.ts")!.content;
85
+ expect(custom).toContain("github.com/mycorp/otel");
86
+ expect(custom).toContain("v1.2.3");
87
+ const bare = collectorConfigImporter.import(object()).files.find((f) => f.path === "custom-components.ts")!.content;
88
+ expect(bare).not.toContain("github.com/mycorp/otel");
89
+ });
90
+ });
56
91
  });
92
+
@@ -6,11 +6,20 @@
6
6
  * it, into a directory of its own, and the host's value becomes
7
7
  * `collectorYaml([...])` over every component, pipeline and service the
8
8
  * import declares, which renders the same config text at build time.
9
+ *
10
+ * A host that holds the config as an object, the OpenTelemetry Operator's
11
+ * `OpenTelemetryCollector` `spec.config`, offers it as `{ config, header }`
12
+ * with `select: "config"`. The config is printed back to the text the
13
+ * standalone import reads, with the `# chant:` header lines the host kept in
14
+ * an annotation put back on top so custom-component pins and semconv use
15
+ * survive. The host's value becomes `collectorConfig([...])`, an object, so
16
+ * the field keeps the type its CRD gives it.
9
17
  */
10
18
 
11
- import type { EmbeddedContentImporter, EmbeddedImport } from "@intentius/chant/import/embedded";
19
+ import type { EmbeddedContent, EmbeddedContentImporter, EmbeddedImport } from "@intentius/chant/import/embedded";
12
20
  import { exportedNames } from "@intentius/chant/import/embedded";
13
- import { looksLikeCollectorConfig } from "../model";
21
+ import { looksLikeCollectorConfig, type CollectorConfig } from "../model";
22
+ import { emitCollectorYaml } from "../yaml";
14
23
  import { OtelCollectorParser } from "./parser";
15
24
  import { OtelCollectorGenerator } from "./generator";
16
25
 
@@ -19,22 +28,37 @@ const PACKAGE = "@intentius/chant-lexicon-otel";
19
28
  /** The module holding `defineComponent` classes: types, not config entities. */
20
29
  const CUSTOM_COMPONENTS = "custom-components.ts";
21
30
 
31
+ function selectedConfig(content: EmbeddedContent): unknown {
32
+ const doc = content.document;
33
+ return typeof doc === "object" && doc !== null ? (doc as { config?: unknown }).config : undefined;
34
+ }
35
+
36
+ /** The text a `{ config, header }` document prints as: the header lines as comments, then the config. */
37
+ function objectConfigText(content: EmbeddedContent): string {
38
+ const header = (content.document as { header?: unknown }).header;
39
+ const lines = Array.isArray(header) ? header.filter((l): l is string => typeof l === "string") : [];
40
+ return emitCollectorYaml(selectedConfig(content) as CollectorConfig, { header: lines });
41
+ }
42
+
22
43
  export const collectorConfigImporter: EmbeddedContentImporter = {
23
44
  what: "an OpenTelemetry Collector config",
24
45
 
25
46
  matches(content) {
47
+ if (content.select === "config") return looksLikeCollectorConfig(selectedConfig(content));
26
48
  return typeof content.text === "string" && content.select === undefined && looksLikeCollectorConfig(content.document);
27
49
  },
28
50
 
29
51
  import(content): EmbeddedImport {
30
- const ir = new OtelCollectorParser().parse(content.text!);
52
+ const asObject = content.select === "config";
53
+ const text = asObject ? objectConfigText(content) : content.text!;
54
+ const ir = new OtelCollectorParser().parse(text);
31
55
  const files = new OtelCollectorGenerator().generate(ir);
32
56
  const bindings = files
33
57
  .filter((f) => f.path !== CUSTOM_COMPONENTS)
34
58
  .flatMap((f) => exportedNames(f.content).map((name) => ({ from: f.path, name })));
35
59
  return {
36
60
  files,
37
- value: { bindings, shape: "list", through: { from: PACKAGE, name: "collectorYaml" } },
61
+ value: { bindings, shape: "list", through: { from: PACKAGE, name: asObject ? "collectorConfig" : "collectorYaml" } },
38
62
  warnings: ir.warnings ?? [],
39
63
  };
40
64
  },
package/src/index.ts CHANGED
@@ -46,9 +46,17 @@ export {
46
46
 
47
47
  // The plain-data model, YAML, checks and topology
48
48
  export * from "./model";
49
- export { buildCollectorConfig, collectorYaml, componentConfig, type BuiltCollector } from "./collector";
49
+ export { buildCollectorConfig, collectorConfig, collectorYaml, componentConfig, type BuiltCollector } from "./collector";
50
50
  export { emitCollectorYaml, type EmitOptions } from "./yaml";
51
- export { configMapCollectorConfigs, describeConfigMapConfig, parseCollectorConfig, type ConfigMapCollectorConfig } from "./configmap";
51
+ export {
52
+ configMapCollectorConfigs,
53
+ describeConfigMapConfig,
54
+ describeOperatorCollectorConfig,
55
+ operatorCollectorConfig,
56
+ parseCollectorConfig,
57
+ type ConfigMapCollectorConfig,
58
+ type OperatorCollectorConfig,
59
+ } from "./configmap";
52
60
  export {
53
61
  validateCollectorConfig,
54
62
  validateCollectorEntities,
@@ -71,14 +79,40 @@ export { attributionIssues, isProtectedResourceAttribute, PROTECTED_RESOURCE_ATT
71
79
 
72
80
  // Composites
73
81
  export { NodeAgent, nodeAgentPropsProblem, type NodeAgentProps, type NodeAgentMembers, type NodeAgentInstance } from "./composites";
82
+ export {
83
+ RedMetrics,
84
+ redMetricsNames,
85
+ redMetricsPropsProblem,
86
+ GenAiPipeline,
87
+ OtlpCollector,
88
+ otlpCollectorPropsProblem,
89
+ TailSamplingTier,
90
+ tailSamplingTierPropsProblem,
91
+ tailSamplingLoadBalancer,
92
+ type RedMetricsProps,
93
+ type RedMetricsMembers,
94
+ type RedMetricsInstance,
95
+ type RedMetricsNames,
96
+ type GenAiPipelineProps,
97
+ type GenAiPipelineMembers,
98
+ type GenAiPipelineInstance,
99
+ type OtlpCollectorProps,
100
+ type OtlpCollectorMembers,
101
+ type OtlpCollectorInstance,
102
+ type TailSamplingTierProps,
103
+ type TailSamplingTierMembers,
104
+ type TailSamplingTierInstance,
105
+ } from "./composites";
74
106
 
75
107
  // What the platform collector composites (docker, k8s, fly) share
76
108
  export {
77
109
  otlpCollector,
110
+ otlpCollectorParts,
78
111
  collectorEndpoints,
79
112
  COLLECTOR_IMAGE,
80
113
  COLLECTOR_CONFIG_PATH,
81
114
  type OtlpCollectorOptions,
115
+ type OtlpCollectorParts,
82
116
  type CollectorPort,
83
117
  type CollectorEndpoints,
84
118
  } from "./platform";
@@ -86,6 +120,7 @@ export {
86
120
  // The GenAI preset: content removal and agent RED and token metrics
87
121
  export {
88
122
  genAiPipeline,
123
+ genAiPipelineParts,
89
124
  genAiComponents,
90
125
  genAiMetrics,
91
126
  GENAI_ATTRIBUTES,
@@ -107,6 +142,7 @@ export {
107
142
  DELTA_READY_EXPORTERS,
108
143
  genAiNeedsDeltaToCumulative,
109
144
  type GenAiPipelineOptions,
145
+ type GenAiPipelineParts,
110
146
  type GenAiComponentsOptions,
111
147
  type GenAiComponents,
112
148
  type GenAiMetricsOptions,
@@ -132,4 +168,23 @@ export {
132
168
  type SpanMetricsNames,
133
169
  type SpanMetricsNamingConfig,
134
170
  type PrometheusNaming,
171
+ promSelector,
172
+ promSumRate,
173
+ promErrorRatio,
174
+ promQuantile,
175
+ promNumber,
176
+ spanKindMatchers,
177
+ spanMetricsRedQueries,
178
+ SPAN_METRICS_KINDS,
179
+ RED_DEFAULT_SPAN_KINDS,
180
+ type PromMatcher,
181
+ type SpanMetricsKind,
182
+ type RedQueryOptions,
183
+ type RedQueries,
135
184
  } from "./metric-names";
185
+
186
+ // Op steps (#3369): the collector binary's checks, collectors as resources a
187
+ // ConvergeOp observes, and the pin audit. The activities resolve from
188
+ // `@intentius/chant-lexicon-otel/op/activities`.
189
+ export { otelcolValidate, otelcolComponents, collectorHealthObserve, collectorAudit } from "./op/builders";
190
+ export { CollectorAuditOp, type CollectorAuditOpConfig, type CollectorAuditOpResources } from "./op/audit-op";