@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,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL120: A component in the collector config holds a literal credential
|
|
3
|
+
*
|
|
4
|
+
* OTEL002's check over the emitted or imported config, with the same key pattern (`authorization`, `api-key`, `api_key`, `token`, `password`, `secret`, `key_pem`, `x-honeycomb-team`). A value containing `${` is a reference the collector expands at start-up, and a key ending in `_file` names a path. This catches what OTEL002 can't see: a config brought in by `chant import`, or one inside a ConfigMap.
|
|
5
|
+
*/
|
|
6
|
+
import { configDiagnostics } from "./otel-helpers.js";
|
|
7
|
+
export const otel120 = {
|
|
8
|
+
id: "OTEL120",
|
|
9
|
+
description: "A component in the collector config holds a literal credential",
|
|
10
|
+
check(ctx) {
|
|
11
|
+
return configDiagnostics(ctx, "OTEL120");
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL121: An exporter sends a credential over plaintext
|
|
3
|
+
*
|
|
4
|
+
* A started exporter sends a credential header (`authorization`, `api-key` and the other OTEL002 keys) or a credential key such as `api_key` or `token` to a non-loopback endpoint that is `http://`, or that has `tls.insecure: true` and isn't `https://`.
|
|
5
|
+
*/
|
|
6
|
+
import { configDiagnostics } from "./otel-helpers.js";
|
|
7
|
+
export const otel121 = {
|
|
8
|
+
id: "OTEL121",
|
|
9
|
+
description: "An exporter sends a credential over plaintext",
|
|
10
|
+
check(ctx) {
|
|
11
|
+
return configDiagnostics(ctx, "OTEL121");
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL122: zpages or pprof listens on a non-loopback address
|
|
3
|
+
*
|
|
4
|
+
* zpages serves span samples and pipeline internals, and pprof serves profiles, with no authentication. A started one bound to anything but loopback (`0.0.0.0`, a pod IP, an `${env:...}` host) is reachable by whoever reaches that address. `health_check` is not reported: it serves only a status, and a kubelet probe needs it on the pod IP.
|
|
5
|
+
*/
|
|
6
|
+
import { configDiagnostics } from "./otel-helpers.js";
|
|
7
|
+
export const otel122 = {
|
|
8
|
+
id: "OTEL122",
|
|
9
|
+
description: "zpages or pprof listens on a non-loopback address",
|
|
10
|
+
check(ctx) {
|
|
11
|
+
return configDiagnostics(ctx, "OTEL122");
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL123: A debug exporter at verbosity detailed shares a pipeline with a real exporter
|
|
3
|
+
*
|
|
4
|
+
* `verbosity: detailed` writes every record, attributes and bodies included, to the collector's log. In a pipeline that also sends to a backend, that copies the backend's data, sensitive values included, to wherever the collector's logs go.
|
|
5
|
+
*/
|
|
6
|
+
import { configDiagnostics } from "./otel-helpers.js";
|
|
7
|
+
export const otel123 = {
|
|
8
|
+
id: "OTEL123",
|
|
9
|
+
description: "A debug exporter at verbosity detailed shares a pipeline with a real exporter",
|
|
10
|
+
check(ctx) {
|
|
11
|
+
return configDiagnostics(ctx, "OTEL123");
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL124: A remote exporter has its sending queue or retries turned off
|
|
3
|
+
*
|
|
4
|
+
* A started exporter with a non-loopback endpoint and `sending_queue.enabled: false` blocks the pipeline when the backend is slow; with `retry_on_failure.enabled: false` it drops a failed request's data.
|
|
5
|
+
*/
|
|
6
|
+
import { configDiagnostics } from "./otel-helpers.js";
|
|
7
|
+
export const otel124 = {
|
|
8
|
+
id: "OTEL124",
|
|
9
|
+
description: "A remote exporter has its sending queue or retries turned off",
|
|
10
|
+
check(ctx) {
|
|
11
|
+
return configDiagnostics(ctx, "OTEL124");
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL125: A pipeline sends to a remote otlp or otlphttp exporter without batching
|
|
3
|
+
*
|
|
4
|
+
* No `batch` processor in the pipeline, and the `otlp` or `otlphttp` exporter (non-loopback endpoint) sets no `sending_queue.batch`. At v0.130.0 neither exporter batches by default, so each small request goes out on its own. Other exporters may batch themselves and are not checked.
|
|
5
|
+
*/
|
|
6
|
+
import { configDiagnostics } from "./otel-helpers.js";
|
|
7
|
+
export const otel125 = {
|
|
8
|
+
id: "OTEL125",
|
|
9
|
+
description: "A pipeline sends to a remote otlp or otlphttp exporter without batching",
|
|
10
|
+
check(ctx) {
|
|
11
|
+
return configDiagnostics(ctx, "OTEL125");
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL126: k8sattributes extracts a metadata field it does not support
|
|
3
|
+
*
|
|
4
|
+
* Each name in a `k8sattributes` processor's `extract.metadata` must be one `Config.Validate` accepts at collector-contrib v0.130.0 (`K8S_ATTRIBUTES_METADATA`); the collector refuses any other.
|
|
5
|
+
*/
|
|
6
|
+
import { configDiagnostics } from "./otel-helpers.js";
|
|
7
|
+
export const otel126 = {
|
|
8
|
+
id: "OTEL126",
|
|
9
|
+
description: "k8sattributes extracts a metadata field it does not support",
|
|
10
|
+
check(ctx) {
|
|
11
|
+
return configDiagnostics(ctx, "OTEL126");
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL127: resourcedetection lists a detector that does not exist
|
|
3
|
+
*
|
|
4
|
+
* Each name in a started `resourcedetection` processor's `detectors` must be one the processor registers at collector-contrib v0.130.0 (`RESOURCE_DETECTORS`); any other fails the processor's build and the collector exits.
|
|
5
|
+
*/
|
|
6
|
+
import { configDiagnostics } from "./otel-helpers.js";
|
|
7
|
+
export const otel127 = {
|
|
8
|
+
id: "OTEL127",
|
|
9
|
+
description: "resourcedetection lists a detector that does not exist",
|
|
10
|
+
check(ctx) {
|
|
11
|
+
return configDiagnostics(ctx, "OTEL127");
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
import { isComponentId } from "../../model.js";
|
|
3
|
+
import { calleeName, COMPONENT_CLASS, literalText, position, propertyName } from "./otel-ast.js";
|
|
4
|
+
/**
|
|
5
|
+
* OTEL001: a component id or instance name the collector can't parse.
|
|
6
|
+
*
|
|
7
|
+
* A pipeline may name a component by id string (`"otlp/backend"`), and every
|
|
8
|
+
* component takes an instance `name`. Both end up in the collector's
|
|
9
|
+
* `type[/name]` syntax, where a space, an empty name or a stray `/` is a
|
|
10
|
+
* config error at collector start. This catches the literal cases in source,
|
|
11
|
+
* before a build: a string in a `Pipeline`'s receivers, processors or
|
|
12
|
+
* exporters that isn't `type` or `type/name`, and a component `name` that is
|
|
13
|
+
* empty, contains whitespace or starts with `/`.
|
|
14
|
+
*/
|
|
15
|
+
export const componentIdSyntaxRule = {
|
|
16
|
+
id: "OTEL001",
|
|
17
|
+
severity: "error",
|
|
18
|
+
category: "correctness",
|
|
19
|
+
description: "Collector component id or instance name is not valid type[/name] syntax",
|
|
20
|
+
check(context) {
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
const source = context.sourceFile;
|
|
23
|
+
const flag = (node, message) => {
|
|
24
|
+
diagnostics.push({ ruleId: "OTEL001", severity: "error", message, file: context.filePath, ...position(source, node) });
|
|
25
|
+
};
|
|
26
|
+
const visit = (node) => {
|
|
27
|
+
if (ts.isNewExpression(node) || ts.isCallExpression(node)) {
|
|
28
|
+
const name = calleeName(node);
|
|
29
|
+
const arg = node.arguments?.[0];
|
|
30
|
+
if (name && arg && ts.isObjectLiteralExpression(arg)) {
|
|
31
|
+
if (name === "Pipeline") {
|
|
32
|
+
for (const prop of arg.properties) {
|
|
33
|
+
const key = propertyName(prop);
|
|
34
|
+
if (key !== "receivers" && key !== "processors" && key !== "exporters")
|
|
35
|
+
continue;
|
|
36
|
+
const init = prop.initializer;
|
|
37
|
+
if (!ts.isArrayLiteralExpression(init))
|
|
38
|
+
continue;
|
|
39
|
+
for (const el of init.elements) {
|
|
40
|
+
const text = literalText(el);
|
|
41
|
+
if (text !== undefined && !isComponentId(text)) {
|
|
42
|
+
flag(el, `"${text}" in ${key} is not a collector component id; write type or type/name, e.g. "otlp" or "otlp/backend"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (name === "Pipeline" || COMPONENT_CLASS.test(name)) {
|
|
48
|
+
for (const prop of arg.properties) {
|
|
49
|
+
if (propertyName(prop) !== "name")
|
|
50
|
+
continue;
|
|
51
|
+
const text = literalText(prop.initializer);
|
|
52
|
+
if (text === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
if (text === "" || /\s/.test(text) || text.startsWith("/")) {
|
|
55
|
+
flag(prop, `instance name "${text}" makes an invalid id; a name is non-empty, has no whitespace and does not start with "/"`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
ts.forEachChild(node, visit);
|
|
62
|
+
};
|
|
63
|
+
visit(source);
|
|
64
|
+
return diagnostics;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { componentIdSyntaxRule } from "./component-id-syntax.js";
|
|
2
|
+
import { literalCredentialRule } from "./literal-credential.js";
|
|
3
|
+
export { componentIdSyntaxRule } from "./component-id-syntax.js";
|
|
4
|
+
export { literalCredentialRule } from "./literal-credential.js";
|
|
5
|
+
/** All lint rules provided by this lexicon (imported by plugin.ts's lintRules()). */
|
|
6
|
+
export const rules = [componentIdSyntaxRule, literalCredentialRule];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
import { calleeName, COMPONENT_CLASS, literalText, position, propertyName } from "./otel-ast.js";
|
|
3
|
+
// Shared with OTEL120, which applies the same check to the emitted YAML.
|
|
4
|
+
import { SECRET_KEY } from "../../config-hygiene.js";
|
|
5
|
+
/**
|
|
6
|
+
* OTEL002: a credential written as a literal in a component's config.
|
|
7
|
+
*
|
|
8
|
+
* Exporters authenticate with headers (`authorization`, `api-key`, vendor
|
|
9
|
+
* headers) or keys (`api_key`, `password`, `key_pem`). A literal value there
|
|
10
|
+
* is a secret in the repository and in the emitted YAML. The collector reads
|
|
11
|
+
* `${env:NAME}` and `${file:/path}` at start-up, so the declaration never
|
|
12
|
+
* needs the value itself. Any string containing `${` is treated as such a
|
|
13
|
+
* reference, and keys ending in `_file` name a path and are left alone.
|
|
14
|
+
*/
|
|
15
|
+
export const literalCredentialRule = {
|
|
16
|
+
id: "OTEL002",
|
|
17
|
+
severity: "error",
|
|
18
|
+
category: "security",
|
|
19
|
+
description: "Collector credential declared as a literal instead of ${env:...}",
|
|
20
|
+
check(context) {
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
const source = context.sourceFile;
|
|
23
|
+
// `const x = { ... }` (or `[...]`, or a string) anywhere in the file, so a
|
|
24
|
+
// config value lifted out of the constructor into a named const, the way
|
|
25
|
+
// COR001 asks, is scanned where it is used.
|
|
26
|
+
const consts = new Map();
|
|
27
|
+
const collect = (node) => {
|
|
28
|
+
if (ts.isVariableDeclarationList(node) && node.flags & ts.NodeFlags.Const) {
|
|
29
|
+
for (const d of node.declarations) {
|
|
30
|
+
if (ts.isIdentifier(d.name) && d.initializer)
|
|
31
|
+
consts.set(d.name.text, d.initializer);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
ts.forEachChild(node, collect);
|
|
35
|
+
};
|
|
36
|
+
collect(source);
|
|
37
|
+
const flag = (key, node) => diagnostics.push({
|
|
38
|
+
ruleId: "OTEL002",
|
|
39
|
+
severity: "error",
|
|
40
|
+
message: `\`${key}\` is a literal credential; write "\${env:NAME}" (or "\${file:/path}") and let the collector read it at start-up`,
|
|
41
|
+
file: context.filePath,
|
|
42
|
+
...position(source, node),
|
|
43
|
+
});
|
|
44
|
+
const scanned = new Set();
|
|
45
|
+
const scanValue = (key, value) => {
|
|
46
|
+
let init = value;
|
|
47
|
+
if (ts.isIdentifier(init)) {
|
|
48
|
+
const resolved = consts.get(init.text);
|
|
49
|
+
if (!resolved)
|
|
50
|
+
return;
|
|
51
|
+
init = resolved;
|
|
52
|
+
}
|
|
53
|
+
if (ts.isObjectLiteralExpression(init) || ts.isArrayLiteralExpression(init)) {
|
|
54
|
+
if (scanned.has(init))
|
|
55
|
+
return;
|
|
56
|
+
scanned.add(init);
|
|
57
|
+
if (ts.isObjectLiteralExpression(init))
|
|
58
|
+
scan(init);
|
|
59
|
+
else
|
|
60
|
+
for (const el of init.elements)
|
|
61
|
+
scanValue(undefined, el);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!key || !SECRET_KEY.test(key) || /_file$/i.test(key))
|
|
65
|
+
return;
|
|
66
|
+
const text = literalText(init);
|
|
67
|
+
if (text === undefined || text === "" || text.includes("${"))
|
|
68
|
+
return;
|
|
69
|
+
flag(key, value);
|
|
70
|
+
};
|
|
71
|
+
const scan = (obj) => {
|
|
72
|
+
for (const prop of obj.properties) {
|
|
73
|
+
if (ts.isShorthandPropertyAssignment(prop))
|
|
74
|
+
scanValue(prop.name.text, prop.name);
|
|
75
|
+
else if (ts.isPropertyAssignment(prop))
|
|
76
|
+
scanValue(propertyName(prop), prop.initializer);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const visit = (node) => {
|
|
80
|
+
if (ts.isNewExpression(node) || ts.isCallExpression(node)) {
|
|
81
|
+
const name = calleeName(node);
|
|
82
|
+
const arg = node.arguments?.[0];
|
|
83
|
+
if (name && COMPONENT_CLASS.test(name) && arg && ts.isObjectLiteralExpression(arg))
|
|
84
|
+
scan(arg);
|
|
85
|
+
}
|
|
86
|
+
ts.forEachChild(node, visit);
|
|
87
|
+
};
|
|
88
|
+
visit(source);
|
|
89
|
+
return diagnostics;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
/** The constructor name of a `new X(...)` or `X(...)` expression, if it is a plain or dotted name. */
|
|
3
|
+
export function calleeName(node) {
|
|
4
|
+
const callee = node.expression;
|
|
5
|
+
if (ts.isIdentifier(callee))
|
|
6
|
+
return callee.text;
|
|
7
|
+
if (ts.isPropertyAccessExpression(callee))
|
|
8
|
+
return callee.name.text;
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
/** Built-in and custom collector component classes follow this naming, e.g. `OtlpExporter`. */
|
|
12
|
+
export const COMPONENT_CLASS = /(Receiver|Processor|Exporter|Extension)$/;
|
|
13
|
+
/** A property's key as text, for identifier and string-literal keys. */
|
|
14
|
+
export function propertyName(prop) {
|
|
15
|
+
if (!ts.isPropertyAssignment(prop))
|
|
16
|
+
return undefined;
|
|
17
|
+
if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))
|
|
18
|
+
return prop.name.text;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
/** The text of a string literal or substitution-free template, else undefined. */
|
|
22
|
+
export function literalText(node) {
|
|
23
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
24
|
+
return node.text;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
export function position(source, node) {
|
|
28
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
29
|
+
return { line: line + 1, column: character + 1 };
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LexiconIndex, lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
|
|
2
|
+
import { lexiconRegistry } from "../catalog.js";
|
|
3
|
+
let cachedIndex = null;
|
|
4
|
+
function getIndex() {
|
|
5
|
+
cachedIndex ?? (cachedIndex = new LexiconIndex(lexiconRegistry()));
|
|
6
|
+
return cachedIndex;
|
|
7
|
+
}
|
|
8
|
+
/** Completions for the otel entity classes: the built-in components, Pipeline and Service. */
|
|
9
|
+
export function completions(ctx) {
|
|
10
|
+
return lexiconCompletions(ctx, getIndex(), "OpenTelemetry Collector entity");
|
|
11
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { LexiconIndex, lexiconHover } from "@intentius/chant/lsp/lexicon-providers";
|
|
2
|
+
import { BUILTIN_CATALOG, lexiconRegistry } from "../catalog.js";
|
|
3
|
+
import { COLLECTOR_PIN } from "../define.js";
|
|
4
|
+
let cachedIndex = null;
|
|
5
|
+
function getIndex() {
|
|
6
|
+
cachedIndex ?? (cachedIndex = new LexiconIndex(lexiconRegistry()));
|
|
7
|
+
return cachedIndex;
|
|
8
|
+
}
|
|
9
|
+
/** Hover for otel entity classes: what the component does and its collector id. */
|
|
10
|
+
export function hover(ctx) {
|
|
11
|
+
return lexiconHover(ctx, getIndex(), resourceHover);
|
|
12
|
+
}
|
|
13
|
+
function resourceHover(className, entry) {
|
|
14
|
+
const cat = BUILTIN_CATALOG.find((c) => c.className === className);
|
|
15
|
+
const lines = [`**${className}**`, "", `otel type: \`${entry.resourceType}\``];
|
|
16
|
+
if (cat?.description)
|
|
17
|
+
lines.push("", cat.description);
|
|
18
|
+
if (cat?.type) {
|
|
19
|
+
lines.push("", `Collector id \`${cat.type}\`, or \`${cat.type}/<name>\` with \`name\` set. ` +
|
|
20
|
+
`Config typed against ${COLLECTOR_PIN.source} ${COLLECTOR_PIN.version}.`);
|
|
21
|
+
}
|
|
22
|
+
return { contents: lines.join("\n") };
|
|
23
|
+
}
|
package/dist/manifest.json
CHANGED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The metric names a collector emits, as the `prometheus` exporter serves
|
|
3
|
+
* them: `spanMetricsNames()` for a `spanmetrics` connector, and the naming
|
|
4
|
+
* rules `genAiMetrics()` uses too.
|
|
5
|
+
*
|
|
6
|
+
* A dashboard or an SLO reads names from here instead of repeating them, so
|
|
7
|
+
* renaming the connector's namespace (or the exporter's) moves every query
|
|
8
|
+
* built from it.
|
|
9
|
+
*
|
|
10
|
+
* Naming follows the collector's Prometheus translation at `COLLECTOR_PIN`
|
|
11
|
+
* with the exporter's defaults: `.` and any other character Prometheus does
|
|
12
|
+
* not allow become `_`; counters take `_total`; a unit becomes a suffix
|
|
13
|
+
* (`ms` is `_milliseconds`, `s` is `_seconds`); the exporter's `namespace`
|
|
14
|
+
* is a prefix joined with `_`; `add_metric_suffixes: false` drops the
|
|
15
|
+
* suffixes. Attribute names become label names the same way
|
|
16
|
+
* (`service.name` is `service_name`).
|
|
17
|
+
*
|
|
18
|
+
* This module holds types and string functions only, so a lexicon that
|
|
19
|
+
* builds queries from it does not load the collector components.
|
|
20
|
+
*/
|
|
21
|
+
import { canonicalComponentType } from "./model.js";
|
|
22
|
+
/** The four dimensions spanmetrics puts on every metric unless `exclude_dimensions` names them. */
|
|
23
|
+
export const SPANMETRICS_DEFAULT_DIMENSIONS = Object.freeze(["service.name", "span.name", "span.kind", "status.code"]);
|
|
24
|
+
/** The spanmetrics namespace when the connector sets none. */
|
|
25
|
+
export const SPANMETRICS_DEFAULT_NAMESPACE = "traces.span.metrics";
|
|
26
|
+
/** The `status.code` value of a span that ended in error: the value an error-ratio query selects. */
|
|
27
|
+
export const SPAN_STATUS_ERROR = "STATUS_CODE_ERROR";
|
|
28
|
+
/** The Prometheus unit words for the units collector components use. */
|
|
29
|
+
const UNIT_WORDS = {
|
|
30
|
+
ms: "milliseconds",
|
|
31
|
+
s: "seconds",
|
|
32
|
+
us: "microseconds",
|
|
33
|
+
ns: "nanoseconds",
|
|
34
|
+
By: "bytes",
|
|
35
|
+
"1": "ratio",
|
|
36
|
+
};
|
|
37
|
+
/** An attribute name as a Prometheus label: `service.name` is `service_name`. */
|
|
38
|
+
export function prometheusLabel(attribute) {
|
|
39
|
+
const label = attribute.replace(/[^A-Za-z0-9_]/g, "_");
|
|
40
|
+
return /^[0-9]/.test(label) ? `key_${label}` : label;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* An OTLP metric name as the `prometheus` exporter serves it.
|
|
44
|
+
*
|
|
45
|
+
* @param type `sum` (a monotonic counter, `_total`) or `histogram`.
|
|
46
|
+
* @param unit The metric's unit, e.g. `ms`. Units in braces (`{call}`) add nothing.
|
|
47
|
+
*/
|
|
48
|
+
export function prometheusMetricName(name, type, unit, naming = {}) {
|
|
49
|
+
let out = name.replace(/[^A-Za-z0-9_:]/g, "_");
|
|
50
|
+
if (naming.namespace)
|
|
51
|
+
out = `${naming.namespace.replace(/[^A-Za-z0-9_:]/g, "_")}_${out}`;
|
|
52
|
+
if (naming.add_metric_suffixes === false)
|
|
53
|
+
return out;
|
|
54
|
+
const word = unit && !unit.startsWith("{") ? (UNIT_WORDS[unit] ?? unit.replace(/[^A-Za-z0-9_:]/g, "_")) : undefined;
|
|
55
|
+
if (word && !out.endsWith(`_${word}`))
|
|
56
|
+
out = `${out}_${word}`;
|
|
57
|
+
if (type === "sum" && !out.endsWith("_total"))
|
|
58
|
+
out = `${out}_total`;
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function configOf(connector) {
|
|
62
|
+
const d = connector;
|
|
63
|
+
if (typeof d.componentType === "string") {
|
|
64
|
+
if (canonicalComponentType("connector", d.componentType) !== "spanmetrics")
|
|
65
|
+
throw new Error(`spanMetricsNames: expected a spanmetrics connector, got ${d.componentType}`);
|
|
66
|
+
return (d.props ?? {});
|
|
67
|
+
}
|
|
68
|
+
return connector;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The Prometheus names of the metrics a `spanmetrics` connector emits, read
|
|
72
|
+
* from its declaration: its namespace, dimensions and histogram unit, and
|
|
73
|
+
* optionally the `prometheus` exporter's namespace and suffix setting.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const spans = new SpanMetricsConnector({ namespace: "shop", histogram: { unit: "s" } });
|
|
78
|
+
* spanMetricsNames(spans).calls.prometheus; // "shop_calls_total"
|
|
79
|
+
* spanMetricsNames(spans).duration!.prometheus; // "shop_duration_seconds"
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function spanMetricsNames(connector, exporter) {
|
|
83
|
+
const c = configOf(connector);
|
|
84
|
+
const naming = exporter && "props" in exporter && typeof exporter.props === "object" ? exporter.props : (exporter ?? {});
|
|
85
|
+
const namespace = c.namespace ?? SPANMETRICS_DEFAULT_NAMESPACE;
|
|
86
|
+
const metricName = (n) => (namespace === "" ? n : `${namespace}.${n}`);
|
|
87
|
+
const excluded = new Set(c.exclude_dimensions ?? []);
|
|
88
|
+
const defaults = SPANMETRICS_DEFAULT_DIMENSIONS.filter((d) => !excluded.has(d));
|
|
89
|
+
const names = (extra) => (extra ?? []).map((d) => d.name);
|
|
90
|
+
const common = [...defaults, ...names(c.dimensions)];
|
|
91
|
+
const metric = (n, type, dims, unit) => ({
|
|
92
|
+
name: metricName(n),
|
|
93
|
+
prometheus: prometheusMetricName(metricName(n), type, unit, naming),
|
|
94
|
+
type,
|
|
95
|
+
...(unit ? { unit } : {}),
|
|
96
|
+
dimensions: dims,
|
|
97
|
+
});
|
|
98
|
+
const label = (attr) => (defaults.includes(attr) ? prometheusLabel(attr) : undefined);
|
|
99
|
+
const labels = {};
|
|
100
|
+
const service = label("service.name");
|
|
101
|
+
const spanName = label("span.name");
|
|
102
|
+
const spanKind = label("span.kind");
|
|
103
|
+
const statusCode = label("status.code");
|
|
104
|
+
if (service)
|
|
105
|
+
labels.service = service;
|
|
106
|
+
if (spanName)
|
|
107
|
+
labels.spanName = spanName;
|
|
108
|
+
if (spanKind)
|
|
109
|
+
labels.spanKind = spanKind;
|
|
110
|
+
if (statusCode)
|
|
111
|
+
labels.statusCode = statusCode;
|
|
112
|
+
return {
|
|
113
|
+
namespace,
|
|
114
|
+
calls: metric("calls", "sum", [...common, ...names(c.calls_dimensions)]),
|
|
115
|
+
...(c.histogram?.disable ? {} : { duration: metric("duration", "histogram", [...common, ...names(c.histogram?.dimensions)], c.histogram?.unit ?? "ms") }),
|
|
116
|
+
...(c.events?.enabled ? { events: metric("events", "sum", [...common, ...names(c.events.dimensions)]) } : {}),
|
|
117
|
+
labels,
|
|
118
|
+
errorStatus: SPAN_STATUS_ERROR,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** The prefix of every metric the `servicegraph` connector emits; the connector takes no namespace. */
|
|
122
|
+
export const SERVICEGRAPH_NAMESPACE = "traces_service_graph";
|
|
123
|
+
/**
|
|
124
|
+
* The Prometheus names of the metrics a `servicegraph` connector emits at
|
|
125
|
+
* `COLLECTOR_PIN`, with its default feature gates: latency in seconds, under
|
|
126
|
+
* `_request_server` and `_request_client`. Every metric carries `client`,
|
|
127
|
+
* `server`, `connection_type` and `failed`, `client_<d>` and `server_<d>` for
|
|
128
|
+
* each configured dimension, and `virtual_node` when
|
|
129
|
+
* `virtual_node_extra_label` is set (connector/servicegraphconnector/connector.go).
|
|
130
|
+
*/
|
|
131
|
+
export function serviceGraphNames(connector, exporter) {
|
|
132
|
+
const d = connector;
|
|
133
|
+
if (typeof d.componentType === "string" && d.componentType !== "servicegraph") {
|
|
134
|
+
throw new Error(`serviceGraphNames: expected a servicegraph connector, got ${d.componentType}`);
|
|
135
|
+
}
|
|
136
|
+
const c = (typeof d.componentType === "string" ? (d.props ?? {}) : connector);
|
|
137
|
+
const naming = exporter && "props" in exporter && typeof exporter.props === "object" ? exporter.props : (exporter ?? {});
|
|
138
|
+
const dimensions = [
|
|
139
|
+
"client",
|
|
140
|
+
"server",
|
|
141
|
+
"connection_type",
|
|
142
|
+
"failed",
|
|
143
|
+
...(c.dimensions ?? []).flatMap((dim) => [`client_${dim}`, `server_${dim}`]),
|
|
144
|
+
...(c.virtual_node_extra_label ? ["virtual_node"] : []),
|
|
145
|
+
];
|
|
146
|
+
const metric = (n, type, unit) => ({
|
|
147
|
+
name: `${SERVICEGRAPH_NAMESPACE}_${n}`,
|
|
148
|
+
prometheus: prometheusMetricName(`${SERVICEGRAPH_NAMESPACE}_${n}`, type, unit, naming),
|
|
149
|
+
type,
|
|
150
|
+
...(unit ? { unit } : {}),
|
|
151
|
+
dimensions,
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
requests: metric("request_total", "sum"),
|
|
155
|
+
failed: metric("request_failed_total", "sum"),
|
|
156
|
+
serverDuration: metric("request_server", "histogram", "s"),
|
|
157
|
+
clientDuration: metric("request_client", "histogram", "s"),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** `metric{a="x", b=~"y"}`; just `metric` with no matchers. */
|
|
161
|
+
export function promSelector(metric, matchers) {
|
|
162
|
+
if (matchers.length === 0)
|
|
163
|
+
return metric;
|
|
164
|
+
return `${metric}{${matchers.map(([l, op, v]) => `${l}${op}${JSON.stringify(v)}`).join(", ")}}`;
|
|
165
|
+
}
|
|
166
|
+
/** `sum by (labels) (rate(sel[range]))`, or `sum (...)` with no labels. */
|
|
167
|
+
export function promSumRate(sel, by, range) {
|
|
168
|
+
return `sum${by.length ? ` by (${by.join(", ")})` : ""} (rate(${sel}[${range}]))`;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* `errors / all` as rates summed by `by`, with the numerator padded to zero:
|
|
172
|
+
* `(errors or all * 0) / all`. A group with calls but no error series gets 0
|
|
173
|
+
* instead of dropping out of the result.
|
|
174
|
+
*/
|
|
175
|
+
export function promErrorRatio(errorSel, allSel, by, range) {
|
|
176
|
+
const all = promSumRate(allSel, by, range);
|
|
177
|
+
return `(\n${promSumRate(errorSel, by, range)}\nor\n${all} * 0\n)\n/\n${all}`;
|
|
178
|
+
}
|
|
179
|
+
/** A number as PromQL writes it, without float noise. */
|
|
180
|
+
export function promNumber(n) {
|
|
181
|
+
return String(Number(n.toPrecision(10)));
|
|
182
|
+
}
|
|
183
|
+
/** `histogram_quantile(q, sum by (le, labels) (rate(bucket[range])))`. */
|
|
184
|
+
export function promQuantile(q, bucketSel, by, range) {
|
|
185
|
+
return `histogram_quantile(${promNumber(q)}, ${promSumRate(bucketSel, ["le", ...by], range)})`;
|
|
186
|
+
}
|
|
187
|
+
/** Every `span_kind` value, in the order the spec lists them. */
|
|
188
|
+
export const SPAN_METRICS_KINDS = Object.freeze([
|
|
189
|
+
"SPAN_KIND_UNSPECIFIED",
|
|
190
|
+
"SPAN_KIND_INTERNAL",
|
|
191
|
+
"SPAN_KIND_SERVER",
|
|
192
|
+
"SPAN_KIND_CLIENT",
|
|
193
|
+
"SPAN_KIND_PRODUCER",
|
|
194
|
+
"SPAN_KIND_CONSUMER",
|
|
195
|
+
]);
|
|
196
|
+
/** The kinds RED queries count unless told otherwise: the spans that serve a request or consume a message. */
|
|
197
|
+
export const RED_DEFAULT_SPAN_KINDS = Object.freeze(["SPAN_KIND_SERVER", "SPAN_KIND_CONSUMER"]);
|
|
198
|
+
const DEFAULT_RED_QUANTILES = [0.5, 0.95, 0.99];
|
|
199
|
+
/**
|
|
200
|
+
* The span-kind matcher for `kinds`, or none: none for `[]`, and none for
|
|
201
|
+
* the default kinds when the connector excludes `span.kind`.
|
|
202
|
+
*/
|
|
203
|
+
export function spanKindMatchers(names, kinds, owner = "spanKindMatchers") {
|
|
204
|
+
const label = names.labels.spanKind;
|
|
205
|
+
if (kinds === undefined)
|
|
206
|
+
return label ? [[label, "=~", RED_DEFAULT_SPAN_KINDS.join("|")]] : [];
|
|
207
|
+
for (const k of kinds) {
|
|
208
|
+
if (!SPAN_METRICS_KINDS.includes(k))
|
|
209
|
+
throw new Error(`${owner}: unknown span kind ${JSON.stringify(k)}; use one of ${SPAN_METRICS_KINDS.join(", ")}`);
|
|
210
|
+
}
|
|
211
|
+
if (kinds.length === 0)
|
|
212
|
+
return [];
|
|
213
|
+
if (!label)
|
|
214
|
+
throw new Error(`${owner}: the connector excludes span.kind, so the metrics can't be filtered by spanKinds`);
|
|
215
|
+
return [[label, "=~", [...new Set(kinds)].join("|")]];
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Rate, error ratio and duration quantiles per service, from a spanmetrics
|
|
219
|
+
* connector's names. The grafana lexicon's `RedDashboard` and the prometheus
|
|
220
|
+
* lexicon's `RedAlerts` both build their queries here.
|
|
221
|
+
*/
|
|
222
|
+
export function spanMetricsRedQueries(names, options) {
|
|
223
|
+
const owner = options.owner ?? "spanMetricsRedQueries";
|
|
224
|
+
const svc = names.labels.service;
|
|
225
|
+
const status = names.labels.statusCode;
|
|
226
|
+
if (!svc)
|
|
227
|
+
throw new Error(`${owner}: the connector excludes service.name, so there is no service to break the metrics down by`);
|
|
228
|
+
if (!status)
|
|
229
|
+
throw new Error(`${owner}: the connector excludes status.code, so errors can't be told from successes`);
|
|
230
|
+
const kind = spanKindMatchers(names, options.spanKinds, owner);
|
|
231
|
+
const scope = [...(options.serviceMatch !== undefined ? [[svc, "=~", options.serviceMatch]] : []), ...kind];
|
|
232
|
+
const by = [svc, ...(options.by ?? [])];
|
|
233
|
+
const calls = promSelector(names.calls.prometheus, scope);
|
|
234
|
+
const errors = promSelector(names.calls.prometheus, [...scope, [status, "=", names.errorStatus]]);
|
|
235
|
+
const buckets = names.duration ? promSelector(`${names.duration.prometheus}_bucket`, scope) : undefined;
|
|
236
|
+
const quantiles = options.quantiles ?? DEFAULT_RED_QUANTILES;
|
|
237
|
+
return {
|
|
238
|
+
service: svc,
|
|
239
|
+
kindMatchers: kind,
|
|
240
|
+
rate: promSumRate(calls, by, options.range),
|
|
241
|
+
errorRatio: promErrorRatio(errors, calls, by, options.range),
|
|
242
|
+
duration: buckets ? quantiles.map((q) => ({ quantile: q, expr: promQuantile(q, buckets, by, options.range) })) : [],
|
|
243
|
+
};
|
|
244
|
+
}
|