You validate:
- API inputs
- database schemas
- configs
But your traces?
👉 Nothing.
So in production you get:
- spans missing attributes
- inconsistent naming across services
- “valid-looking” traces that are actually wrong
And nobody notices.
The Hidden Problem with OpenTelemetry
OpenTelemetry gives you flexibility.
Too much flexibility.
You can write this:
ctx, span := tracer.Start(ctx, "payment")
And this somewhere else:
ctx, span := tracer.Start(ctx, "payment_authorize")
Or forget attributes:
span.SetAttributes() // missing critical fields
Nothing breaks.
But your observability?
👉 Silently degrades over time.
Meet spancheck
Instead of relying on discipline, you define a tracing contract.
And enforce it at runtime.
The Idea: Tracing Contracts
Define what “correct tracing” looks like — in YAML:
version: 1
attributes:
- payment.processor
- payment.id
- payment.job_name
spans:
payment.authorize:
kind: internal
required_attributes:
- payment.processor
- payment.id
payment.reconcile:
kind: consumer
required_attributes:
- payment.job_name
This becomes your source of truth.
No Code Changes Required
This is the key part.
You don’t touch your existing tracing calls.
Just plug into OpenTelemetry:
registry, _ := tracing.LoadRegistry(contractYAML)
processor := tracing.NewValidationProcessor(registry, logger)
tp := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(processor),
)
otel.SetTracerProvider(tp)
Your existing code stays exactly the same:
ctx, span := tracer.Start(ctx, "payment.authorize")
What Happens at Runtime
When spans finish, SpanCheck validates them.
You get warnings like:
- ⚠️ unknown span →
unknown_span - ⚠️ missing attribute →
missing_required_attribute
No crashes. No breaking changes.
But now:
👉 you see when your tracing is wrong
Why Runtime Validation > Linting
Static tools can guess.
Runtime sees reality.
| Static linting | Runtime validation |
|---|---|
| checks code | checks actual spans |
| misses dynamic paths | sees everything |
| requires patterns | zero assumptions |
| partial safety | full coverage |
Why This Matters
Bad traces don’t fail loudly.
They fail silently.
That means:
- dashboards look fine
- incidents take longer
- debugging becomes guesswork
You think you have observability.
You don’t.
What This Unlocks
Once you have contracts:
- consistent span naming across services
- guaranteed required attributes
- shared conventions across teams
- confidence in your traces
This is the missing layer in most observability setups.
Try It
go get github.com/iw4p/spancheck
Then plug it into your tracer and add a YAML contract.
That’s it.
Final Thought
We enforce:
- types
- schemas
- configs
But not traces?
Observability should be correct by default — not optional.
👉 Check the code here spancheck

Top comments (0)