@intentius/chant-lexicon-otel 0.101.0 → 0.104.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/attribution.d.ts +69 -0
- package/dist/attribution.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/connectors.d.ts.map +1 -1
- package/dist/components/processors.d.ts +1 -1
- package/dist/components/processors.d.ts.map +1 -1
- package/dist/components/sampling.d.ts +3 -0
- package/dist/components/sampling.d.ts.map +1 -1
- package/dist/composites/node-agent.d.ts +4 -3
- package/dist/composites/node-agent.d.ts.map +1 -1
- package/dist/config-hygiene.d.ts +33 -0
- package/dist/config-hygiene.d.ts.map +1 -0
- package/dist/define.d.ts +18 -2
- package/dist/define.d.ts.map +1 -1
- package/dist/genai.d.ts.map +1 -1
- package/dist/import/generator.d.ts +5 -1
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/init-templates.d.ts.map +1 -1
- package/dist/integrity.json +15 -5
- package/dist/lint/audit-catalog.d.ts +3 -2
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/otel-helpers.d.ts +2 -2
- package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/otel118.d.ts +8 -0
- package/dist/lint/post-synth/otel118.d.ts.map +1 -0
- package/dist/lint/post-synth/otel119.d.ts +8 -0
- package/dist/lint/post-synth/otel119.d.ts.map +1 -0
- package/dist/lint/post-synth/otel120.d.ts +8 -0
- package/dist/lint/post-synth/otel120.d.ts.map +1 -0
- package/dist/lint/post-synth/otel121.d.ts +8 -0
- package/dist/lint/post-synth/otel121.d.ts.map +1 -0
- package/dist/lint/post-synth/otel122.d.ts +8 -0
- package/dist/lint/post-synth/otel122.d.ts.map +1 -0
- package/dist/lint/post-synth/otel123.d.ts +8 -0
- package/dist/lint/post-synth/otel123.d.ts.map +1 -0
- package/dist/lint/post-synth/otel124.d.ts +8 -0
- package/dist/lint/post-synth/otel124.d.ts.map +1 -0
- package/dist/lint/post-synth/otel125.d.ts +8 -0
- package/dist/lint/post-synth/otel125.d.ts.map +1 -0
- package/dist/lint/post-synth/otel126.d.ts +8 -0
- package/dist/lint/post-synth/otel126.d.ts.map +1 -0
- package/dist/lint/post-synth/otel127.d.ts +8 -0
- package/dist/lint/post-synth/otel127.d.ts.map +1 -0
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/metric-names.d.ts +29 -0
- package/dist/metric-names.d.ts.map +1 -1
- package/dist/model.d.ts +24 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/okf/index.md +10 -0
- package/dist/okf/rules/OTEL118.md +11 -0
- package/dist/okf/rules/OTEL119.md +11 -0
- package/dist/okf/rules/OTEL120.md +11 -0
- package/dist/okf/rules/OTEL121.md +11 -0
- package/dist/okf/rules/OTEL122.md +11 -0
- package/dist/okf/rules/OTEL123.md +11 -0
- package/dist/okf/rules/OTEL124.md +11 -0
- package/dist/okf/rules/OTEL125.md +11 -0
- package/dist/okf/rules/OTEL126.md +11 -0
- package/dist/okf/rules/OTEL127.md +11 -0
- package/dist/platform.d.ts +11 -4
- package/dist/platform.d.ts.map +1 -1
- package/dist/rules/literal-credential.ts +2 -3
- package/dist/rules/otel-helpers.ts +15 -4
- package/dist/rules/otel118.ts +17 -0
- package/dist/rules/otel119.ts +17 -0
- package/dist/rules/otel120.ts +17 -0
- package/dist/rules/otel121.ts +17 -0
- package/dist/rules/otel122.ts +17 -0
- package/dist/rules/otel123.ts +17 -0
- package/dist/rules/otel124.ts +17 -0
- package/dist/rules/otel125.ts +17 -0
- package/dist/rules/otel126.ts +17 -0
- package/dist/rules/otel127.ts +17 -0
- package/dist/semconv.d.ts +2 -1
- package/dist/semconv.d.ts.map +1 -1
- package/dist/skill-defs.d.ts +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-otel.md +7 -3
- package/dist/validate-config.d.ts +29 -3
- package/dist/validate-config.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/attribution.ts +385 -0
- package/src/codegen/docs.ts +8 -1
- package/src/components/k8s-receivers.test.ts +5 -2
- package/src/components/processors.ts +1 -1
- package/src/components/sampling.ts +3 -0
- package/src/composites/composites.test.ts +16 -0
- package/src/composites/node-agent.ts +3 -2
- package/src/config-hygiene.ts +470 -0
- package/src/connectors.test.ts +3 -0
- package/src/define.ts +25 -4
- package/src/import/generator.test.ts +42 -4
- package/src/import/generator.ts +7 -2
- package/src/import/parser.test.ts +11 -0
- package/src/import/parser.ts +16 -4
- package/src/import/roundtrip.test.ts +90 -14
- package/src/index.ts +7 -0
- package/src/init-templates.ts +4 -2
- package/src/lint/audit-catalog.ts +87 -2
- package/src/lint/post-synth/hygiene.test.ts +334 -0
- package/src/lint/post-synth/index.ts +20 -0
- package/src/lint/post-synth/otel-helpers.ts +15 -4
- package/src/lint/post-synth/otel118.test.ts +308 -0
- package/src/lint/post-synth/otel118.ts +17 -0
- package/src/lint/post-synth/otel119.ts +17 -0
- package/src/lint/post-synth/otel120.ts +17 -0
- package/src/lint/post-synth/otel121.ts +17 -0
- package/src/lint/post-synth/otel122.ts +17 -0
- package/src/lint/post-synth/otel123.ts +17 -0
- package/src/lint/post-synth/otel124.ts +17 -0
- package/src/lint/post-synth/otel125.ts +17 -0
- package/src/lint/post-synth/otel126.ts +17 -0
- package/src/lint/post-synth/otel127.ts +17 -0
- package/src/lint/post-synth/post-synth.test.ts +31 -0
- package/src/lint/rules/literal-credential.ts +2 -3
- package/src/metric-names.test.ts +24 -2
- package/src/metric-names.ts +66 -1
- package/src/model.ts +48 -0
- package/src/platform.test.ts +61 -0
- package/src/platform.ts +35 -22
- package/src/plugin.test.ts +10 -0
- package/src/semconv.ts +6 -2
- package/src/skills/chant-otel.md +7 -3
- package/src/validate-config.ts +85 -9
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { configDiagnostics } from "./otel-helpers";
|
|
9
|
+
|
|
10
|
+
export const otel127: PostSynthCheck = {
|
|
11
|
+
id: "OTEL127",
|
|
12
|
+
description: "resourcedetection lists a detector that does not exist",
|
|
13
|
+
|
|
14
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
15
|
+
return configDiagnostics(ctx, "OTEL127");
|
|
16
|
+
},
|
|
17
|
+
};
|
package/dist/semconv.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* A collector config has no metadata channel, so a config cannot say which
|
|
6
6
|
* semconv version its attribute keys follow. What it can show is that it uses
|
|
7
7
|
* a vocabulary: a `spanmetrics` dimension named `gen_ai.request.model`, an
|
|
8
|
-
* OTTL statement deleting `gen_ai.input.messages
|
|
8
|
+
* OTTL statement deleting `gen_ai.input.messages`, a `k8sattributes`
|
|
9
|
+
* processor extracting `k8s.pod.name`. `semconvUsage()` finds
|
|
9
10
|
* those and pairs each vocabulary with the pin this package's keys follow, the
|
|
10
11
|
* same way `collectorTopology()` pairs a component type with the pin of the
|
|
11
12
|
* definition this process has. Reading a parsed YAML file and reading the
|
package/dist/semconv.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semconv.d.ts","sourceRoot":"","sources":["../src/semconv.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"semconv.d.ts","sourceRoot":"","sources":["../src/semconv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAc,KAAK,eAAe,EAAsB,MAAM,SAAS,CAAC;AAE/E,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,SAAS,CAAC;IACf,2EAA2E;IAC3E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACpC;AAOD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,iBAAiB,CAGhE,CAAC;AAEH,mEAAmE;AACnE,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAWD,0GAA0G;AAC1G,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,YAAY,EAAE,CAYpE"}
|
package/dist/skill-defs.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** The otel lexicon's AI skills, read from src/skills/. */
|
|
2
|
-
export declare const otelSkills: () => import("
|
|
2
|
+
export declare const otelSkills: () => import("@intentius/chant").SkillDefinition[];
|
|
3
3
|
//# sourceMappingURL=skill-defs.d.ts.map
|
package/dist/skill-defs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-defs.d.ts","sourceRoot":"","sources":["../src/skill-defs.ts"],"names":[],"mappings":"AAEA,2DAA2D;AAC3D,eAAO,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"skill-defs.d.ts","sourceRoot":"","sources":["../src/skill-defs.ts"],"names":[],"mappings":"AAEA,2DAA2D;AAC3D,eAAO,MAAM,UAAU,oDAgCrB,CAAC"}
|
|
@@ -17,7 +17,7 @@ export default { lexicons: ["otel"] };
|
|
|
17
17
|
|
|
18
18
|
`chant init --lexicon otel` scaffolds a collector; `--template k8s-agent` scaffolds a per-node Kubernetes agent and `--template genai` a GenAI collector.
|
|
19
19
|
|
|
20
|
-
For a per-node Kubernetes agent, `NodeAgent({ exporters, metricExporters?, clusterName?, kubeletStats? })` declares the whole config: OTLP, host metrics, container logs, `k8sattributes` filtered to the node, and `memory_limiter` first and `batch` last on every pipeline. Pass `Object.values(agent.members)` wherever an entity list is taken, such as the k8s lexicon's `OtelCollector` `config
|
|
20
|
+
For a per-node Kubernetes agent, `NodeAgent({ exporters, metricExporters?, clusterName?, kubeletStats? })` declares the whole config: OTLP, host metrics, container logs, `k8sattributes` filtered to the node, and `memory_limiter` first and `batch` last on every pipeline. Pass `Object.values(agent.members)` wherever an entity list is taken, such as the k8s lexicon's `OtelCollector` `config`, which adds what the config reads from the node: `K8S_NODE_NAME` from `spec.nodeName`, the host root at `/hostfs` and `/var/log/pods` read-only, group 0 for the log files, and the RBAC.
|
|
21
21
|
|
|
22
22
|
## Declaring components
|
|
23
23
|
|
|
@@ -78,10 +78,14 @@ It deletes prompt, completion, system-instruction and tool-call content from spa
|
|
|
78
78
|
|
|
79
79
|
- Reference components by entity where you can. A string id (`"otlp/backend"`) is allowed for a component declared elsewhere, and OTEL101 fails the build if nothing declares it.
|
|
80
80
|
- Put `memory_limiter` first in `processors` (OTEL105).
|
|
81
|
-
- Never write a credential literally. Use `"${env:NAME}"` and the collector reads it at start-up (OTEL002).
|
|
81
|
+
- Never write a credential literally. Use `"${env:NAME}"` and the collector reads it at start-up (OTEL002 in source, OTEL120 in the config). Don't send one over `http://` or `tls.insecure: true` (OTEL121).
|
|
82
82
|
- Every pipeline needs at least one receiver and one exporter (OTEL102). A declared component no pipeline uses is a warning (OTEL103).
|
|
83
83
|
- Don't split metrics by a per-request id (`gen_ai.conversation.id`, `gen_ai.response.id`, `gen_ai.tool.call.id`, `session.id`, `user.id`, `enduser.id`) or a content key: each value starts new time series (OTEL116).
|
|
84
84
|
- Give every listener its own port. The collector's own metrics already use localhost:8888, so a `prometheus` exporter there makes it exit with "address already in use" (OTEL117).
|
|
85
|
+
- Keep `zpages` and `pprof` on localhost (OTEL122). `health_check` may listen on 0.0.0.0 for probes.
|
|
86
|
+
- Put `batch` in every pipeline that sends to a remote `otlp` or `otlphttp` exporter (OTEL125), and leave the exporter's `sending_queue` and `retry_on_failure` on (OTEL124).
|
|
87
|
+
- Use a `drop` policy rather than `invert_match`, and `readers` rather than `service.telemetry.metrics.address` (OTEL119).
|
|
88
|
+
- In a workspace, or with `telemetry.attribution: true`, leave `service.name`, `service.version`, `deployment.environment.name`, `vcs.ref.head.revision` and `chant.*` resource attributes alone: `insert` rather than `upsert`, and `override: false` on `resourcedetection` (OTEL118).
|
|
85
89
|
- Declared extensions are enabled in declaration order unless a `Service` lists `extensions` itself.
|
|
86
90
|
|
|
87
91
|
## Starting from an existing config
|
|
@@ -97,4 +101,4 @@ The importer writes `receivers.ts`, `processors.ts`, `exporters.ts`, `connectors
|
|
|
97
101
|
|
|
98
102
|
## Reading the result
|
|
99
103
|
|
|
100
|
-
`collectorTopologyOf(entities)` returns the pipelines, each component's endpoints and schema pin, for each exporter the signals it carries, the connector `edges` between pipelines, and under `semconv` the semantic-conventions version
|
|
104
|
+
`collectorTopologyOf(entities)` returns the pipelines, each component's endpoints and schema pin, for each exporter the signals it carries, the connector `edges` between pipelines, and under `semconv` the semantic-conventions version the config's `gen_ai.` keys (`GENAI_SEMCONV_PIN`) and `k8s.` keys (`SEMCONV_PIN`) follow, as plain data.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
11
11
|
import { type CollectorConfig } from "./model.js";
|
|
12
12
|
import "./components/connectors.js";
|
|
13
|
-
export type CollectorIssueCode = "OTEL101" | "OTEL102" | "OTEL103" | "OTEL104" | "OTEL105" | "OTEL106" | "OTEL107" | "OTEL108" | "OTEL109" | "OTEL112" | "OTEL113" | "OTEL114" | "OTEL115" | "OTEL116" | "OTEL117";
|
|
13
|
+
export type CollectorIssueCode = "OTEL101" | "OTEL102" | "OTEL103" | "OTEL104" | "OTEL105" | "OTEL106" | "OTEL107" | "OTEL108" | "OTEL109" | "OTEL112" | "OTEL113" | "OTEL114" | "OTEL115" | "OTEL116" | "OTEL117" | "OTEL118" | "OTEL119" | "OTEL120" | "OTEL121" | "OTEL122" | "OTEL123" | "OTEL124" | "OTEL125" | "OTEL126" | "OTEL127";
|
|
14
14
|
export interface CollectorIssue {
|
|
15
15
|
code: CollectorIssueCode;
|
|
16
16
|
severity: "error" | "warning";
|
|
@@ -25,10 +25,36 @@ export interface CollectorIssue {
|
|
|
25
25
|
* each connector's signals against its definition (OTEL112), cycles through
|
|
26
26
|
* connectors (OTEL113), connector ids shared with a receiver or exporter
|
|
27
27
|
* (OTEL114), `routing` connector targets (OTEL115), the attributes
|
|
28
|
-
* connectors split metrics by (OTEL116),
|
|
29
|
-
* on the same address (OTEL117)
|
|
28
|
+
* connectors split metrics by (OTEL116), started components that listen
|
|
29
|
+
* on the same address (OTEL117), and the deprecation, exposure, delivery and
|
|
30
|
+
* name-list checks in `config-hygiene.ts` (OTEL119-OTEL127).
|
|
30
31
|
*/
|
|
31
32
|
export declare function validateCollectorConfig(config: CollectorConfig): CollectorIssue[];
|
|
33
|
+
/** One address a started component listens on. */
|
|
34
|
+
export interface Listener {
|
|
35
|
+
/** Who listens: `receiver "otlp" (protocols.grpc.endpoint)`. */
|
|
36
|
+
owner: string;
|
|
37
|
+
component?: string;
|
|
38
|
+
host: string;
|
|
39
|
+
port: number;
|
|
40
|
+
transport: "tcp" | "udp";
|
|
41
|
+
}
|
|
42
|
+
/** A config key path a component listens on, and the transport it listens with. */
|
|
43
|
+
export interface ListenerPath {
|
|
44
|
+
path: string[];
|
|
45
|
+
transport: "tcp" | "udp";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The config key paths to the addresses a receiver or exporter of this type
|
|
49
|
+
* listens on, from the table OTEL117 checks, with the transport each listens
|
|
50
|
+
* with in `body`. Empty for a type not in the table, whose `endpoint` may be
|
|
51
|
+
* a server it connects to. `collectorEndpoints` reads the ports from here, so
|
|
52
|
+
* the ports a platform composite publishes and the listeners OTEL117 compares
|
|
53
|
+
* are the same set.
|
|
54
|
+
*/
|
|
55
|
+
export declare function listenerPaths(kind: "receiver" | "exporter", type: string, body: unknown): ListenerPath[];
|
|
56
|
+
/** The addresses a component of this kind listens on, from the OTEL117 table and its defaults. */
|
|
57
|
+
export declare function componentListeners(kind: "receiver" | "exporter" | "extension", id: string, body: unknown): Listener[];
|
|
32
58
|
/**
|
|
33
59
|
* Check declared entities for what only the declaration knows (OTEL107-OTEL109):
|
|
34
60
|
* each component's own config rules, duplicate ids, and custom components'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-config.d.ts","sourceRoot":"","sources":["../src/validate-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,OAAO,
|
|
1
|
+
{"version":3,"file":"validate-config.d.ts","sourceRoot":"","sources":["../src/validate-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,OAAO,EAAoF,KAAK,eAAe,EAA4B,MAAM,SAAS,CAAC;AAO3J,OAAO,yBAAyB,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,EAAE,CAmKjF;AA6OD,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;CAC1B;AA4ED,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;CAC1B;AAYD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,YAAY,EAAE,CAExG;AAuBD,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,CAWrH;AAqHD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,cAAc,EAAE,CAyDpH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-otel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.104.0",
|
|
4
4
|
"description": "OpenTelemetry Collector lexicon for chant — typed receivers, processors, exporters, connectors, extensions and pipelines, serialized to collector YAML",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"validate": "tsx src/validate-cli.ts",
|
|
57
57
|
"docs": "tsx src/codegen/docs-cli.ts",
|
|
58
58
|
"prepack": "npm run generate && npm run bundle && npm run validate && npm run build",
|
|
59
|
-
"build": "
|
|
59
|
+
"build": "../../scripts/tsc7.sh -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && find dist -type f \\( -name \"*.js\" -o -name \"*.js.map\" \\) -delete"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"js-yaml": "^4.3.1"
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"typescript": "^5.9.3"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@intentius/chant": "^0.
|
|
71
|
+
"@intentius/chant": "^0.104.0",
|
|
72
72
|
"typescript": "^5.9.3"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTEL118: a collector processor that can remove or replace a resource
|
|
3
|
+
* attribute chant's telemetry attribution stamps (ws-060, #3375).
|
|
4
|
+
*
|
|
5
|
+
* A workload built inside a workspace carries `service.name`,
|
|
6
|
+
* `service.version`, `deployment.environment.name`, `vcs.ref.head.revision`
|
|
7
|
+
* and `chant.*` keys on its resource. hud and behold join a span to a member,
|
|
8
|
+
* a release and a graph node by them. A collector between the workload and the
|
|
9
|
+
* backend can undo that, and these are the processors that can:
|
|
10
|
+
*
|
|
11
|
+
* - `resource`: a `delete`, `update`, `upsert` or `hash` action on a protected
|
|
12
|
+
* `key`, or a `delete` or `hash` whose `pattern` matches one. `insert` only
|
|
13
|
+
* adds a missing key. `extract` writes the pattern's named groups, and an RE2
|
|
14
|
+
* group name has no dot, so it can't name a protected key.
|
|
15
|
+
* - `transform`: an OTTL statement on the resource's attributes that calls
|
|
16
|
+
* `set`, `delete_key` or `delete_matching_keys` on a protected key, or
|
|
17
|
+
* `keep_keys` or `keep_matching_keys` that leaves one out. This is a text
|
|
18
|
+
* match over the statement, not a parse.
|
|
19
|
+
* - `resourcedetection` with `override` on (its default at contrib v0.130.0,
|
|
20
|
+
* `factory.go` `createDefaultConfig`) and a detector that can write a
|
|
21
|
+
* protected key: `env` (the collector's own `OTEL_RESOURCE_ATTRIBUTES`, and
|
|
22
|
+
* the default detector list), `dynatrace` (any key in its properties file),
|
|
23
|
+
* `heroku` (`service.name`, `service.version`) and `elastic_beanstalk`
|
|
24
|
+
* (`service.version`), per each detector's `metadata.yaml` at that tag.
|
|
25
|
+
* - `groupbyattrs`: a protected key in `keys`. A record attribute with that
|
|
26
|
+
* key is copied over the resource's (`attribute_groups.go`
|
|
27
|
+
* `buildReferenceResource`).
|
|
28
|
+
* - `redaction`: it runs over resource attributes too. With `allow_all_keys`
|
|
29
|
+
* off, its default, every key not in `allowed_keys` or `ignored_keys` is
|
|
30
|
+
* deleted; a `blocked_key_patterns` entry matching a protected key that isn't
|
|
31
|
+
* ignored masks its value. `blocked_values` matches values, which a config
|
|
32
|
+
* doesn't show, so it is not checked.
|
|
33
|
+
* - `sumologic`: `translate_attributes`, on by default, renames `service.name`
|
|
34
|
+
* to `service` on log and metric resources; `nest_attributes` (off by
|
|
35
|
+
* default) turns dotted keys into maps; an `aggregate_attributes` prefix
|
|
36
|
+
* moves the keys it matches.
|
|
37
|
+
* - `schema`: a target in the `https://opentelemetry.io/schemas/` family below
|
|
38
|
+
* 1.29.0 renames `vcs.ref.head.revision` back to
|
|
39
|
+
* `vcs.repository.ref.revision`, and below 1.27.0 renames
|
|
40
|
+
* `deployment.environment.name` back to `deployment.environment`, for data
|
|
41
|
+
* with a newer schema URL.
|
|
42
|
+
* - `metricstransform`: a `group` transform whose `group_resource_labels`
|
|
43
|
+
* sets a protected key.
|
|
44
|
+
* - `logstransform`: an operator that adds, removes, moves or copies onto a
|
|
45
|
+
* `resource["<key>"]` field for a protected key, a `retain` that lists
|
|
46
|
+
* resource fields and leaves one out, or a parser with `parse_to: resource`.
|
|
47
|
+
*
|
|
48
|
+
* `k8sattributes` can extract `service.name` and `service.version`, but it
|
|
49
|
+
* writes pod metadata only where the key is absent or empty
|
|
50
|
+
* (`processor.go` `setResourceAttribute`), so it never replaces the
|
|
51
|
+
* workload's value. The `attributes` processor acts on span, log and metric
|
|
52
|
+
* attributes, not the resource's. Neither is checked. Only processors some
|
|
53
|
+
* pipeline lists are.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import { TELEMETRY_ATTRIBUTES } from "@intentius/chant/telemetry-attribution";
|
|
57
|
+
import { canonicalTypeOf, type CollectorConfig } from "./model";
|
|
58
|
+
import type { CollectorIssue } from "./validate-config";
|
|
59
|
+
|
|
60
|
+
/** The keys the attribution stamps. Any other `chant.*` key is protected too. */
|
|
61
|
+
export const PROTECTED_RESOURCE_ATTRIBUTES: readonly string[] = TELEMETRY_ATTRIBUTES.map((a) => a.key);
|
|
62
|
+
|
|
63
|
+
/** True for a key OTEL118 protects: one of `PROTECTED_RESOURCE_ATTRIBUTES`, or any `chant.*` key. */
|
|
64
|
+
export function isProtectedResourceAttribute(key: string): boolean {
|
|
65
|
+
return PROTECTED_RESOURCE_ATTRIBUTES.includes(key) || key.startsWith("chant.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The protected keys a regular expression matches. An RE2 pattern JavaScript can't compile matches none. */
|
|
69
|
+
function protectedMatching(pattern: string): string[] {
|
|
70
|
+
let re: RegExp;
|
|
71
|
+
try {
|
|
72
|
+
re = new RegExp(pattern);
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
return PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => re.test(k));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function quoteAll(keys: string[]): string {
|
|
80
|
+
return keys.map((k) => `"${k}"`).join(", ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const REPLACING_ACTIONS: Record<string, string> = {
|
|
84
|
+
delete: "removes",
|
|
85
|
+
update: "replaces",
|
|
86
|
+
upsert: "replaces",
|
|
87
|
+
hash: "replaces with its hash",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function resourceActionFindings(body: Record<string, unknown>): string[] {
|
|
91
|
+
const out: string[] = [];
|
|
92
|
+
const actions = Array.isArray(body.attributes) ? body.attributes : [];
|
|
93
|
+
for (const raw of actions) {
|
|
94
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
95
|
+
const { key, action, pattern } = raw as { key?: unknown; action?: unknown; pattern?: unknown };
|
|
96
|
+
const verb = typeof action === "string" ? REPLACING_ACTIONS[action] : undefined;
|
|
97
|
+
if (!verb) continue;
|
|
98
|
+
if (typeof key === "string" && isProtectedResourceAttribute(key)) {
|
|
99
|
+
out.push(`its ${action} action ${verb} "${key}"`);
|
|
100
|
+
} else if (typeof pattern === "string" && (action === "delete" || action === "hash")) {
|
|
101
|
+
const hit = protectedMatching(pattern);
|
|
102
|
+
if (hit.length > 0) out.push(`its ${action} action's pattern "${pattern}" matches ${quoteAll(hit)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── transform: a text match over OTTL ────────────────────────────────
|
|
109
|
+
|
|
110
|
+
const OTTL_CALL = /\b(set|delete_key|delete_matching_keys|keep_keys|keep_matching_keys)\s*\(/g;
|
|
111
|
+
|
|
112
|
+
/** The top-level arguments of the call whose `(` is at `open`, as written. */
|
|
113
|
+
function callArguments(text: string, open: number): string[] {
|
|
114
|
+
const args: string[] = [];
|
|
115
|
+
let depth = 0;
|
|
116
|
+
let start = open + 1;
|
|
117
|
+
let quoted = false;
|
|
118
|
+
for (let i = open; i < text.length; i++) {
|
|
119
|
+
const c = text[i];
|
|
120
|
+
if (quoted) {
|
|
121
|
+
if (c === "\\") i++;
|
|
122
|
+
else if (c === '"') quoted = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (c === '"') quoted = true;
|
|
126
|
+
else if (c === "(" || c === "[" || c === "{") depth++;
|
|
127
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
128
|
+
depth--;
|
|
129
|
+
if (depth === 0) {
|
|
130
|
+
args.push(text.slice(start, i).trim());
|
|
131
|
+
return args;
|
|
132
|
+
}
|
|
133
|
+
} else if (c === "," && depth === 1) {
|
|
134
|
+
args.push(text.slice(start, i).trim());
|
|
135
|
+
start = i + 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return args;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The OTTL string literals in `text`, unescaped. */
|
|
142
|
+
function stringLiterals(text: string): string[] {
|
|
143
|
+
return [...text.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1].replace(/\\(["\\])/g, "$1"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Whether `path` is the resource's attribute map, and the key it indexes.
|
|
148
|
+
* `resource.attributes` always is; a bare `attributes` is only in a statement
|
|
149
|
+
* group whose context is `resource`.
|
|
150
|
+
*/
|
|
151
|
+
function resourcePath(path: string, resourceContext: boolean): { key?: string } | undefined {
|
|
152
|
+
const m = /^(resource\.)?attributes\s*(?:\[\s*"((?:[^"\\]|\\.)*)"\s*\])?$/.exec(path);
|
|
153
|
+
if (!m || (!m[1] && !resourceContext)) return undefined;
|
|
154
|
+
return m[2] === undefined ? {} : { key: m[2].replace(/\\(["\\])/g, "$1") };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function statementFindings(statement: string, resourceContext: boolean): string[] {
|
|
158
|
+
const out: string[] = [];
|
|
159
|
+
for (const call of statement.matchAll(OTTL_CALL)) {
|
|
160
|
+
const fn = call[1];
|
|
161
|
+
const args = callArguments(statement, call.index! + call[0].length - 1);
|
|
162
|
+
const target = resourcePath(args[0] ?? "", resourceContext);
|
|
163
|
+
if (!target) continue;
|
|
164
|
+
const [literal] = stringLiterals(args[1] ?? "");
|
|
165
|
+
if (fn === "set") {
|
|
166
|
+
if (target.key === undefined) out.push(`set replaces the whole resource attribute map`);
|
|
167
|
+
else if (isProtectedResourceAttribute(target.key)) out.push(`set replaces "${target.key}"`);
|
|
168
|
+
} else if (target.key !== undefined) {
|
|
169
|
+
continue;
|
|
170
|
+
} else if (fn === "delete_key" && literal !== undefined && isProtectedResourceAttribute(literal)) {
|
|
171
|
+
out.push(`delete_key removes "${literal}"`);
|
|
172
|
+
} else if (fn === "delete_matching_keys" && literal !== undefined) {
|
|
173
|
+
const hit = protectedMatching(literal);
|
|
174
|
+
if (hit.length > 0) out.push(`delete_matching_keys "${literal}" removes ${quoteAll(hit)}`);
|
|
175
|
+
} else if (fn === "keep_keys") {
|
|
176
|
+
const kept = new Set(stringLiterals(args[1] ?? ""));
|
|
177
|
+
const dropped = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !kept.has(k));
|
|
178
|
+
if (dropped.length > 0) out.push(`keep_keys drops ${quoteAll(dropped)}`);
|
|
179
|
+
} else if (fn === "keep_matching_keys" && literal !== undefined) {
|
|
180
|
+
const hit = new Set(protectedMatching(literal));
|
|
181
|
+
const dropped = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !hit.has(k));
|
|
182
|
+
if (dropped.length > 0) out.push(`keep_matching_keys "${literal}" drops ${quoteAll(dropped)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function transformFindings(body: Record<string, unknown>): string[] {
|
|
189
|
+
const out: string[] = [];
|
|
190
|
+
for (const [field, groups] of Object.entries(body)) {
|
|
191
|
+
if (!field.endsWith("_statements") || !Array.isArray(groups)) continue;
|
|
192
|
+
for (const group of groups) {
|
|
193
|
+
// A plain string infers its context from its paths, so only `resource.` paths reach the resource.
|
|
194
|
+
const [statements, resourceContext] =
|
|
195
|
+
typeof group === "string"
|
|
196
|
+
? [[group], false]
|
|
197
|
+
: typeof group === "object" && group !== null
|
|
198
|
+
? [(group as { statements?: unknown }).statements, (group as { context?: unknown }).context === "resource"]
|
|
199
|
+
: [[], false];
|
|
200
|
+
if (!Array.isArray(statements)) continue;
|
|
201
|
+
for (const statement of statements) {
|
|
202
|
+
if (typeof statement !== "string") continue;
|
|
203
|
+
for (const finding of statementFindings(statement, resourceContext)) {
|
|
204
|
+
out.push(`the OTTL text \`${statement}\` matches a call where ${finding}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── resourcedetection ────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/** Detectors that can write a protected key, and which. */
|
|
215
|
+
const OVERRIDING_DETECTORS: Record<string, string> = {
|
|
216
|
+
env: "any key in the collector's own OTEL_RESOURCE_ATTRIBUTES",
|
|
217
|
+
dynatrace: "any key in the Dynatrace properties file",
|
|
218
|
+
heroku: '"service.name" and "service.version"',
|
|
219
|
+
elastic_beanstalk: '"service.version"',
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function detectionFindings(body: Record<string, unknown>): string[] {
|
|
223
|
+
// `override` defaults to true, and `detectors` to [env], at contrib v0.130.0.
|
|
224
|
+
if (body.override === false) return [];
|
|
225
|
+
const detectors = Array.isArray(body.detectors) ? body.detectors : ["env"];
|
|
226
|
+
const how = body.override === true ? "override: true" : "override on by default";
|
|
227
|
+
return detectors
|
|
228
|
+
.filter((d): d is string => typeof d === "string" && d in OVERRIDING_DETECTORS)
|
|
229
|
+
.map((d) => `with ${how}, the ${d} detector writes ${OVERRIDING_DETECTORS[d]} over the service's own value; set override: false`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── processors that rewrite the resource as a side effect ─────────────
|
|
233
|
+
|
|
234
|
+
function strings(value: unknown): string[] {
|
|
235
|
+
return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function groupByAttrsFindings(body: Record<string, unknown>): string[] {
|
|
239
|
+
const keys = strings(body.keys).filter(isProtectedResourceAttribute);
|
|
240
|
+
return keys.length > 0 ? [`it groups by ${quoteAll(keys)}, so a record attribute with that key is copied over the resource's value`] : [];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function redactionFindings(body: Record<string, unknown>): string[] {
|
|
244
|
+
const out: string[] = [];
|
|
245
|
+
const ignored = new Set(strings(body.ignored_keys));
|
|
246
|
+
const allowed = new Set(strings(body.allowed_keys));
|
|
247
|
+
const exposed = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !ignored.has(k));
|
|
248
|
+
if (body.allow_all_keys !== true) {
|
|
249
|
+
const deleted = exposed.filter((k) => !allowed.has(k));
|
|
250
|
+
if (deleted.length > 0) {
|
|
251
|
+
const how = body.allow_all_keys === false ? "allow_all_keys: false" : "allow_all_keys off by default";
|
|
252
|
+
out.push(`with ${how}, it deletes resource keys missing from allowed_keys and ignored_keys: ${quoteAll(deleted)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const pattern of strings(body.blocked_key_patterns)) {
|
|
256
|
+
const masked = exposed.filter((k) => protectedMatching(pattern).includes(k));
|
|
257
|
+
if (masked.length > 0) out.push(`its blocked_key_patterns entry "${pattern}" masks the value of ${quoteAll(masked)}`);
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function sumologicFindings(body: Record<string, unknown>): string[] {
|
|
263
|
+
const out: string[] = [];
|
|
264
|
+
// `translate_attributes` defaults to true at contrib v0.130.0.
|
|
265
|
+
if (body.translate_attributes !== false) {
|
|
266
|
+
out.push(`translate_attributes${body.translate_attributes === true ? "" : " (on by default)"} renames "service.name" to "service" on log and metric resources`);
|
|
267
|
+
}
|
|
268
|
+
const nest = body.nest_attributes;
|
|
269
|
+
if (typeof nest === "object" && nest !== null && (nest as { enabled?: unknown }).enabled === true) {
|
|
270
|
+
const include = strings((nest as { include?: unknown }).include);
|
|
271
|
+
const exclude = strings((nest as { exclude?: unknown }).exclude);
|
|
272
|
+
const nested = PROTECTED_RESOURCE_ATTRIBUTES.filter(
|
|
273
|
+
(k) => (include.length === 0 || include.some((p) => k.startsWith(p))) && !exclude.some((p) => k.startsWith(p)),
|
|
274
|
+
);
|
|
275
|
+
if (nested.length > 0) out.push(`nest_attributes turns ${quoteAll(nested)} into nested maps`);
|
|
276
|
+
}
|
|
277
|
+
for (const pair of Array.isArray(body.aggregate_attributes) ? body.aggregate_attributes : []) {
|
|
278
|
+
const prefixes = strings((pair as { prefixes?: unknown } | null)?.prefixes);
|
|
279
|
+
const moved = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => prefixes.some((p) => k.startsWith(p)));
|
|
280
|
+
if (moved.length > 0) out.push(`aggregate_attributes prefixes ${quoteAll(prefixes)} move ${quoteAll(moved)} into one attribute`);
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Schema versions whose `all` changes rename a protected key, in semantic-conventions' schemas/1.37.0.
|
|
286
|
+
const SCHEMA_RENAMES: ReadonlyArray<{ version: [number, number, number]; from: string; to: string }> = [
|
|
287
|
+
{ version: [1, 29, 0], from: "vcs.repository.ref.revision", to: "vcs.ref.head.revision" },
|
|
288
|
+
{ version: [1, 27, 0], from: "deployment.environment", to: "deployment.environment.name" },
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
function schemaFindings(body: Record<string, unknown>): string[] {
|
|
292
|
+
const out: string[] = [];
|
|
293
|
+
for (const target of strings(body.targets)) {
|
|
294
|
+
const m = /^https?:\/\/opentelemetry\.io\/schemas\/(\d+)\.(\d+)\.(\d+)$/.exec(target);
|
|
295
|
+
if (!m) continue;
|
|
296
|
+
const v = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
297
|
+
const older = (r: [number, number, number]) => v[0] < r[0] || (v[0] === r[0] && (v[1] < r[1] || (v[1] === r[1] && v[2] < r[2])));
|
|
298
|
+
for (const rename of SCHEMA_RENAMES) {
|
|
299
|
+
if (older(rename.version)) out.push(`its target ${target} renames "${rename.to}" back to "${rename.from}" on data with a newer schema URL`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function metricsTransformFindings(body: Record<string, unknown>): string[] {
|
|
306
|
+
const out: string[] = [];
|
|
307
|
+
for (const t of Array.isArray(body.transforms) ? body.transforms : []) {
|
|
308
|
+
if (typeof t !== "object" || t === null || (t as { action?: unknown }).action !== "group") continue;
|
|
309
|
+
const labels = (t as { group_resource_labels?: unknown }).group_resource_labels;
|
|
310
|
+
const keys = typeof labels === "object" && labels !== null ? Object.keys(labels).filter(isProtectedResourceAttribute) : [];
|
|
311
|
+
if (keys.length > 0) out.push(`a group transform's group_resource_labels sets ${quoteAll(keys)} on the new resource`);
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The key of a stanza `resource["<key>"]` field, or undefined. */
|
|
317
|
+
function resourceField(field: unknown): string | undefined {
|
|
318
|
+
if (typeof field !== "string") return undefined;
|
|
319
|
+
const m = /^resource\[\s*["']([^"']*)["']\s*\]$/.exec(field.trim());
|
|
320
|
+
return m?.[1];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function logsTransformFindings(body: Record<string, unknown>): string[] {
|
|
324
|
+
const out: string[] = [];
|
|
325
|
+
for (const raw of Array.isArray(body.operators) ? body.operators : []) {
|
|
326
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
327
|
+
const op = raw as Record<string, unknown>;
|
|
328
|
+
const type = String(op.type ?? "");
|
|
329
|
+
const touched = [
|
|
330
|
+
...(type === "add" || type === "remove" ? [resourceField(op.field)] : []),
|
|
331
|
+
...(type === "move" ? [resourceField(op.from), resourceField(op.to)] : []),
|
|
332
|
+
...(type === "copy" ? [resourceField(op.to)] : []),
|
|
333
|
+
].filter((k): k is string => k !== undefined && isProtectedResourceAttribute(k));
|
|
334
|
+
if (touched.length > 0) out.push(`its ${type} operator writes or removes ${quoteAll(touched)}`);
|
|
335
|
+
if (type === "retain") {
|
|
336
|
+
const kept = strings(op.fields).map(resourceField).filter((k): k is string => k !== undefined);
|
|
337
|
+
const dropped = PROTECTED_RESOURCE_ATTRIBUTES.filter((k) => !kept.includes(k));
|
|
338
|
+
if (kept.length > 0 && dropped.length > 0) out.push(`its retain operator keeps only the resource fields it lists, dropping ${quoteAll(dropped)}`);
|
|
339
|
+
}
|
|
340
|
+
if (op.parse_to === "resource") out.push(`its ${type} operator parses into the resource, so a parsed key can replace any protected key`);
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const FINDINGS: Record<string, (body: Record<string, unknown>) => string[]> = {
|
|
346
|
+
resource: resourceActionFindings,
|
|
347
|
+
transform: transformFindings,
|
|
348
|
+
resourcedetection: detectionFindings,
|
|
349
|
+
groupbyattrs: groupByAttrsFindings,
|
|
350
|
+
redaction: redactionFindings,
|
|
351
|
+
sumologic: sumologicFindings,
|
|
352
|
+
schema: schemaFindings,
|
|
353
|
+
metricstransform: metricsTransformFindings,
|
|
354
|
+
logstransform: logsTransformFindings,
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* OTEL118 over one collector config: each processor a pipeline lists that can
|
|
359
|
+
* remove or replace a protected resource attribute. Not part of
|
|
360
|
+
* `validateCollectorConfig`, because it applies only to a build that stamps
|
|
361
|
+
* the attribution: the post-synth check runs it when the context carries
|
|
362
|
+
* `telemetry`.
|
|
363
|
+
*/
|
|
364
|
+
export function attributionIssues(config: CollectorConfig): CollectorIssue[] {
|
|
365
|
+
const pipelinesOf = new Map<string, string[]>();
|
|
366
|
+
for (const [pipeline, body] of Object.entries(config.service?.pipelines ?? {})) {
|
|
367
|
+
for (const id of body?.processors ?? []) pipelinesOf.set(String(id), [...(pipelinesOf.get(String(id)) ?? []), pipeline]);
|
|
368
|
+
}
|
|
369
|
+
const issues: CollectorIssue[] = [];
|
|
370
|
+
for (const [id, pipelines] of pipelinesOf) {
|
|
371
|
+
const raw = config.processors?.[id];
|
|
372
|
+
const body = typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
|
|
373
|
+
const type = canonicalTypeOf("processor", id);
|
|
374
|
+
const findings = Object.prototype.hasOwnProperty.call(FINDINGS, type) ? FINDINGS[type](body) : [];
|
|
375
|
+
for (const finding of findings) {
|
|
376
|
+
issues.push({
|
|
377
|
+
code: "OTEL118",
|
|
378
|
+
severity: "warning",
|
|
379
|
+
component: id,
|
|
380
|
+
message: `processor "${id}" (pipelines ${pipelines.join(", ")}): ${finding}. Chant's telemetry attribution stamps those resource attributes, and a span that loses them can't be joined to its member, release or declaration`,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return issues;
|
|
385
|
+
}
|
package/src/codegen/docs.ts
CHANGED
|
@@ -55,7 +55,14 @@ connector id shared with a receiver or exporter (OTEL114), a \`routing\` route
|
|
|
55
55
|
to a pipeline that doesn't receive from it (OTEL115), a connector
|
|
56
56
|
splitting metrics by a per-request GenAI attribute such as
|
|
57
57
|
\`gen_ai.conversation.id\` (OTEL116), two started components listening
|
|
58
|
-
on the same address (OTEL117), and a literal credential in source (OTEL002)
|
|
58
|
+
on the same address (OTEL117), and a literal credential in source (OTEL002)
|
|
59
|
+
or in the config (OTEL120). Others report deprecated fields (OTEL119),
|
|
60
|
+
credentials sent in plaintext (OTEL121), zpages or pprof off loopback
|
|
61
|
+
(OTEL122), detailed debug output beside a backend (OTEL123), delivery and
|
|
62
|
+
batching settings (OTEL124, OTEL125), and k8sattributes fields or
|
|
63
|
+
resourcedetection detectors the collector doesn't know (OTEL126, OTEL127). In
|
|
64
|
+
a build that stamps telemetry attribution, OTEL118 reports a processor that can
|
|
65
|
+
drop or overwrite one of the attribution's resource attributes.
|
|
59
66
|
`;
|
|
60
67
|
|
|
61
68
|
const outputFormat = `The otel lexicon serializes every otel entity in a build into **one
|
|
@@ -5,7 +5,7 @@ import { otelSerializer } from "../serializer";
|
|
|
5
5
|
import { collectorYaml } from "../collector";
|
|
6
6
|
import { collectorTopology } from "../topology";
|
|
7
7
|
import { validateCollectorEntities } from "../validate-config";
|
|
8
|
-
import { COLLECTOR_PIN } from "../define";
|
|
8
|
+
import { COLLECTOR_PIN, SEMCONV_PIN } from "../define";
|
|
9
9
|
import { DebugExporter } from "./exporters";
|
|
10
10
|
import { K8sClusterReceiver, KubeletStatsReceiver } from "./k8s-receivers";
|
|
11
11
|
import { Pipeline } from "../pipeline";
|
|
@@ -43,7 +43,10 @@ describe("k8s_cluster receiver", () => {
|
|
|
43
43
|
metrics: { "k8s.pod.status_reason": { enabled: true } },
|
|
44
44
|
});
|
|
45
45
|
const out = primary(otelSerializer.serialize(new Map([["cluster", cluster as unknown as Declarable]])));
|
|
46
|
-
|
|
46
|
+
// The `k8s.pod.status_reason` metric name is a k8s key, so the header names the pin.
|
|
47
|
+
expect(out).toBe(`# chant: semconv k8s ${SEMCONV_PIN.source}@${SEMCONV_PIN.version} (k8s_cluster)
|
|
48
|
+
|
|
49
|
+
receivers:
|
|
47
50
|
k8s_cluster:
|
|
48
51
|
auth_type: serviceAccount
|
|
49
52
|
collection_interval: 30s
|
|
@@ -24,6 +24,7 @@ export interface NumericAttributePolicyConfig {
|
|
|
24
24
|
key: string;
|
|
25
25
|
min_value?: number;
|
|
26
26
|
max_value?: number;
|
|
27
|
+
/** @deprecated Inverted decisions are deprecated since collector-contrib v0.126.0; use a `drop` policy (OTEL119). */
|
|
27
28
|
invert_match?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -46,6 +47,7 @@ export interface StringAttributePolicyConfig {
|
|
|
46
47
|
values: string[];
|
|
47
48
|
enabled_regex_matching?: boolean;
|
|
48
49
|
cache_max_size?: number;
|
|
50
|
+
/** @deprecated Inverted decisions are deprecated since collector-contrib v0.126.0; use a `drop` policy (OTEL119). */
|
|
49
51
|
invert_match?: boolean;
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -70,6 +72,7 @@ export interface TraceStatePolicyConfig {
|
|
|
70
72
|
export interface BooleanAttributePolicyConfig {
|
|
71
73
|
key: string;
|
|
72
74
|
value: boolean;
|
|
75
|
+
/** @deprecated Inverted decisions are deprecated since collector-contrib v0.126.0; use a `drop` policy (OTEL119). */
|
|
73
76
|
invert_match?: boolean;
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -10,6 +10,8 @@ import { OtlpExporter, PrometheusExporter } from "../components/exporters";
|
|
|
10
10
|
import { collectorYaml } from "../collector";
|
|
11
11
|
import { validateCollectorConfig } from "../validate-config";
|
|
12
12
|
import type { CollectorConfig } from "../model";
|
|
13
|
+
import { SEMCONV_PIN } from "../define";
|
|
14
|
+
import { semconvUsage } from "../semconv";
|
|
13
15
|
|
|
14
16
|
const gateway = new OtlpExporter({ name: "gateway", endpoint: "otel-gateway.observability.svc:4317", tls: { insecure: true } });
|
|
15
17
|
const base: NodeAgentProps = { exporters: [gateway] };
|
|
@@ -136,3 +138,17 @@ describe("NodeAgent options", () => {
|
|
|
136
138
|
expect((config.receivers?.filelog as Loose).exclude).toEqual(["/var/log/pods/*/agent/*.log"]);
|
|
137
139
|
});
|
|
138
140
|
});
|
|
141
|
+
|
|
142
|
+
describe("NodeAgent semconv", () => {
|
|
143
|
+
test("the YAML names the k8s semantic conventions its keys follow", () => {
|
|
144
|
+
const yaml = collectorYaml(Object.values(NodeAgent({ ...base, clusterName: "prod-eu-1" }).members) as Declarable[]);
|
|
145
|
+
expect(yaml.split("\n")[0]).toBe(`# chant: semconv k8s ${SEMCONV_PIN.source}@${SEMCONV_PIN.version} (k8sattributes, resource)`);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("k8s_cluster and k8sattributes as names are not k8s keys", () => {
|
|
149
|
+
expect(semconvUsage({ receivers: { k8s_cluster: {} }, processors: { k8sattributes: {}, "resource/k8s": {} } })).toEqual([]);
|
|
150
|
+
expect(semconvUsage({ processors: { "transform/x": { statements: ['delete_matching_keys(resource.attributes, "^k8s\\\\.pod\\\\..*")'] } } })).toEqual([
|
|
151
|
+
{ namespace: "k8s", ...SEMCONV_PIN, components: ["transform/x"] },
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
});
|