@intentius/chant-lexicon-otel 0.124.30 → 0.124.32
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/dist/attribution.js +391 -0
- package/dist/catalog.js +42 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +101 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate.js +43 -0
- package/dist/codegen/package.js +28 -0
- package/dist/collector.js +150 -0
- package/dist/components/common.js +50 -0
- package/dist/components/connectors.js +242 -0
- package/dist/components/exporters.js +57 -0
- package/dist/components/extensions.js +58 -0
- package/dist/components/filtering.js +106 -0
- package/dist/components/index.js +9 -0
- package/dist/components/k8s-receivers.js +36 -0
- package/dist/components/processors.js +103 -0
- package/dist/components/receivers.js +61 -0
- package/dist/components/sampling.js +257 -0
- package/dist/composites/catalog.js +368 -0
- package/dist/composites/genai-pipeline.js +28 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/node-agent.js +156 -0
- package/dist/composites/otlp-collector.js +57 -0
- package/dist/composites/red-metrics.js +95 -0
- package/dist/composites/tail-sampling-tier.js +132 -0
- package/dist/config-hygiene.js +464 -0
- package/dist/configmap.js +86 -0
- package/dist/define.js +204 -0
- package/dist/detect.js +22 -0
- package/dist/genai.js +569 -0
- package/dist/import/embedded.js +57 -0
- package/dist/import/generator.js +459 -0
- package/dist/import/parser.js +199 -0
- package/dist/index.js +34 -0
- package/dist/init-templates.js +119 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +49 -0
- package/dist/lint/post-synth/index.js +52 -0
- package/dist/lint/post-synth/otel-helpers.js +103 -0
- package/dist/lint/post-synth/otel101.js +13 -0
- package/dist/lint/post-synth/otel102.js +13 -0
- package/dist/lint/post-synth/otel103.js +13 -0
- package/dist/lint/post-synth/otel104.js +13 -0
- package/dist/lint/post-synth/otel105.js +13 -0
- package/dist/lint/post-synth/otel106.js +13 -0
- package/dist/lint/post-synth/otel107.js +13 -0
- package/dist/lint/post-synth/otel108.js +13 -0
- package/dist/lint/post-synth/otel109.js +13 -0
- package/dist/lint/post-synth/otel112.js +13 -0
- package/dist/lint/post-synth/otel113.js +13 -0
- package/dist/lint/post-synth/otel114.js +13 -0
- package/dist/lint/post-synth/otel115.js +13 -0
- package/dist/lint/post-synth/otel116.js +13 -0
- package/dist/lint/post-synth/otel117.js +13 -0
- package/dist/lint/post-synth/otel118.js +13 -0
- package/dist/lint/post-synth/otel119.js +13 -0
- package/dist/lint/post-synth/otel120.js +13 -0
- package/dist/lint/post-synth/otel121.js +13 -0
- package/dist/lint/post-synth/otel122.js +13 -0
- package/dist/lint/post-synth/otel123.js +13 -0
- package/dist/lint/post-synth/otel124.js +13 -0
- package/dist/lint/post-synth/otel125.js +13 -0
- package/dist/lint/post-synth/otel126.js +13 -0
- package/dist/lint/post-synth/otel127.js +13 -0
- package/dist/lint/rules/component-id-syntax.js +66 -0
- package/dist/lint/rules/index.js +6 -0
- package/dist/lint/rules/literal-credential.js +91 -0
- package/dist/lint/rules/otel-ast.js +30 -0
- package/dist/lsp/completions.js +11 -0
- package/dist/lsp/hover.js +23 -0
- package/dist/manifest.json +1 -1
- package/dist/metric-names.js +244 -0
- package/dist/model.js +90 -0
- package/dist/op/activities/collector-audit.js +213 -0
- package/dist/op/activities/collector-health.js +176 -0
- package/dist/op/activities/exec.js +16 -0
- package/dist/op/activities/github.js +110 -0
- package/dist/op/activities/index.js +16 -0
- package/dist/op/activities/otelcol.js +173 -0
- package/dist/op/activity-contracts.js +64 -0
- package/dist/op/audit-op.js +33 -0
- package/dist/op/builders.js +38 -0
- package/dist/package-cli.js +14 -0
- package/dist/pipeline.js +31 -0
- package/dist/platform.js +144 -0
- package/dist/plugin.js +126 -0
- package/dist/semconv.js +50 -0
- package/dist/serializer.js +26 -0
- package/dist/skill-defs.js +34 -0
- package/dist/topology.js +104 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate-config.js +706 -0
- package/dist/validate.js +85 -0
- package/dist/yaml.js +157 -0
- package/package.json +8 -8
- package/src/codegen/package.ts +2 -2
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared entities -> `CollectorConfig` -> YAML.
|
|
3
|
+
*
|
|
4
|
+
* The serializer runs this over a build's entities, and a composite in another
|
|
5
|
+
* lexicon (the k8s `GkeOtelCollector`, which puts the YAML in a ConfigMap)
|
|
6
|
+
* runs it over the entities it creates. Both get the same config and the same
|
|
7
|
+
* text.
|
|
8
|
+
*/
|
|
9
|
+
import { definitionFor, isOTelComponent } from "./define.js";
|
|
10
|
+
import { SECTION_OF } from "./model.js";
|
|
11
|
+
import { isPipelineEntity, isServiceEntity } from "./pipeline.js";
|
|
12
|
+
import { emitCollectorYaml } from "./yaml.js";
|
|
13
|
+
import { semconvUsage } from "./semconv.js";
|
|
14
|
+
function entityList(entities) {
|
|
15
|
+
return entities instanceof Map ? [...entities.values()] : [...entities];
|
|
16
|
+
}
|
|
17
|
+
/** A config value with component entities turned into ids and `undefined` keys dropped. */
|
|
18
|
+
function plain(value) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (isOTelComponent(value))
|
|
22
|
+
return value.componentId;
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return value.map(plain);
|
|
25
|
+
if (typeof value === "object" && value !== null) {
|
|
26
|
+
const toJSON = value.toJSON;
|
|
27
|
+
if (typeof toJSON === "function")
|
|
28
|
+
return plain(toJSON.call(value));
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [k, v] of Object.entries(value)) {
|
|
31
|
+
if (v !== undefined)
|
|
32
|
+
out[k] = plain(v);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
/** A component's config, as it appears under its id: its props without `name`. */
|
|
39
|
+
export function componentConfig(component) {
|
|
40
|
+
const { name: _name, ...rest } = (component.props ?? {});
|
|
41
|
+
return plain(rest);
|
|
42
|
+
}
|
|
43
|
+
function refId(ref) {
|
|
44
|
+
if (isOTelComponent(ref))
|
|
45
|
+
return ref.componentId;
|
|
46
|
+
return String(ref);
|
|
47
|
+
}
|
|
48
|
+
/** Build the collector config for a set of declared entities. Non-otel entities are ignored. */
|
|
49
|
+
export function buildCollectorConfig(entities) {
|
|
50
|
+
const all = entityList(entities).filter((e) => e && e.lexicon === "otel");
|
|
51
|
+
const warnings = [];
|
|
52
|
+
const components = [];
|
|
53
|
+
const byId = new Map();
|
|
54
|
+
const pipelines = [];
|
|
55
|
+
let service;
|
|
56
|
+
const addComponent = (c) => {
|
|
57
|
+
const key = `${c.componentKind}:${c.componentId}`;
|
|
58
|
+
const existing = byId.get(key);
|
|
59
|
+
if (existing === c)
|
|
60
|
+
return;
|
|
61
|
+
if (existing) {
|
|
62
|
+
warnings.push(`otel: two ${c.componentKind}s declare the id "${c.componentId}"; the first one is emitted`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
byId.set(key, c);
|
|
66
|
+
components.push(c);
|
|
67
|
+
};
|
|
68
|
+
for (const e of all) {
|
|
69
|
+
if (isOTelComponent(e))
|
|
70
|
+
addComponent(e);
|
|
71
|
+
else if (isPipelineEntity(e))
|
|
72
|
+
pipelines.push(e);
|
|
73
|
+
else if (isServiceEntity(e)) {
|
|
74
|
+
if (service)
|
|
75
|
+
warnings.push("otel: more than one Service is declared; the first one is used");
|
|
76
|
+
else
|
|
77
|
+
service = e;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// A component a pipeline or the service references by entity is part of the
|
|
81
|
+
// config even when the caller didn't list it: it carries its own config.
|
|
82
|
+
for (const p of pipelines) {
|
|
83
|
+
for (const ref of [...(p.props.receivers ?? []), ...(p.props.processors ?? []), ...(p.props.exporters ?? [])]) {
|
|
84
|
+
if (isOTelComponent(ref))
|
|
85
|
+
addComponent(ref);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const ref of service?.props.extensions ?? []) {
|
|
89
|
+
if (isOTelComponent(ref))
|
|
90
|
+
addComponent(ref);
|
|
91
|
+
}
|
|
92
|
+
const config = {};
|
|
93
|
+
for (const c of components) {
|
|
94
|
+
const section = SECTION_OF[c.componentKind];
|
|
95
|
+
const bucket = (config[section] ?? (config[section] = {}));
|
|
96
|
+
bucket[c.componentId] = componentConfig(c);
|
|
97
|
+
}
|
|
98
|
+
const pipelineMap = {};
|
|
99
|
+
for (const p of pipelines) {
|
|
100
|
+
if (p.pipelineId in pipelineMap) {
|
|
101
|
+
warnings.push(`otel: two pipelines declare the id "${p.pipelineId}"; the first one is emitted`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const entry = { receivers: (p.props.receivers ?? []).map(refId) };
|
|
105
|
+
const processors = (p.props.processors ?? []).map(refId);
|
|
106
|
+
if (processors.length > 0)
|
|
107
|
+
entry.processors = processors;
|
|
108
|
+
entry.exporters = (p.props.exporters ?? []).map(refId);
|
|
109
|
+
pipelineMap[p.pipelineId] = entry;
|
|
110
|
+
}
|
|
111
|
+
const extensions = service?.props.extensions
|
|
112
|
+
? service.props.extensions.map(refId)
|
|
113
|
+
: components.filter((c) => c.componentKind === "extension").map((c) => c.componentId);
|
|
114
|
+
if (pipelines.length > 0 || extensions.length > 0 || service?.props.telemetry) {
|
|
115
|
+
config.service = {};
|
|
116
|
+
if (extensions.length > 0)
|
|
117
|
+
config.service.extensions = extensions;
|
|
118
|
+
if (service?.props.telemetry)
|
|
119
|
+
config.service.telemetry = plain(service.props.telemetry);
|
|
120
|
+
if (pipelines.length > 0)
|
|
121
|
+
config.service.pipelines = pipelineMap;
|
|
122
|
+
}
|
|
123
|
+
const header = [];
|
|
124
|
+
for (const c of components) {
|
|
125
|
+
const def = definitionFor(c.entityType);
|
|
126
|
+
if (!def || def.builtin)
|
|
127
|
+
continue;
|
|
128
|
+
const pin = def.pin;
|
|
129
|
+
const digest = pin?.digest ? ` ${pin.digest}` : "";
|
|
130
|
+
header.push(`chant: ${c.componentKind} ${c.componentId} schema ${pin?.source ?? "(unpinned)"}@${pin?.version ?? "(unpinned)"}${digest}`);
|
|
131
|
+
}
|
|
132
|
+
for (const use of semconvUsage(config)) {
|
|
133
|
+
const digest = use.digest ? ` ${use.digest}` : "";
|
|
134
|
+
header.push(`chant: semconv ${use.namespace} ${use.source}@${use.version}${digest} (${use.components.join(", ")})`);
|
|
135
|
+
}
|
|
136
|
+
return { config, header, warnings, components, pipelines, service };
|
|
137
|
+
}
|
|
138
|
+
/** Collector YAML for a set of declared entities, exactly as the serializer emits it. */
|
|
139
|
+
export function collectorYaml(entities) {
|
|
140
|
+
const built = buildCollectorConfig(entities);
|
|
141
|
+
return emitCollectorYaml(built.config, { header: built.header });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The collector config object for a set of declared entities, for a host that
|
|
145
|
+
* holds the config as an object rather than as text: the OpenTelemetry
|
|
146
|
+
* Operator's `OpenTelemetryCollector` `spec.config` (v1beta1).
|
|
147
|
+
*/
|
|
148
|
+
export function collectorConfig(entities) {
|
|
149
|
+
return buildCollectorConfig(entities).config;
|
|
150
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings shared by many collector components (the collector's `config*`
|
|
3
|
+
* packages: configtls, configgrpc, confighttp, configretry, exporterhelper).
|
|
4
|
+
* Keys use the collector's own snake_case spelling, since they are emitted
|
|
5
|
+
* unchanged.
|
|
6
|
+
*/
|
|
7
|
+
const GO_DURATION_UNITS = { ns: 1e-6, us: 1e-3, "µs": 1e-3, "μs": 1e-3, ms: 1, s: 1e3, m: 60e3, h: 3600e3 };
|
|
8
|
+
/** A Go duration in milliseconds, `fallback` when unset, or undefined when it isn't one this can read. */
|
|
9
|
+
export function goDurationMs(d, fallback) {
|
|
10
|
+
if (d === undefined)
|
|
11
|
+
return fallback;
|
|
12
|
+
if (typeof d === "number")
|
|
13
|
+
return d / 1e6;
|
|
14
|
+
// time.ParseDuration takes a bare "0" without a unit.
|
|
15
|
+
if (/^[+-]?0$/.test(d.trim()))
|
|
16
|
+
return 0;
|
|
17
|
+
const m = d.trim().match(/^([+-]?)((?:\d+(?:\.\d*)?|\.\d+)(?:ns|us|µs|μs|ms|s|m|h))+$/);
|
|
18
|
+
if (!m)
|
|
19
|
+
return undefined;
|
|
20
|
+
let total = 0;
|
|
21
|
+
for (const part of d.trim().matchAll(/(\d+(?:\.\d*)?|\.\d+)(ns|us|µs|μs|ms|s|m|h)/g)) {
|
|
22
|
+
total += Number(part[1]) * GO_DURATION_UNITS[part[2]];
|
|
23
|
+
}
|
|
24
|
+
return m[1] === "-" ? -total : total;
|
|
25
|
+
}
|
|
26
|
+
/** Problems in the `sending_queue` of an exporter's `exporterhelper` settings. */
|
|
27
|
+
export function queueIssues(c) {
|
|
28
|
+
const q = c.sending_queue;
|
|
29
|
+
if (!q)
|
|
30
|
+
return [];
|
|
31
|
+
const issues = [];
|
|
32
|
+
if (q.blocking !== undefined) {
|
|
33
|
+
issues.push(q.block_on_overflow !== undefined
|
|
34
|
+
? "sending_queue.blocking is set next to block_on_overflow; remove blocking, the collector reads block_on_overflow"
|
|
35
|
+
: "sending_queue.blocking is not a collector setting; use sending_queue.block_on_overflow");
|
|
36
|
+
}
|
|
37
|
+
const b = q.batch;
|
|
38
|
+
if (!b)
|
|
39
|
+
return issues;
|
|
40
|
+
if (q.enabled === false)
|
|
41
|
+
issues.push("sending_queue.batch is set but sending_queue.enabled is false; batching runs in the queue");
|
|
42
|
+
if (b.min_size !== undefined && b.min_size < 0)
|
|
43
|
+
issues.push("sending_queue.batch.min_size is negative");
|
|
44
|
+
if (b.max_size !== undefined && b.max_size < 0)
|
|
45
|
+
issues.push("sending_queue.batch.max_size is negative");
|
|
46
|
+
if (b.max_size !== undefined && b.max_size !== 0 && b.min_size !== undefined && b.max_size < b.min_size) {
|
|
47
|
+
issues.push(`sending_queue.batch.max_size (${b.max_size}) is below min_size (${b.min_size}); the collector rejects this`);
|
|
48
|
+
}
|
|
49
|
+
return issues;
|
|
50
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in connectors: spanmetrics, servicegraph, routing, forward, count, sum,
|
|
3
|
+
* signaltometrics.
|
|
4
|
+
*
|
|
5
|
+
* A connector is an exporter in one pipeline and a receiver in another. Put
|
|
6
|
+
* the same entity in `exporters` of the pipeline that feeds it and in
|
|
7
|
+
* `receivers` of the pipeline it feeds. `connects` lists the signal pairs each
|
|
8
|
+
* connector's factory registers; OTEL112 checks pipelines against them.
|
|
9
|
+
*/
|
|
10
|
+
import { defineBuiltin } from "../define.js";
|
|
11
|
+
const SAME_SIGNAL = [
|
|
12
|
+
{ from: "traces", to: "traces" },
|
|
13
|
+
{ from: "metrics", to: "metrics" },
|
|
14
|
+
{ from: "logs", to: "logs" },
|
|
15
|
+
];
|
|
16
|
+
/** Turns spans into request, error and duration (RED) metrics. */
|
|
17
|
+
export const SpanMetricsConnector = defineBuiltin({
|
|
18
|
+
kind: "connector",
|
|
19
|
+
type: "spanmetrics",
|
|
20
|
+
description: "Turns spans into call-count and duration metrics per service, span name, kind and status",
|
|
21
|
+
connects: [{ from: "traces", to: "metrics" }],
|
|
22
|
+
validate: (c) => {
|
|
23
|
+
const problems = [];
|
|
24
|
+
if (c.histogram?.explicit && c.histogram?.exponential) {
|
|
25
|
+
problems.push("histogram: set explicit or exponential buckets, not both");
|
|
26
|
+
}
|
|
27
|
+
if (c.events?.enabled && (c.events.dimensions ?? []).length === 0) {
|
|
28
|
+
problems.push("events: enabled needs at least one dimension");
|
|
29
|
+
}
|
|
30
|
+
const reserved = new Set(["service.name", "span.name", "span.kind", "status.code"]);
|
|
31
|
+
for (const d of c.dimensions ?? []) {
|
|
32
|
+
if (reserved.has(d.name))
|
|
33
|
+
problems.push(`dimensions: "${d.name}" is already a default dimension`);
|
|
34
|
+
reserved.add(d.name);
|
|
35
|
+
}
|
|
36
|
+
return problems;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
/** Builds service-to-service edge metrics from paired client and server spans. */
|
|
40
|
+
export const ServiceGraphConnector = defineBuiltin({
|
|
41
|
+
kind: "connector",
|
|
42
|
+
type: "servicegraph",
|
|
43
|
+
description: "Builds service-to-service request and latency metrics from paired client and server spans",
|
|
44
|
+
connects: [{ from: "traces", to: "metrics" }],
|
|
45
|
+
});
|
|
46
|
+
/** Sends each item to the pipelines whose route matches it. */
|
|
47
|
+
export const RoutingConnector = defineBuiltin({
|
|
48
|
+
kind: "connector",
|
|
49
|
+
type: "routing",
|
|
50
|
+
description: "Routes traces, metrics or logs to pipelines by OTTL condition",
|
|
51
|
+
connects: SAME_SIGNAL,
|
|
52
|
+
validate: (c) => {
|
|
53
|
+
const problems = [];
|
|
54
|
+
if ((c.table ?? []).length === 0)
|
|
55
|
+
problems.push("table is empty");
|
|
56
|
+
(c.table ?? []).forEach((item, i) => {
|
|
57
|
+
if (!item.statement && !item.condition)
|
|
58
|
+
problems.push(`table[${i}]: set a condition or a statement`);
|
|
59
|
+
if (item.statement && item.condition)
|
|
60
|
+
problems.push(`table[${i}]: set a condition or a statement, not both`);
|
|
61
|
+
if (item.context === "request" && !item.condition)
|
|
62
|
+
problems.push(`table[${i}]: the request context needs a condition`);
|
|
63
|
+
if ((item.pipelines ?? []).length === 0)
|
|
64
|
+
problems.push(`table[${i}]: no pipelines`);
|
|
65
|
+
});
|
|
66
|
+
return problems;
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
/** Passes data unchanged from one pipeline to another of the same signal. */
|
|
70
|
+
export const ForwardConnector = defineBuiltin({
|
|
71
|
+
kind: "connector",
|
|
72
|
+
type: "forward",
|
|
73
|
+
description: "Passes data unchanged to another pipeline of the same signal",
|
|
74
|
+
connects: SAME_SIGNAL,
|
|
75
|
+
});
|
|
76
|
+
/** Counts spans, span events, metrics, data points or log records, as metrics. */
|
|
77
|
+
export const CountConnector = defineBuiltin({
|
|
78
|
+
kind: "connector",
|
|
79
|
+
type: "count",
|
|
80
|
+
description: "Counts spans, span events, metrics, data points or log records and emits the counts as metrics",
|
|
81
|
+
connects: [
|
|
82
|
+
{ from: "traces", to: "metrics" },
|
|
83
|
+
{ from: "metrics", to: "metrics" },
|
|
84
|
+
{ from: "logs", to: "metrics" },
|
|
85
|
+
{ from: "profiles", to: "metrics" },
|
|
86
|
+
],
|
|
87
|
+
validate: (c) => {
|
|
88
|
+
const problems = [];
|
|
89
|
+
for (const [section, metrics] of Object.entries(c)) {
|
|
90
|
+
for (const [name, info] of Object.entries(metrics ?? {})) {
|
|
91
|
+
if (name === "")
|
|
92
|
+
problems.push(`${section}: metric name missing`);
|
|
93
|
+
if (section === "metrics" && (info?.attributes ?? []).length > 0) {
|
|
94
|
+
problems.push(`metrics.${name}: attributes are not supported when counting metrics`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return problems;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
/** Sums a numeric attribute of spans, span events, data points or log records, as metrics. */
|
|
102
|
+
export const SumConnector = defineBuiltin({
|
|
103
|
+
kind: "connector",
|
|
104
|
+
type: "sum",
|
|
105
|
+
description: "Sums a numeric attribute of spans, span events, data points or log records and emits the sums as metrics",
|
|
106
|
+
connects: [
|
|
107
|
+
{ from: "traces", to: "metrics" },
|
|
108
|
+
{ from: "metrics", to: "metrics" },
|
|
109
|
+
{ from: "logs", to: "metrics" },
|
|
110
|
+
],
|
|
111
|
+
validate: (c) => {
|
|
112
|
+
const problems = [];
|
|
113
|
+
let total = 0;
|
|
114
|
+
for (const [section, metrics] of Object.entries(c)) {
|
|
115
|
+
for (const [name, info] of Object.entries(metrics ?? {})) {
|
|
116
|
+
total++;
|
|
117
|
+
if (name === "")
|
|
118
|
+
problems.push(`${section}: metric name missing`);
|
|
119
|
+
if (!info?.source_attribute)
|
|
120
|
+
problems.push(`${section}.${name}: source_attribute is missing`);
|
|
121
|
+
if (section === "metrics" && (info?.attributes ?? []).length > 0) {
|
|
122
|
+
problems.push(`${section}.${name}: attributes are not supported when summing metrics`);
|
|
123
|
+
}
|
|
124
|
+
(info?.attributes ?? []).forEach((a, i) => {
|
|
125
|
+
if (!a.key)
|
|
126
|
+
problems.push(`${section}.${name}.attributes[${i}]: key is missing`);
|
|
127
|
+
});
|
|
128
|
+
// At the pinned release the connector adds each value once per
|
|
129
|
+
// attribute key (sumconnector summer.increment), so two keys double
|
|
130
|
+
// every sum.
|
|
131
|
+
if ((info?.attributes ?? []).length > 1) {
|
|
132
|
+
problems.push(`${section}.${name}: more than one attribute multiplies each sum by the number of attributes in the pinned collector; split by one attribute`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (total === 0)
|
|
137
|
+
problems.push("no metric is configured, so the connector emits nothing");
|
|
138
|
+
return problems;
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
// ── signaltometrics ──────────────────────────────────────────────────
|
|
142
|
+
//
|
|
143
|
+
// Typed against connector/signaltometricsconnector at COLLECTOR_PIN
|
|
144
|
+
// (collector-contrib v0.130.0): config/config.go and README.md at that tag,
|
|
145
|
+
// https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.130.0/connector/signaltometricsconnector
|
|
146
|
+
// The collector type is `signaltometrics`. Upstream marks every signal pair
|
|
147
|
+
// alpha. At v0.130.0 there is no span event section; spans, data points,
|
|
148
|
+
// logs and profiles each take a list of metrics.
|
|
149
|
+
/** The input sections of a `signaltometrics` connector, one per signal it reads. */
|
|
150
|
+
export const SIGNAL_TO_METRICS_SIGNALS = ["spans", "datapoints", "logs", "profiles"];
|
|
151
|
+
/** The metric types a `signaltometrics` entry can produce. Each entry names exactly one. */
|
|
152
|
+
export const SIGNAL_TO_METRICS_TYPES = ["sum", "gauge", "histogram", "exponential_histogram"];
|
|
153
|
+
/**
|
|
154
|
+
* The connector's default explicit histogram bounds, used when `histogram`
|
|
155
|
+
* sets no `buckets`. They are plain numbers in whatever unit `value` returns,
|
|
156
|
+
* not durations.
|
|
157
|
+
*/
|
|
158
|
+
export const SIGNAL_TO_METRICS_DEFAULT_BUCKETS = Object.freeze([
|
|
159
|
+
2, 4, 6, 8, 10, 50, 100, 200, 400, 800, 1000, 1400, 2000, 5000, 10000, 15000,
|
|
160
|
+
]);
|
|
161
|
+
/** Every metric entry of a `signaltometrics` config, in section order. Reads untyped config too. */
|
|
162
|
+
export function signalToMetricsEntries(config) {
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const signal of SIGNAL_TO_METRICS_SIGNALS) {
|
|
165
|
+
const list = config?.[signal];
|
|
166
|
+
if (!Array.isArray(list))
|
|
167
|
+
continue;
|
|
168
|
+
list.forEach((metric, index) => {
|
|
169
|
+
const m = (metric ?? {});
|
|
170
|
+
const types = SIGNAL_TO_METRICS_TYPES.filter((t) => m[t] !== undefined && m[t] !== null);
|
|
171
|
+
out.push({ signal, index, metric: m, types });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
const GROK_KEY_SELECTOR = /ExtractGrokPatterns\([^)]*\)\s*\[[^\]]+\]/;
|
|
177
|
+
/** The checks config/config.go makes at v0.130.0 that need no OTTL parser, plus increasing buckets. */
|
|
178
|
+
function validateSignalToMetrics(c) {
|
|
179
|
+
const problems = [];
|
|
180
|
+
const entries = signalToMetricsEntries(c);
|
|
181
|
+
if (entries.length === 0) {
|
|
182
|
+
problems.push("no metric is configured under spans, datapoints, logs or profiles; the collector refuses the connector");
|
|
183
|
+
}
|
|
184
|
+
for (const { signal, index, metric, types } of entries) {
|
|
185
|
+
const m = metric;
|
|
186
|
+
const at = m.name ? `${signal}[${index}] (${m.name})` : `${signal}[${index}]`;
|
|
187
|
+
if (!m.name)
|
|
188
|
+
problems.push(`${at}: name is missing`);
|
|
189
|
+
if (types.length !== 1) {
|
|
190
|
+
problems.push(`${at}: name exactly one metric type (sum, gauge, histogram or exponential_histogram); ${types.length === 0 ? "found none" : `found ${types.join(" and ")}`}`);
|
|
191
|
+
}
|
|
192
|
+
for (const t of types) {
|
|
193
|
+
const body = m[t];
|
|
194
|
+
if (typeof body?.value !== "string" || body.value === "")
|
|
195
|
+
problems.push(`${at}: ${t}.value is missing`);
|
|
196
|
+
}
|
|
197
|
+
const buckets = m.histogram?.buckets;
|
|
198
|
+
if (Array.isArray(buckets) && buckets.some((b, i) => i > 0 && !(b > buckets[i - 1]))) {
|
|
199
|
+
problems.push(`${at}: histogram.buckets must increase`);
|
|
200
|
+
}
|
|
201
|
+
const maxSize = m.exponential_histogram?.max_size;
|
|
202
|
+
if (typeof maxSize === "number" && maxSize !== 0 && (maxSize < 2 || maxSize > 16384)) {
|
|
203
|
+
problems.push(`${at}: exponential_histogram.max_size must be between 2 and 16384`);
|
|
204
|
+
}
|
|
205
|
+
const gauge = m.gauge?.value;
|
|
206
|
+
if (typeof gauge === "string" && gauge.includes("ExtractGrokPatterns") && !GROK_KEY_SELECTOR.test(gauge)) {
|
|
207
|
+
problems.push(`${at}: gauge.value with ExtractGrokPatterns needs one key selector, ExtractGrokPatterns(...)["key"]`);
|
|
208
|
+
}
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
(m.attributes ?? []).forEach((a, i) => {
|
|
211
|
+
if (!a?.key) {
|
|
212
|
+
problems.push(`${at}: attributes[${i}]: key is missing`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (a.default_value !== undefined && a.optional) {
|
|
216
|
+
problems.push(`${at}: attributes "${a.key}": set default_value or optional, not both`);
|
|
217
|
+
}
|
|
218
|
+
if (seen.has(a.key))
|
|
219
|
+
problems.push(`${at}: attributes "${a.key}" is listed twice`);
|
|
220
|
+
seen.add(a.key);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return problems;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Builds metrics from spans, data points, logs or profiles, each with the
|
|
227
|
+
* name, type, unit, attributes and conditions you choose. A histogram, sum or
|
|
228
|
+
* gauge takes its value from an OTTL expression, such as an attribute or the
|
|
229
|
+
* span's duration.
|
|
230
|
+
*/
|
|
231
|
+
export const SignalToMetricsConnector = defineBuiltin({
|
|
232
|
+
kind: "connector",
|
|
233
|
+
type: "signaltometrics",
|
|
234
|
+
description: "Builds named sum, gauge and histogram metrics from spans, data points, logs or profiles, with values and conditions in OTTL",
|
|
235
|
+
connects: [
|
|
236
|
+
{ from: "traces", to: "metrics" },
|
|
237
|
+
{ from: "metrics", to: "metrics" },
|
|
238
|
+
{ from: "logs", to: "metrics" },
|
|
239
|
+
{ from: "profiles", to: "metrics" },
|
|
240
|
+
],
|
|
241
|
+
validate: validateSignalToMetrics,
|
|
242
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in exporters: otlp, otlphttp, debug, prometheus, googlecloud.
|
|
3
|
+
*/
|
|
4
|
+
import { defineBuiltin } from "../define.js";
|
|
5
|
+
import { queueIssues } from "./common.js";
|
|
6
|
+
/** Sends OTLP over gRPC to another collector or a backend. */
|
|
7
|
+
export const OtlpExporter = defineBuiltin({
|
|
8
|
+
kind: "exporter",
|
|
9
|
+
type: "otlp",
|
|
10
|
+
description: "Sends traces, metrics and logs over OTLP gRPC",
|
|
11
|
+
validate: (c) => [...(c.endpoint ? [] : ["endpoint is empty"]), ...queueIssues(c)],
|
|
12
|
+
endpoints: (c) => (c.endpoint ? [c.endpoint] : []),
|
|
13
|
+
protocols: () => ["grpc"],
|
|
14
|
+
});
|
|
15
|
+
/** Sends OTLP over HTTP. `endpoint` gets `/v1/traces` etc. appended; the per-signal endpoints are used as given. */
|
|
16
|
+
export const OtlpHttpExporter = defineBuiltin({
|
|
17
|
+
kind: "exporter",
|
|
18
|
+
type: "otlphttp",
|
|
19
|
+
description: "Sends traces, metrics and logs over OTLP HTTP",
|
|
20
|
+
validate: (c) => [
|
|
21
|
+
...(c.endpoint || c.traces_endpoint || c.metrics_endpoint || c.logs_endpoint
|
|
22
|
+
? []
|
|
23
|
+
: ["set endpoint, or at least one of traces_endpoint, metrics_endpoint and logs_endpoint"]),
|
|
24
|
+
...queueIssues(c),
|
|
25
|
+
],
|
|
26
|
+
endpoints: (c) => [c.endpoint, c.traces_endpoint, c.metrics_endpoint, c.logs_endpoint].filter((e) => typeof e === "string" && e !== ""),
|
|
27
|
+
protocols: (c) => [c.encoding === "json" ? "http/json" : "http/protobuf"],
|
|
28
|
+
});
|
|
29
|
+
/** Writes telemetry to the collector's console, for debugging. */
|
|
30
|
+
export const DebugExporter = defineBuiltin({
|
|
31
|
+
kind: "exporter",
|
|
32
|
+
type: "debug",
|
|
33
|
+
description: "Writes telemetry to the collector's console, for debugging",
|
|
34
|
+
endpoints: () => ["console"],
|
|
35
|
+
});
|
|
36
|
+
/** Serves metrics on an endpoint Prometheus scrapes. */
|
|
37
|
+
export const PrometheusExporter = defineBuiltin({
|
|
38
|
+
kind: "exporter",
|
|
39
|
+
type: "prometheus",
|
|
40
|
+
description: "Serves metrics on an endpoint for Prometheus to scrape",
|
|
41
|
+
validate: (c) => (c.endpoint ? [] : ["endpoint is empty, so there is nothing to scrape"]),
|
|
42
|
+
endpoints: (c) => (c.endpoint ? [c.endpoint] : []),
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Sends traces to Cloud Trace, metrics to Cloud Monitoring and logs to Cloud
|
|
46
|
+
* Logging. With no endpoint overrides its topology endpoint is
|
|
47
|
+
* `googlecloud://projects/<project>`, naming where the data lands.
|
|
48
|
+
*/
|
|
49
|
+
export const GoogleCloudExporter = defineBuiltin({
|
|
50
|
+
kind: "exporter",
|
|
51
|
+
type: "googlecloud",
|
|
52
|
+
description: "Sends traces to Cloud Trace, metrics to Cloud Monitoring and logs to Cloud Logging",
|
|
53
|
+
endpoints: (c) => {
|
|
54
|
+
const overrides = [c.trace?.endpoint, c.metric?.endpoint, c.log?.endpoint].filter((e) => typeof e === "string" && e !== "");
|
|
55
|
+
return [`googlecloud://projects/${c.project ?? "(detected)"}`, ...overrides];
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in extensions: health_check, pprof, zpages, k8s_leader_elector.
|
|
3
|
+
*/
|
|
4
|
+
import { defineBuiltin } from "../define.js";
|
|
5
|
+
import { goDurationMs } from "./common.js";
|
|
6
|
+
/** Serves an HTTP health endpoint for liveness and readiness probes. */
|
|
7
|
+
export const HealthCheckExtension = defineBuiltin({
|
|
8
|
+
kind: "extension",
|
|
9
|
+
type: "health_check",
|
|
10
|
+
description: "Serves an HTTP health endpoint for liveness and readiness probes",
|
|
11
|
+
endpoints: (c) => [`${c.endpoint ?? "localhost:13133"}${c.path ?? "/"}`],
|
|
12
|
+
});
|
|
13
|
+
/** Serves Go's net/http/pprof profiling endpoints. */
|
|
14
|
+
export const PprofExtension = defineBuiltin({
|
|
15
|
+
kind: "extension",
|
|
16
|
+
type: "pprof",
|
|
17
|
+
description: "Serves Go pprof profiling endpoints",
|
|
18
|
+
endpoints: (c) => [c.endpoint ?? "localhost:1777"],
|
|
19
|
+
});
|
|
20
|
+
/** Serves in-process zPages for live debugging of pipelines. */
|
|
21
|
+
export const ZPagesExtension = defineBuiltin({
|
|
22
|
+
kind: "extension",
|
|
23
|
+
type: "zpages",
|
|
24
|
+
description: "Serves zPages for live debugging of receivers and exporters",
|
|
25
|
+
endpoints: (c) => [c.endpoint ?? "localhost:55679"],
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Elects one leader among collector replicas through a Kubernetes Lease, so a
|
|
29
|
+
* receiver that names it (`k8s_cluster`'s `k8s_leader_elector`) collects only
|
|
30
|
+
* in the replica holding the lease. The collector's ServiceAccount needs
|
|
31
|
+
* access to `leases` in `coordination.k8s.io` in `lease_namespace`.
|
|
32
|
+
*/
|
|
33
|
+
export const K8sLeaderElectorExtension = defineBuiltin({
|
|
34
|
+
kind: "extension",
|
|
35
|
+
type: "k8s_leader_elector",
|
|
36
|
+
description: "Elects one leader among collector replicas through a Kubernetes Lease",
|
|
37
|
+
validate: (c) => {
|
|
38
|
+
const problems = [];
|
|
39
|
+
// config.go Validate.
|
|
40
|
+
if (!c.lease_name || !c.lease_namespace)
|
|
41
|
+
problems.push("lease_name and lease_namespace must be set");
|
|
42
|
+
// client-go's leaderelection.NewLeaderElector, which leader_elector.go calls when the extension starts.
|
|
43
|
+
const lease = goDurationMs(c.lease_duration, 15000);
|
|
44
|
+
const renew = goDurationMs(c.renew_deadline, 10000);
|
|
45
|
+
const retry = goDurationMs(c.retry_period, 2000);
|
|
46
|
+
for (const [key, ms] of [["lease_duration", lease], ["renew_deadline", renew], ["retry_period", retry]]) {
|
|
47
|
+
if (ms !== undefined && ms <= 0)
|
|
48
|
+
problems.push(`${key} must be positive`);
|
|
49
|
+
}
|
|
50
|
+
if (lease !== undefined && renew !== undefined && lease > 0 && renew > 0 && lease <= renew) {
|
|
51
|
+
problems.push("lease_duration must be greater than renew_deadline");
|
|
52
|
+
}
|
|
53
|
+
if (renew !== undefined && retry !== undefined && renew > 0 && retry > 0 && renew <= 1.2 * retry) {
|
|
54
|
+
problems.push("renew_deadline must be greater than 1.2 times retry_period");
|
|
55
|
+
}
|
|
56
|
+
return problems;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in processors that drop, rewrite or redact telemetry: filter,
|
|
3
|
+
* transform, redaction.
|
|
4
|
+
*
|
|
5
|
+
* `filter` and `transform` take OTTL. chant types the structure around it
|
|
6
|
+
* (which signal, which context, which list) and keeps each OTTL condition or
|
|
7
|
+
* statement as a string; the collector parses them when it loads the config.
|
|
8
|
+
*/
|
|
9
|
+
import { defineBuiltin } from "../define.js";
|
|
10
|
+
/** Drops spans, span events, metrics, data points and log records that match OTTL conditions. */
|
|
11
|
+
export const FilterProcessor = defineBuiltin({
|
|
12
|
+
kind: "processor",
|
|
13
|
+
type: "filter",
|
|
14
|
+
description: "Drops spans, metrics and logs that match OTTL conditions",
|
|
15
|
+
validate: (c) => {
|
|
16
|
+
const lists = [
|
|
17
|
+
["traces.span", c.traces?.span],
|
|
18
|
+
["traces.spanevent", c.traces?.spanevent],
|
|
19
|
+
["metrics.metric", c.metrics?.metric],
|
|
20
|
+
["metrics.datapoint", c.metrics?.datapoint],
|
|
21
|
+
["logs.log_record", c.logs?.log_record],
|
|
22
|
+
];
|
|
23
|
+
const problems = [];
|
|
24
|
+
for (const [path, list] of lists) {
|
|
25
|
+
(list ?? []).forEach((cond, i) => {
|
|
26
|
+
if (typeof cond !== "string" || cond.trim() === "")
|
|
27
|
+
problems.push(`${path}[${i}] is an empty condition`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const legacy = [
|
|
31
|
+
["traces", !!(c.traces?.span || c.traces?.spanevent), !!(c.spans?.include || c.spans?.exclude)],
|
|
32
|
+
["metrics", !!(c.metrics?.metric || c.metrics?.datapoint), !!(c.metrics?.include || c.metrics?.exclude)],
|
|
33
|
+
["logs", !!c.logs?.log_record, !!(c.logs?.include || c.logs?.exclude)],
|
|
34
|
+
];
|
|
35
|
+
for (const [signal, ottl, matchBlocks] of legacy) {
|
|
36
|
+
if (ottl && matchBlocks)
|
|
37
|
+
problems.push(`${signal} mixes OTTL conditions with include/exclude, which the collector rejects`);
|
|
38
|
+
}
|
|
39
|
+
if (lists.every(([, list]) => !list || list.length === 0) && legacy.every(([, , matchBlocks]) => !matchBlocks)) {
|
|
40
|
+
problems.push("no condition is set for any signal, so the processor drops nothing");
|
|
41
|
+
}
|
|
42
|
+
return problems;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const TRANSFORM_CONTEXTS = {
|
|
46
|
+
trace_statements: ["resource", "scope", "span", "spanevent"],
|
|
47
|
+
metric_statements: ["resource", "scope", "metric", "datapoint"],
|
|
48
|
+
log_statements: ["resource", "scope", "log"],
|
|
49
|
+
};
|
|
50
|
+
/** Rewrites telemetry with OTTL statements, grouped per signal and context. */
|
|
51
|
+
export const TransformProcessor = defineBuiltin({
|
|
52
|
+
kind: "processor",
|
|
53
|
+
type: "transform",
|
|
54
|
+
description: "Rewrites spans, metrics and logs with OTTL statements, grouped by context",
|
|
55
|
+
validate: (c) => {
|
|
56
|
+
const problems = [];
|
|
57
|
+
let total = 0;
|
|
58
|
+
for (const field of Object.keys(TRANSFORM_CONTEXTS)) {
|
|
59
|
+
const allowed = TRANSFORM_CONTEXTS[field];
|
|
60
|
+
(c[field] ?? []).forEach((entry, i) => {
|
|
61
|
+
if (typeof entry === "string") {
|
|
62
|
+
if (entry.trim() === "")
|
|
63
|
+
problems.push(`${field}[${i}] is an empty statement`);
|
|
64
|
+
else
|
|
65
|
+
total++;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (entry.context !== undefined && !allowed.includes(entry.context)) {
|
|
69
|
+
problems.push(`${field}[${i}] has context "${entry.context}"; ${field} runs in ${allowed.join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
if (!entry.statements || entry.statements.length === 0) {
|
|
72
|
+
problems.push(`${field}[${i}] has no statements`);
|
|
73
|
+
}
|
|
74
|
+
total += entry.statements?.length ?? 0;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (total === 0)
|
|
78
|
+
problems.push("no statement is set for any signal, so the processor changes nothing");
|
|
79
|
+
return problems;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
/** Deletes attributes whose keys aren't allowed, and masks or hashes values that match blocked patterns. */
|
|
83
|
+
export const RedactionProcessor = defineBuiltin({
|
|
84
|
+
kind: "processor",
|
|
85
|
+
type: "redaction",
|
|
86
|
+
description: "Deletes attributes not on an allow list and masks or hashes values that match block patterns",
|
|
87
|
+
validate: (c) => {
|
|
88
|
+
const problems = [];
|
|
89
|
+
if (c.allow_all_keys !== true && c.allowed_keys === undefined) {
|
|
90
|
+
problems.push("neither allow_all_keys nor allowed_keys is set, so every attribute is deleted; set allow_all_keys: true to keep keys, or allowed_keys: [] if deleting all is intended");
|
|
91
|
+
}
|
|
92
|
+
if (c.allow_all_keys === true && c.allowed_keys !== undefined && c.allowed_keys.length > 0) {
|
|
93
|
+
problems.push("allowed_keys has no effect while allow_all_keys is true");
|
|
94
|
+
}
|
|
95
|
+
for (const field of ["blocked_key_patterns", "blocked_values", "allowed_values"]) {
|
|
96
|
+
(c[field] ?? []).forEach((p, i) => {
|
|
97
|
+
if (typeof p !== "string" || p === "")
|
|
98
|
+
problems.push(`${field}[${i}] is an empty pattern`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (c.hash_function !== undefined && !c.blocked_values?.length && !c.blocked_key_patterns?.length) {
|
|
102
|
+
problems.push("hash_function is set but nothing is blocked, so nothing is hashed");
|
|
103
|
+
}
|
|
104
|
+
return problems;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./common.js";
|
|
2
|
+
export * from "./receivers.js";
|
|
3
|
+
export * from "./k8s-receivers.js";
|
|
4
|
+
export * from "./processors.js";
|
|
5
|
+
export * from "./filtering.js";
|
|
6
|
+
export * from "./exporters.js";
|
|
7
|
+
export * from "./connectors.js";
|
|
8
|
+
export * from "./extensions.js";
|
|
9
|
+
export * from "./sampling.js";
|