picatz / picatz/flowstate

Telemetry declared in the schema: attributes, events and errors generated rather than typed

Open
#537 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

observability
Dominant language
Go
Stars
9
Forks
0
Avg merge
3h 3m
Merged PRs (30d)
509

Description

Raised by the owner, 2026-08-13. Workstream of #522, and the mechanism that would make its first invariant enforceable instead of aspirational.

The problem this solves

#522's invariant 1 says a run id, workflow name, step id, tenant and trigger must be spelled the same way in a span attribute, a log field and a metric label. Today that is enforced by convention, which is exactly the failure mode this repository keeps finding: a rule applied in one place and not in its sibling. Five instances in one week, including two walks missing Node.Undo and two missing wf.Triggers.

Telemetry multiplies the siblings. Every attribute key exists in the recording call, in whatever documentation describes it, in a dashboard panel that queries it (#525), and in the cardinality allowlist that decides whether it is safe to use as a label (#526). Four copies of one fact, none of which the compiler compares.

The proposal

Declare telemetry in the schema and generate the rest, the way this repo already does for validation and documentation.

The precedents are all local and already load-bearing:

  • protovalidate puts validation rules in proto options, including CEL expressions, and flow docs generate reads those same rules to derive a task's field names, types and required-ness. The schema is already a documentation source, not only a code one.
  • protodoc carries the schema's own comments in a checked-in descriptor set, because protoc strips SourceCodeInfo from generated Go.
  • The plugin boundary reconstructs a plugin's descriptors so its tasks are validated, completed and documented exactly like built-in ones.

So a custom option declaring an attribute, its type, its stability, and its cardinality class would let one descriptor generate: the Go constants used at recording sites, the allowlist that refuses a peer-controlled value as a metric label, the reference documentation, and the registry a dashboard test asserts against. A renamed attribute becomes a compile error and a failed dashboard test rather than a silently empty panel.

The OpenTelemetry conventions to adopt while doing it

The owner named these, and they are the right sources rather than inventing house style:

  • Naming (https://opentelemetry.io/docs/specs/semconv/general/naming/): namespaced, dotted, no redundant prefixes. Worth auditing what we emit today against it. A rename is a breaking change for anyone with a dashboard, so audit now, migrate once, deliberately.
  • Attributes (https://opentelemetry.io/docs/specs/semconv/general/attributes/): OTel maintains its own attribute registry, which is the same idea this proposal applies to our domain. Prefer an existing convention attribute over inventing one wherever the meaning matches.
  • Recording errors (https://opentelemetry.io/docs/specs/semconv/general/recording-errors/): directly relevant to the leak fixed in #531, where the answer was to record a classification rather than the raw error. Check our classification against error.type and adopt the convention if it fits, since an operator's tooling already understands it.
  • Events (https://opentelemetry.io/docs/specs/semconv/general/events/): the signal we are not using at all, and the one with the clearest fit. This system has genuine domain events, a run started, a step retried, a compensation ran, a trigger fired and was refused, a schedule caught up. Those are currently either log lines with ad hoc text or nothing. Named, structured events with a declared payload are what makes them queryable, and a proto message per event is the obvious declaration.
  • Session (https://opentelemetry.io/docs/specs/semconv/general/session/): probably not applicable, since it models a client-side user session. Say so explicitly after checking rather than leaving it unexamined.

Events deserve their own thought

Of the four signals, events are the one where a proto-first approach is most natural, because an event has a payload and a payload is exactly what a schema describes. RunStarted, StepFailed, CompensationRan, DeliveryRefused as messages would give a stable name, a typed body, generated documentation, and a natural place to state which fields are safe to emit.

It also raises the containment question immediately, which is a feature: an event payload is a place a secret could ride out, so declaring the payload in the schema means the redaction rules can be attached to the same fields rather than remembered.

Where a small expression language might earn its place

protovalidate demonstrates CEL rules living in proto options and being enforced. The analogous idea here would be a rule attached to an attribute, for example that its cardinality is bounded, or that a value must be redacted before emission. Worth evaluating, but only after the plain declarative version works: a generator that emits constants and an allowlist is valuable on its own, and expressions are the kind of thing that is easy to add and impossible to remove.

What to be careful about

  • This is schema surface. Options in the proto are covered by buf breaking and are read by plugins. An attribute declaration is a public contract, not configuration.
  • Do not rename anything in the same change that introduces the mechanism. Land the generator, then migrate names deliberately, so the diff that changes what an operator's dashboards query is small and reviewable on its own.
  • The generator must be honest about drift, the way flow docs generate already is: generate, then git diff --exit-code, including a check for untracked output, which is the trap #487 fixed for the other generators.
  • Prove the property, not the plumbing. The deliverable is that a new attribute cannot be recorded without being declared, and a declared attribute cannot silently disappear from a dashboard. A generator that produces constants nobody is forced to use has not bought the invariant.

Related

  • #522 observability umbrella, invariant 1
  • #525 dashboard verification against an attribute registry, which this would supply
  • #526 metrics cardinality allowlist, which this would generate rather than have someone maintain
  • #531 the error-recording leak, and the convention question it raises
  • #535 composable secrets, since a declared event payload is a place redaction rules can attach

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading #522, #525, #526, #531 and the local precedents named in the issue: protovalidate, protodoc, the plugin descriptor boundary, and flow docs generate. Establish how proto options, checked-in descriptors, generated outputs, buf breaking, and git diff --exit-code are currently used. Done means a scoped design and implementation path can enforce declared telemetry across recording, cardinality, documentation, and dashboard verification without renaming existing attributes.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
build-system, observability
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.