@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.
- package/README.md +1 -1
- package/dist/collector.d.ts +6 -0
- package/dist/collector.d.ts.map +1 -1
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/genai-pipeline.d.ts +33 -0
- package/dist/composites/genai-pipeline.d.ts.map +1 -0
- package/dist/composites/index.d.ts +8 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/otlp-collector.d.ts +43 -0
- package/dist/composites/otlp-collector.d.ts.map +1 -0
- package/dist/composites/red-metrics.d.ts +77 -0
- package/dist/composites/red-metrics.d.ts.map +1 -0
- package/dist/composites/tail-sampling-tier.d.ts +90 -0
- package/dist/composites/tail-sampling-tier.d.ts.map +1 -0
- package/dist/configmap.d.ts +16 -0
- package/dist/configmap.d.ts.map +1 -1
- package/dist/genai.d.ts +19 -0
- package/dist/genai.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +8 -0
- package/dist/import/embedded.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +3 -3
- package/dist/lint/post-synth/otel-helpers.d.ts +9 -2
- package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/metric-names.d.ts +68 -0
- package/dist/metric-names.d.ts.map +1 -1
- package/dist/op/activities/collector-audit.d.ts +95 -0
- package/dist/op/activities/collector-audit.d.ts.map +1 -0
- package/dist/op/activities/collector-health.d.ts +74 -0
- package/dist/op/activities/collector-health.d.ts.map +1 -0
- package/dist/op/activities/exec.d.ts +19 -0
- package/dist/op/activities/exec.d.ts.map +1 -0
- package/dist/op/activities/github.d.ts +55 -0
- package/dist/op/activities/github.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +20 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/otelcol.d.ts +96 -0
- package/dist/op/activities/otelcol.d.ts.map +1 -0
- package/dist/op/activity-contracts.d.ts +96 -0
- package/dist/op/activity-contracts.d.ts.map +1 -0
- package/dist/op/audit-op.d.ts +35 -0
- package/dist/op/audit-op.d.ts.map +1 -0
- package/dist/op/builders.d.ts +43 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/platform.d.ts +14 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/rules/otel-helpers.ts +18 -5
- package/package.json +14 -3
- package/src/collector.ts +9 -0
- package/src/composites/catalog.test.ts +1 -1
- package/src/composites/catalog.ts +292 -0
- package/src/composites/genai-pipeline.test.ts +53 -0
- package/src/composites/genai-pipeline.ts +40 -0
- package/src/composites/index.ts +8 -0
- package/src/composites/otlp-collector.test.ts +43 -0
- package/src/composites/otlp-collector.ts +70 -0
- package/src/composites/red-metrics.test.ts +131 -0
- package/src/composites/red-metrics.ts +148 -0
- package/src/composites/tail-sampling-tier.test.ts +100 -0
- package/src/composites/tail-sampling-tier.ts +182 -0
- package/src/configmap-operator.test.ts +44 -0
- package/src/configmap.ts +35 -0
- package/src/detect.ts +13 -1
- package/src/genai.ts +72 -59
- package/src/import/embedded.test.ts +36 -0
- package/src/import/embedded.ts +28 -4
- package/src/index.ts +57 -2
- package/src/lint/post-synth/otel-helpers.ts +18 -5
- package/src/metric-names.ts +141 -0
- package/src/op/activities/collector-audit.test.ts +166 -0
- package/src/op/activities/collector-audit.ts +265 -0
- package/src/op/activities/collector-health.test.ts +115 -0
- package/src/op/activities/collector-health.ts +211 -0
- package/src/op/activities/exec.ts +33 -0
- package/src/op/activities/github.ts +127 -0
- package/src/op/activities/index.ts +19 -0
- package/src/op/activities/otelcol.test.ts +162 -0
- package/src/op/activities/otelcol.ts +233 -0
- package/src/op/activity-contracts.test.ts +103 -0
- package/src/op/activity-contracts.ts +92 -0
- package/src/op/audit-op.ts +53 -0
- package/src/op/builders.ts +52 -0
- package/src/op/testdata/otelcol-components-v0.130.0.yaml +82 -0
- 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
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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)
|
|
780
|
-
return
|
|
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
|
+
|
package/src/import/embedded.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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";
|