Lint Rules
The otel lexicon’s rules use the OTEL prefix. OTEL0xx rules read your TypeScript source during chant lint. OTEL1xx checks run after a build: OTEL101 to OTEL106 and OTEL112 to OTEL127 read the emitted collector YAML (and any other output document shaped like a collector config), OTEL107 to OTEL109 read the declared entities. OTEL110 and OTEL111 are unused: the tail sampling placement checks they were reserved for read Kubernetes manifests, so they ship in the k8s lexicon as WK8601 to WK8603.
| Id | Severity | Catches |
|---|---|---|
| OTEL001 | error | a pipeline id string that isn’t type or type/name, or an instance name that is empty, has whitespace or starts with / |
| OTEL002 | error | a literal credential (authorization, api-key, api_key, token, password, secret, key_pem) in a component’s config |
| OTEL101 | error | a pipeline using a receiver, processor or exporter nobody declared, or a connector listed as an exporter but never as a receiver (or the reverse) |
| OTEL102 | error | a pipeline with no receivers, or no exporters |
| OTEL103 | warning | a declared component (connectors included) no pipeline uses, or an extension missing from service.extensions |
| OTEL104 | error | service.extensions naming an undeclared extension |
| OTEL105 | warning | memory_limiter somewhere other than first in a pipeline’s processors |
| OTEL106 | error | a pipeline id that isn’t traces, metrics or logs (optionally /name), or a reference that isn’t id syntax |
| OTEL107 | error | a component’s config breaking its definition’s own rules, see Components |
| OTEL108 | error | two components, or two pipelines, declaring the same id |
| OTEL109 | error | a custom component without a usable schema pin |
| OTEL112 | error | a connector joining a pipeline whose signal it can’t pair with any pipeline on the other side, e.g. spanmetrics as the receiver of a traces pipeline |
| OTEL113 | error | pipelines that feed each other in a cycle through connectors, see OTEL113 |
| OTEL114 | error | a connector id also declared as a receiver or exporter, see OTEL114 |
| OTEL115 | error | a routing connector route naming a pipeline that does not receive from it, see OTEL115 |
| OTEL116 | warning | a connector splitting metrics by a GenAI id (gen_ai.conversation.id, session.id and others) or a content key, see below |
| OTEL117 | error | two components the collector starts listening on the same address, so it exits with “address already in use”, see below |
| OTEL118 | warning | in a build that stamps telemetry attribution, a pipeline processor that can remove or replace service.name, service.version, deployment.environment.name, vcs.ref.head.revision or a chant.* resource attribute, see below |
| OTEL119 | warning | a field deprecated at or before the pinned release: invert_match, service.telemetry.metrics.address, spanmetrics dimensions_cache_size, see below |
| OTEL120 | error | a literal credential in the collector config, OTEL002’s check for emitted and imported YAML, see below |
| OTEL121 | warning | an exporter that sends a credential to an http:// endpoint or with tls.insecure: true |
| OTEL122 | warning | zpages or pprof listening on an address other than loopback, see below |
| OTEL123 | warning | a debug exporter at verbosity: detailed in a pipeline that also sends to another exporter, see below |
| OTEL124 | warning | an exporter with a remote endpoint and sending_queue.enabled: false or retry_on_failure.enabled: false, see below |
| OTEL125 | warning | a pipeline sending to a remote otlp or otlphttp exporter with no batch processor and no sending_queue.batch |
| OTEL126 | error | a k8sattributes extract.metadata field the processor doesn’t support, see below |
| OTEL127 | error | a resourcedetection detector the processor doesn’t have |
Configs in Kubernetes ConfigMaps
Section titled “Configs in Kubernetes ConfigMaps”OtelCollector, OtelCollectorGateway and GkeOtelCollector put the rendered config in a ConfigMap in the k8s output, under config.yaml. chant build hands each lexicon’s checks only that lexicon’s output, so the otel lexicon’s checks never see it. The k8s lexicon’s WK8604 runs the same config checks there, over every ConfigMap value that parses as a collector config, and reports each finding under its OTEL id with the ConfigMap’s namespace, name and key:
error: [otlp/tmepo] ConfigMap observability/otel-agent-config, key config.yaml: pipeline "logs" uses exporter "otlp/tmepo", which is not declared under exporters; the collector refuses to start (otel)A project that declares collectors only through the k8s composites needs only k8s in its lexicons. Adding otel as well also emits the components you export as a collector config of their own, with no pipelines, which OTEL103 then reports as unused. OTEL107 to OTEL109 read declared entities, so they don’t run for such a project.
OTEL002 and credentials
Section titled “OTEL002 and credentials”The collector expands ${env:NAME} and ${file:/path} when it loads its config, so a credential never needs to be in the declaration. Any value containing ${ is treated as such a reference, and keys ending in _file are paths, not secrets. The check follows a setting lifted into a named const in the same file, the shape COR001 asks for and chant import writes, and looks inside lists, such as a Prometheus scrape job’s basic_auth.
OTEL101, OTEL112 and connectors
Section titled “OTEL101, OTEL112 and connectors”A connector is an exporter in one pipeline and a receiver in another, and the collector refuses a connector listed on only one side (OTEL101). Each connector also supports fixed signal pairs, as its factory registers them:
| Connector | Pairs |
|---|---|
spanmetrics, servicegraph | traces to metrics |
count | traces, metrics, logs or profiles to metrics |
routing, forward | traces to traces, metrics to metrics, logs to logs |
OTEL112 applies the collector’s rule: every pipeline feeding a connector must pair, through a supported pair, with some pipeline the connector feeds, and every pipeline it feeds must pair with some pipeline feeding it. The message names the connector’s pairs. A custom connector is checked when its defineComponent call lists connects; without it, OTEL112 stays silent for that connector. Both rules read one collector config at a time, so pipelines split across build roots are not joined.
OTEL113: connector cycles
Section titled “OTEL113: connector cycles”A connector carries data from the pipeline that lists it in exporters to the pipeline that lists it in receivers. The collector builds one graph from those hops and refuses to start when the graph has a cycle. This config fails because traces/a feeds traces/b through forward/ab, and traces/b feeds traces/a back through forward/ba:
service: pipelines: traces/a: receivers: [otlp, forward/ba] exporters: [forward/ab] traces/b: receivers: [forward/ab] exporters: [forward/ba, debug]The message names one cycle hop by hop, traces/a -> forward/ab -> traces/b -> forward/ba -> traces/a, and starts at the first pipeline in the config. A pipeline that lists the same forward connector as both receiver and exporter is a cycle of one. OTEL113 walks the edges that collectorTopology() returns, so a hop counts only where the connector supports that signal pair: metrics back into traces through spanmetrics is no edge, and OTEL112 reports it instead. Several cycles through the same pipelines give one finding. Break the cycle by dropping a hop, or by sending that pipeline’s data to an exporter rather than back upstream.
OTEL114: connector id collisions
Section titled “OTEL114: connector id collisions”A pipeline names a connector the same way it names a receiver or an exporter, so a connector id must differ from every declared receiver and exporter id. The collector checks this before it looks at pipelines, so it refuses the config even when nothing uses the colliding receiver or exporter. Collisions need a type that exists both as a connector and as a receiver or exporter (datadog is one in collector-contrib) or a custom component, and the fix is a name: datadog/connector rather than datadog.
receivers: datadog: { endpoint: 127.0.0.1:8126 }connectors: datadog: {} # OTEL114: rename to datadog/connectorOTEL115: routing targets
Section titled “OTEL115: routing targets”The routing connector hands each item to the pipelines its matching route names, in table[].pipelines, or to default_pipelines when no route matches. It can only hand data to a pipeline it feeds, so each of those pipelines must list the connector in its receivers. The collector refuses to start when a route names any other pipeline, or one that service.pipelines does not declare.
connectors: routing: default_pipelines: [traces/other] # OTEL115: traces/other receives from otlp, not routing table: - statement: route() where attributes["tenant"] == "acme" pipelines: [traces/acme]service: pipelines: traces/in: { receivers: [otlp], exporters: [routing] } traces/acme: { receivers: [routing], exporters: [debug] } traces/other: { receivers: [otlp], exporters: [debug] }The message says where the target came from (table[0].pipelines or default_pipelines), and a pipeline named in several routes is reported once. Every connector of type routing is checked, named instances such as routing/tenants included.
OTEL116: high-cardinality GenAI attributes
Section titled “OTEL116: high-cardinality GenAI attributes”A metric gets one time series per distinct set of attribute values. Some GenAI attributes take a new value on every request or conversation, so a connector that splits a metric by one of them adds series as fast as traffic arrives, until the backend runs out of memory. The collector accepts such a config without a warning. OTEL116 reports these keys:
| Key | Defined in | Varies per |
|---|---|---|
gen_ai.conversation.id | semantic-conventions v1.41.1, model/gen-ai/registry.yaml | conversation |
gen_ai.response.id | v1.41.1, model/gen-ai/registry.yaml | response |
gen_ai.tool.call.id | v1.41.1, model/gen-ai/registry.yaml | tool call |
gen_ai.request.previous_response.id | semantic-conventions-genai, unreleased main | request |
gen_ai.memory.record.id | semantic-conventions-genai, unreleased main | memory record |
session.id | v1.41.1, model/session/registry.yaml | session |
user.id | v1.41.1, model/user/registry.yaml | user |
enduser.id | v1.41.1, model/enduser/registry.yaml | end user |
v1.41.1 is the lexicon’s GENAI_SEMCONV_PIN. None of these keys is an attribute of any GenAI metric the conventions define at that pin (model/gen-ai/metrics.yaml), which use the operation, provider, model, server address and port, error type and token type. The two keys from semantic-conventions-genai are listed because instrumentation already sets them, before that repository has a release to pin.
OTEL116 also reports the content keys genAiPipeline() deletes, GENAI_CONTENT_ATTRIBUTES (gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions, the tool call arguments and result, the retrieval query and documents, and the deprecated gen_ai.prompt and gen_ai.completion), and indexed keys such as gen_ai.prompt.0.content. A content value is unbounded, and putting it in a metric label also copies prompts and completions into the metrics backend.
The check reads these fields of each connector in a collector config:
| Connector | Fields |
|---|---|
spanmetrics | dimensions, calls_dimensions, histogram.dimensions, events.dimensions |
servicegraph | dimensions |
count, sum | each metric’s attributes |
signaltometrics | each metric’s attributes and include_resource_attributes |
An empty or unset include_resource_attributes keeps every resource attribute. Which ones those are depends on the SDK, not the config, so OTEL116 says nothing about it; spanmetrics keeps the resource attributes the same way. The keys live in GENAI_HIGH_CARDINALITY_ATTRIBUTES and GENAI_CONTENT_ATTRIBUTES, and genAiCardinalityRisk(key) tells you whether a key is one of them.
Keep these attributes on spans and logs, where a per-request value costs nothing extra, and split metrics by the bounded ones. If you have bounded one of these keys some other way, for example a transform processor that maps user ids to a few tiers before the connector, turn the rule off for the project with lint.rules: { OTEL116: "off" }. Post-synth checks have no per-key option. genAiPipeline()’s own output passes OTEL116 with every option.
OTEL117: listen address collisions
Section titled “OTEL117: listen address collisions”Each receiver and exporter a pipeline lists, each extension in service.extensions, and the collector’s own metrics endpoint bind an address when the collector starts. When two of them want the same one, the second bind fails with “address already in use” and the collector exits. otelcol validate builds the config without binding anything, so it passes. The usual case is a prometheus exporter on port 8888, which the collector already uses for its own metrics:
exporters: prometheus: endpoint: 0.0.0.0:8888 # the collector's own metrics are on localhost:8888Two addresses collide when the ports match and the hosts match, or either host is a wildcard (0.0.0.0, :: or empty). A UDP listener (the jaeger receiver’s thrift_compact and thrift_binary) only collides with another UDP listener. A host written as ${env:POD_IP} is compared as written.
OTEL117 knows these listeners and their defaults at the pinned collector release:
| Component | Address | Default |
|---|---|---|
otlp receiver | protocols.grpc.endpoint, protocols.http.endpoint | localhost:4317, localhost:4318, for a protocol that is listed |
zipkin receiver | endpoint | localhost:9411 |
jaeger receiver | protocols.grpc, thrift_http, thrift_binary, thrift_compact endpoints | localhost:14250, localhost:14268, localhost:6832 (UDP), localhost:6831 (UDP), for a protocol that is listed |
prometheus exporter | endpoint | none |
health_check, zpages, pprof extensions | endpoint | localhost:13133, localhost:55679, localhost:1777 |
| the collector’s own metrics | service.telemetry.metrics.readers[].pull.exporter.prometheus host and port | localhost:8888, none with level: none |
Other components are not checked, since an endpoint can be a server the component connects to rather than one it serves (the kubeletstats receiver’s is the kubelet). A component no pipeline lists, or an extension missing from service.extensions, never starts, so it can’t collide.
OTEL118: telemetry attribution keys
Section titled “OTEL118: telemetry attribution keys”A workload built inside a workspace, or in a project with telemetry.attribution: true, carries resource attributes that join its spans to a member, a release and a declaration: service.name, service.version, deployment.environment.name, vcs.ref.head.revision, and chant.workspace, chant.member and chant.decl (decision ws-060). A collector between the workload and the backend can undo that. OTEL118 reports each processor a pipeline lists that can remove or replace one of these keys, or any other chant.* key:
| Processor | Reported |
|---|---|
resource | a delete, update, upsert or hash action on a protected key, or a delete or hash whose pattern matches one |
transform | a statement on the resource’s attributes (resource.attributes, or attributes in a context: resource group) calling set or delete_key on a protected key, delete_matching_keys with a pattern that matches one, or keep_keys or keep_matching_keys that leaves one out |
resourcedetection | override on, which is the default at the pinned release, with the env, dynatrace, heroku or elastic_beanstalk detector. env is also the default detector list. |
groupbyattrs | a protected key in keys: a record attribute with that key is copied over the resource’s value |
redaction | allow_all_keys off, its default, with a protected key missing from allowed_keys and ignored_keys, which deletes it. Also a blocked_key_patterns entry that matches a protected key, which masks it. |
sumologic | translate_attributes, on by default, which renames service.name to service on log and metric resources. Also nest_attributes over a protected key, or an aggregate_attributes prefix that matches one. |
schema | a target in the https://opentelemetry.io/schemas/ family below 1.29.0, which renames vcs.ref.head.revision back to vcs.repository.ref.revision. Below 1.27.0 it also renames deployment.environment.name back to deployment.environment. |
metricstransform | a group transform whose group_resource_labels sets a protected key |
logstransform | an add, remove, move or copy operator on resource["<key>"] for a protected key, a retain that lists resource fields and leaves one out, or a parser with parse_to: resource |
insert only adds a key the resource lacks, so it passes. extract writes the pattern’s named groups, and a group name can’t contain a dot, so it passes too. The transform case matches the statement’s text rather than parsing OTTL, and the message says so. A redaction blocked_values pattern matches values, which the config doesn’t show, so it isn’t checked.
Two processors that look relevant are not reported. k8sattributes can extract service.name and service.version from pod labels, but it writes pod metadata only where the key is absent or empty, so it never replaces the workload’s value. NodeAgent therefore passes. The attributes processor changes span, log and metric attributes, not the resource’s.
The check runs only when the build stamps the attribution. A project outside a workspace without telemetry.attribution: true gets no OTEL118 findings. For resourcedetection, set override: false to keep the values the workload set.
OTEL119: deprecated fields
Section titled “OTEL119: deprecated fields”These fields still work at the pinned release, v0.130.0, and are deprecated there. A later release removes them, so a config that uses one breaks on the next pin bump.
| Field | Deprecated in | Use instead |
|---|---|---|
invert_match: true in a tail_sampling policy or sub-policy | collector-contrib v0.126.0 (#39833): “The invert decisions (InvertSampled and InvertNotSampled) have been deprecated” | a drop policy |
service.telemetry.metrics.address | collector v0.111.0 (#11205) | service.telemetry.metrics.readers |
dimensions_cache_size on spanmetrics | collector-contrib v0.125.0 (#39646), marked Deprecated [v0.130.0] in connector/spanmetricsconnector/config.go (#41101) | aggregation_cardinality_limit |
The versions come from each repository’s CHANGELOG.md at the v0.130.0 tag. invert_match: false is not reported, since it asks for no inverted decision. The invert_match fields of the lexicon’s policy types carry @deprecated. Renames and removals after the pin are left to the pin bump.
OTEL120 and OTEL121: credentials in the config
Section titled “OTEL120 and OTEL121: credentials in the config”OTEL002 reads TypeScript, so a config brought in with chant import, or one inside a ConfigMap, never reaches it. OTEL120 runs the same check over the collector config: the same key pattern, ${ values treated as references, _file keys treated as paths, and list items not inheriting their parent’s key. It reports every receiver, processor, exporter, extension and connector, with the key path, such as headers.authorization. A literal written in TypeScript is reported twice, once by each check.
OTEL121 looks at what an exporter sends, not how it is written. It reports a started exporter whose endpoint is not loopback, that sends a header matching the credential pattern or has a top-level credential key (api_key, token), and that sends in plaintext. Plaintext is an http:// endpoint, or tls.insecure: true on an endpoint that isn’t https://. An HTTP exporter takes TLS from the scheme, and a gRPC exporter from tls.insecure. An ${env:...} reference counts here, since the value still crosses the network. A credential an auth extension adds is not checked.
OTEL122: zpages and pprof
Section titled “OTEL122: zpages and pprof”zpages serves span samples and pipeline internals, and pprof serves heap and goroutine profiles and runs CPU profiles on request. Neither has authentication. OTEL122 reports either one, when service.extensions starts it, on any host but localhost, 127.0.0.0/8 or ::1. That includes 0.0.0.0, an empty host, and an ${env:POD_IP} host. The default addresses are loopback, so zpages: {} passes. Bind them to localhost and reach them with kubectl port-forward.
health_check is not reported, although OTEL117 knows its address too. It serves only a status, and a kubelet probe reaches it on the pod IP, so it has to listen there. otlpCollector(), genAiPipeline(), NodeAgent and the init templates bind it on 0.0.0.0:13133 for that reason, and so do the k8s OtelCollector and OtelCollectorGateway, whose default config is otlpCollector().
OTEL123: detailed debug output
Section titled “OTEL123: detailed debug output”verbosity: detailed makes the debug exporter write every span, metric point and log record to the collector’s log, with all attributes and bodies. In a pipeline that also sends to a backend, that copies the backend’s data to the log, and the log often goes to a different store with different access. OTEL123 reports this case only. A pipeline whose only exporters are debug is a test setup and passes, as does verbosity: basic or normal.
OTEL124 and OTEL125: delivery
Section titled “OTEL124 and OTEL125: delivery”Both look at exporters with a remote endpoint: an endpoint (or traces_endpoint, metrics_endpoint, logs_endpoint) whose host is not loopback. An ${env:...} endpoint counts as remote. The prometheus exporter serves rather than sends and is skipped.
OTEL124 reports sending_queue.enabled: false, which makes a slow backend block the pipeline, and retry_on_failure.enabled: false, which drops a failed request’s data.
OTEL125 reports a pipeline with no batch processor (any batch/name counts) that sends to a remote otlp or otlphttp exporter with no sending_queue.batch. At v0.130.0 those two exporters do not batch by default: NewDefaultQueueConfig in exporter/exporterhelper/internal/queue_sender.go sets no batch. Other exporters may batch on their own, so OTEL125 leaves them alone and covers only these two.
OTEL126 and OTEL127: names from a fixed list
Section titled “OTEL126 and OTEL127: names from a fixed list”The collector rejects an unknown name at start-up. These two checks catch it without a collector binary.
OTEL126 compares each k8sattributes extract.metadata entry with the fields Config.Validate accepts in processor/k8sattributesprocessor/config.go at collector-contrib v0.130.0. The list is K8S_ATTRIBUTES_METADATA: the k8s.namespace, k8s.pod, k8s.deployment, k8s.replicaset, k8s.daemonset, k8s.statefulset, k8s.job, k8s.cronjob, k8s.node and k8s.container names and ids, k8s.pod.hostname, k8s.pod.start_time, k8s.pod.ip, k8s.cluster.uid, container.id, container.image.name, container.image.tag, container.image.repo_digests, and service.namespace, service.name, service.version, service.instance.id. Any other name makes the collector refuse the config. Labels and annotations are extracted with extract.labels and extract.annotations.
OTEL127 compares each detector a started resourcedetection lists with the map NewFactory registers in processor/resourcedetectionprocessor/factory.go at v0.130.0, RESOURCE_DETECTORS: aks, azure, consul, docker, dynatrace, ec2, ecs, eks, elastic_beanstalk, env, gcp, heroku, k8snode, kubeadm, lambda, openshift and system. An unknown name fails the processor’s build with “invalid detector key” and the collector exits. The Elastic Beanstalk detector is spelled elastic_beanstalk, and the message says so for elasticbeanstalk. An ${env:...} entry is not checked.
Using the checks outside a build
Section titled “Using the checks outside a build”The same logic is exported as plain functions. validateCollectorConfig(config) returns OTEL101 to OTEL106, OTEL112 to OTEL117 and OTEL119 to OTEL127 findings for any parsed collector config, attributionIssues(config) returns OTEL118 findings, and validateCollectorEntities(entities) returns OTEL107 to OTEL109 for declared entities. The k8s lexicon’s GkeOtelCollector tests use the first to check the config the composite renders.