Requirement file format
A Requirement is one named, versioned rule with required remediation text. It can assert on Effective state, on Observed state, or on both. It can instead assert schema conformance, which is judged against the Schema Registry alone.
Requirements live in a library directory: a flat directory of *.yaml or
*.yml files, one concern per file, so a change to one Requirement is a
one-file diff in review. Each file holds one Requirement or a list of them.
telecraft check -library and telecraft snapshot -library name that
directory. Subdirectories are not read.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | yes | none | Unique across the library. |
title |
string | no | empty | Short human-readable name. |
description |
string | no | empty | What the Requirement asks for, and why. |
version |
integer | yes | none | 1 or higher. Raise it when you tighten the Requirement, so the change is dated and visible. |
requirement_level |
string | no | recommended |
One of required, conditionally_required, recommended, opt_in. |
owner |
string | yes | none | The accountable party. An Exemption from this Requirement is valid only with this owner's review. |
environments |
list of strings | no | empty | Narrows where the Requirement applies. Empty applies everywhere. |
config |
mapping | no | absent | The Effective-state assertion. |
signal |
mapping | no | absent | The Observed-state assertion. |
schema_conformance |
mapping | no | absent | The Schema Registry reference. See Schema conformance assertions. |
placement |
string | no | landed |
Which reading a schema assertion is judged against. Only on a schema_conformance Requirement. |
remediation |
string | yes | none | The concrete change that closes the gap. |
At least one of config, signal and schema_conformance must be present.
schema_conformance doesn't combine with the other two: see
Schema conformance assertions.
# requirements/delivery.yaml
- id: logs-delivered
title: Logs are collected and delivered
description: >
The Service's logs reach a backend. Collection may be by file tail or by
OTLP: how the logs are gathered is the owner's choice, that they arrive
is not.
version: 1
requirement_level: required
owner: platform-observability
config:
has_receiver: [filelog, otlp]
signal:
kind: logs
present: true
window: 24h
min_volume: 1
remediation: >
Add a filelog or otlp receiver to the collector serving this Service and
wire it into a logs pipeline.
Requirement levels
requirement_level uses the four-level vocabulary of the OpenTelemetry
semantic conventions, unchanged. It's richer than a binary required list:
recommended is the natural level for attribute coverage that isn't yet
stable upstream.
| Value | Meaning |
|---|---|
required |
The assertion must hold. |
conditionally_required |
The assertion must hold where its condition applies. |
recommended |
The default when requirement_level is absent. |
opt_in |
The assertion holds only where you opt in. |
A value outside these four is a load error.
Environments
environments narrows a Requirement to the named Environments. An absent or
empty list applies everywhere. Write one environment-neutral assertion with a
narrowing list, rather than one file per Environment.
You define the Environment vocabulary, so the loader has no authority over which environments exist. A name that matches nothing the estate knows is not a load error. It surfaces as an authoring finding instead.
An empty string in the list, or a name listed twice, is a load error.
Assertion kinds
The kind of a Requirement follows from the assertions present. You don't write it, so it can never disagree with them.
| Kind | Present |
|---|---|
config |
config only. |
signal |
signal only. |
config_and_signal |
Both. |
schema_conformance |
schema_conformance, and neither of the other two. |
Asserting on both readings lets the evaluator cross them. A config-only Requirement can pass for a collector that delivers nothing, and a signal-only one can fail without naming a cause.
Config assertions
config evaluates against the Effective reading: the running config the
collector reports. Each list is satisfied if any of its entries is present:
"collect logs somehow" is the real requirement, and filelog versus otlp
is an implementation detail.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
has_receiver |
list of strings | no | empty | Receiver types, any of which satisfies the list. |
has_processor |
list of strings | no | empty | Processor types, any of which satisfies the list. |
has_exporter |
list of strings | no | empty | Exporter types, any of which satisfies the list. |
A config block with all three lists empty is a load error.
Matching is on the type prefix, so a component the collector reports as
otlp/onramp satisfies an assertion asking for otlp. Config assertions
carry no pipeline scope, so an assertion matches any pipeline.
Signal assertions
signal evaluates against the Observed reading: telemetry that landed in a
backend over a window. Every field is expressed in terms of signal presence,
volume, and attribute coverage. A Requirement can't carry a backend query,
so every Requirement works against every backend.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
kind |
string | yes | none | One of logs, metrics, traces. |
present |
boolean | no | false |
Whether the signal must be present. |
window |
duration string | yes | none | The trailing window, such as 24h or 90m. Must be positive. |
min_volume |
integer | no | 0 |
Minimum record count in the window. Must not be negative. |
required_attributes |
list of strings | no | empty | Attribute names that must appear on essentially every record. |
attribute_coverage |
number | no | 1.0 |
The fraction of records that must carry each required attribute, within (0, 1]. |
profiles isn't a signal kind here: the signal is alpha upstream, so the
evaluator can't judge it reliably. Blueprint lanes do carry a profiles
lane, because a Blueprint can route a signal the evaluator can't yet judge.
min_volume catches a pipeline that's alive but delivering almost nothing,
which a presence check alone would call healthy. attribute_coverage tells a
partially instrumented estate from an uninstrumented one. The default
requires total coverage unless you relax it.
Schema conformance assertions
schema_conformance asks whether the telemetry that arrived is the shape the
Schema Registry says it should be. It's a reference into a registry version,
never a copy of one: you name a version and a scope within it, and the
registry says which attributes that scope demands, at which requirement level,
with which types and which enum members.
There's no attribute list here, and adding one is a load error. A list in a requirement file is a second copy of something the registry already states, and the copy drifts the first time somebody edits one and not the other.
A Requirement of this kind runs against an estate. The evaluator takes three readings for each signal and window the reference covers, resolves what the pinned registry version demands of the scope, and judges one against the other:
| Reading | What it answers |
|---|---|
| Attribute names | Which attribute names the records carried, which is what a demand for an attribute is judged against. |
| Grouping-key values | Which spans, metrics or events arrived, which is what says whose required-set is in play. |
| Distinct values | What an attribute the registry declares as an enum actually carries. |
Where a reading can't be taken, or the reference resolved to no version, the
verdict is unknown with the cause: no reading, so no verdict, and never a
silent pass.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
registry_version |
string | yes, unless tracking | none | The Schema Registry version, by the ref it was imported at. |
track |
string | no | empty | head opts this reference into judging against whichever version is active. The only value is head. |
scope |
mapping | yes | none | What conformance is demanded of. |
scope.groups |
list of strings | no | empty | Registry group ids, such as span.db.client. |
scope.namespaces |
list of strings | no | empty | Attribute-name prefixes, such as db: every attribute under the prefix is demanded. |
signals |
list of strings | yes | none | The signals the scope is judged on: logs, metrics, traces. |
window |
duration string | yes | none | The trailing window, such as 24h. Must be positive. |
At least one of scope.groups and scope.namespaces must be present. An
empty scope would demand the whole registry of every Service by omission.
Whether the covered signals arrived at all is read over the same window. That
reading is what tells not_delivered from misconfigured. Nothing arrived is
not_delivered; telemetry arrived and is missing an attribute the registry
demands at required is misconfigured; every required attribute in use is
compliant.
# requirements/schema.yaml
- id: db-spans-conform
title: Database spans carry what the registry demands
version: 1
requirement_level: required
owner: platform-observability
environments: [production]
schema_conformance:
registry_version: v1.4.0
scope:
groups: [span.db.client]
signals: [traces]
window: 24h
remediation: >
Add the missing attributes to the database instrumentation, or upgrade
the instrumentation library to a version that sets them.
A schema_conformance Requirement doesn't also carry config or signal.
Collector config can't see instrumentation, so there's no Effective reading to
cross a schema verdict against, and no outcome for the combination. Two
Requirements say the two things honestly.
Enum values
Where the registry declares an attribute as an enum, the evaluator reads what
that attribute actually carries and judges it against the declared members. An
attribute carrying a value the registry never declared is a finding, at
whatever level the registry demands the attribute: a breach on a required
attribute is a violation and flips the outcome to misconfigured, and the
same breach on a recommended one is an improvement that rides alongside.
Without this, an attribute is judged on whether its name is there and never on
what it holds, so a wrong value reads as clean.
The remediation names both ways out, because the registry is yours: either stop emitting the value, or add it as a member. It also names the group the members are declared in, which isn't always the group that demanded the attribute.
The value reading is hard-capped and reports its own truncation, and the two directions of a clipped reading don't mean the same thing:
- A value the reading returned is in the telemetry. Truncation only says there may be more, so an undeclared value found in a clipped reading is still a breach.
- A reading that found nothing undeclared has proved nothing, because the
values it returned aren't the only ones the window holds. A clipped reading
with no breach in it is
unknown, nevercompliant.
That second rule is the one the attribute-name check doesn't have. A clipped name reading that found everything demanded is still a pass, because the records it didn't read can only add names. A clipped value set has its violations exactly where it stopped looking.
An enum attribute in use whose values nobody read is unknown too. Only
attributes the registry declares as enums are read this way, and only where
the name reading found them in use: an attribute nobody sets buys no round
trip, because whether it's missing is a question the presence check has
already answered.
Groups and required-sets
Semantic conventions state their required-sets per group, so what's demanded
depends on which groups arrived. The evaluator reads the grouping key each
signal is grouped by (span.name for traces, metric.name for metrics,
event.name for logs) and judges each arrived group's required-set, rather
than one flat union of attributes across the scope.
A group in scope that never arrived is not_delivered for that group. Its
required-set isn't in play, and nothing it alone demands is judged: the
attributes are absent because the telemetry is, not because the
instrumentation is wrong, so reporting them as missing would name a fix nobody
can make. An attribute another group in play also demands is still judged,
through that group. A group that arrived and is missing a required attribute
is misconfigured, as it always was. The two now read differently.
Presence in the grouping-key reading is proof a group arrived, because extra
records can only add group names. A group a truncated reading doesn't name
is unknown: a clipped reading can't tell a group that didn't arrive from one
it didn't sample, so the group is neither judged nor written off.
A group can only be located this way where the registry states which
grouping-key value its records carry. A metric group declares its
metric_name, so it can. A span group can't: the convention model declares
a span's kind and its attributes and says nothing about its name, which
semantic conventions state as prose over other attributes. An event group
can't either, for a different reason: upstream declares an event's name, and
the Schema Registry import doesn't carry it yet. A group that can't be located
keeps its demands in play and is judged against the scope's own reading, which
is what every group did before this check existed.
Findings
A schema-conformance finding belongs to the Service it was judged on, in the Environment it was judged in, and routes to that Service's owner. That's the party who can act on it. The fix is a change to the service's own instrumentation, so a Tier or a collector as the target would send the work to somebody who can't make it.
The finding writes its own remediation out of the registry rather than
repeating the line you authored. It names the group that demanded the
attribute, the attribute, the type the registry declares it at, and the level.
Where the registry carries a deprecation notice, the finding hands over
upstream's own migration note, including what the attribute was renamed to.
The remediation you author stays on the Requirement, and is what a finding
falls back to when it has nothing better to say.
Where a missing attribute sits on a resource or entity group, the
remediation also mentions collection-time enrichment: a processor such as
k8sattributes can add it when the service can't. That's a suggestion and
nothing more. The finding doesn't split in two and doesn't reroute, so the
Service's owner still owns it.
Pinned and tracking references
A reference pins a version by default. An adopter's registry tightening a requirement level is a change every Service's score feels, so it's adopted deliberately rather than overnight.
track: head is the opt-in for a scope that should follow the registry as it
moves, which is usually your own namespaced attributes rather than upstream's.
A reference either pins or tracks. Doing both is a load error, and doing
neither is too.
Resolving the reference
The reference is resolved when the library loads, against the Schema Registry versions the platform has imported. A pinned version that isn't installed is a load error, as is a group id or a namespace the pinned version carries nothing under. A reference that resolved to nothing would demand nothing, and a Requirement that demands nothing passes every Service silently, which is the one failure this loader exists to prevent.
A tracking reference names no version, so there's nothing to resolve a scope
against; what must exist is an imported registry for there to be a head at
all. Which installed version is the active one is an activation decision, and
nothing makes it yet, so a tracking reference loads and then reads unknown
at evaluation. Pin a version to get a verdict today.
Where the installed versions live is the command's to say. Each command that
loads a library takes a -schema-registries directory, holding one
schema-registry-<ref>.json artefact per imported version, which is what
schema-registry-import writes:
go run ./cmd/schema-registry-import -repo https://git.example/registry -ref v1.4.0
telecraft check -library requirements/ -estate estate.yaml \
-schema-registries schema-registries/
Schema Registry versions are instance-side artefacts, not estate content, the same as Catalogue versions and for the same reason: they're imported, retained version by version, and shared by every estate the instance judges. A library that references one and is loaded without the directory doesn't load at all, which is the fail-closed direction: the alternative is a Requirement that evaluates nothing and scores every Service clean.
Placement
placement says which reading a schema assertion is judged against.
| Value | Meaning |
|---|---|
landed |
Telemetry that has already landed in a backend. The default. |
live |
The findings the Live-check tap emitted at collection time. |
A live Requirement is judged against the findings a
weaver registry live-check service emitted over the sampled stream a
teeing Tier sends it, read back from your backend as ordinary log records.
The tap sees the stream before any backend does, which is what unlocks the
checks a landed reading can't make: value types, metric units and
instruments, and the naming checks. See
Tiers for the rendered pipeline that feeds the tap
and Serve configurations
for deploying the service.
The reference and the levels are judged exactly as at landed: a
violation-level finding on a required attribute reads misconfigured,
and improvement and information findings ride alongside. What differs is
what silence means. The tap emits findings, not heartbeats, so the
evaluation reads a liveness leg beside them: whether anything was sent to
the tap in the window, taken from the teeing Tier's own telemetry for the
tap exporter.
- The Requirement reads
compliantonly when the tap was fed in the window, nothing failed to send, and no violation arrived. - A tap that was fed nothing, a send failure, or a reading nobody could
take reads
unknown: it counts against the Service and never passes. - A
liveRequirement never readsnot_delivered. The tap samples one branch of the stream, so a Service it did not see readsunknown.
placement on a Requirement with no schema_conformance block is a load
error. Nothing else has a placement.
Load errors
The load fails closed and returns nothing. Every message names the file, and for field errors the field. The load refuses on:
- an unknown or misspelled field, anywhere in the document
- a malformed document, an empty file, or more than one YAML document in the file
- a file that is neither one mapping nor a list of mappings
- a library directory that doesn't exist, or holds no
*.yamlor*.ymlfiles at all: an empty library would judge everything compliant with nothing checked - a missing
id, or aniddefined in two files - a missing
owner, a missingremediation, or aversionbelow 1 - a
requirement_leveloutside the four values - no
config, nosignaland noschema_conformance, or aconfigblock with no entries - a
signalblock with an unknownkind, a non-positivewindow, a negativemin_volume, anattribute_coverageoutside(0, 1], or an empty required attribute name - a
schema_conformanceblock alongside aconfigorsignalblock - an
attributesorrequired_attributeslist inside aschema_conformanceblock: the registry says which attributes a scope carries, and a copy drifts - a
schema_conformanceblock that both pins aregistry_versionand setstrack: head, or that does neither, or that sets atrackother thanhead - a
schema_conformanceblock with an emptyscope, nosignals, an unknown signal kind, a signal listed twice, or a non-positivewindow - a
registry_versionthat isn't installed, or that doesn't load - a
scope.groupsentry the pinned registry version doesn't declare, or ascope.namespacesentry it carries no attribute under - a
track: headreference when no Schema Registry version is installed - a
schema_conformanceblock when the load was given no Schema Registry directory to resolve it against - a
placementoutsidelandedandlive, or aplacementon a Requirement with noschema_conformanceblock - an empty or duplicated entry in
environments
Authoring findings
An authoring finding means the library is valid and loads, but something you
wrote can never take effect. Authoring findings appear in the check
report's authoring_findings array and are never part of the exit code.
The library raises one kind: an environments entry that matches no
Environment the estate declares. If every entry is unknown, one finding says
the Requirement never applies. If some are known, one finding per unknown
entry says that entry never matches.
Exemptions raise authoring findings of their own into the same array.
Satisfies claims and versioning
version makes tightening a Requirement a dated, visible event rather than a
silent change in everyone's score.
A Blueprint claims Requirements through version-stamped satisfies entries of
the form <requirement-id>@<version>, where the version is the Requirement
version the claim was made against. See
Satisfies claims for the authoring rules.
A claim states intent. The evaluator always judges against the Requirement's
current version, so a claim can't freeze the rule at an older version. When
config passes the version it claims but fails the current one, the outcome is
library_drift: the Requirement moved and the config hasn't caught up. That's
distinct from never having complied. Its remediation is the version diff
rather than re-instrumenting, and the repository owns it rather than a row.
telecraft check -source -catalogue detects it and reports it in the
report's library_drift section.
Claims that pass at both the claimed and the current version, but are stamped
behind, are housekeeping nudges: visible in the report's housekeeping
array, never counted.
Outcomes
The eight outcomes, in severity order, worst first:
| Outcome | Meaning |
|---|---|
broken_pipeline |
Effective says yes, Observed says no. Someone configured this and it isn't working. |
not_configured |
Effective says no, Observed says no. The requirement is unmet, and the owner needs to instrument. |
not_delivered |
Observed says no, with no Effective evidence to explain why. |
misconfigured |
A config assertion failed with no signal reading to cross it against. |
library_drift |
The config passes the version it claims or pins while failing the current one. |
unknown |
No reading gave any evidence. Never silently a pass or a failure. |
ungoverned |
Observed says yes, Effective says no. The requirement is met, so this passes, but the report shows it. |
compliant |
The requirement is met. |
compliant and ungoverned are the passing outcomes. library_drift is the
one outcome the Effective by Observed cross never produces: it's judged from
the Intended reading, the config in git.