Reusable runtime OpenTelemetry tracing contract validation for Go.
spancheck lets an app define its tracing contract in YAML, load it at startup, and validate emitted spans at runtime through an OpenTelemetry SpanProcessor without changing existing tracer.Start(...) callsites.
flowchart TB
subgraph Setup
A["App-owned YAML contract"]
B["spancheck loads and validates contract"]
C["Registry + Validation Processor"]
D["TracerProvider configured"]
A --> B --> C --> D
end
subgraph Runtime
E["Existing app code<br/>tracer.Start(...)"]
F["Spans end"]
G["Processor validates span"]
H["Warnings logged"]
E --> F --> G --> H
end
Public import path:
import "github.com/iw4p/spancheck/tracing"The library accepts raw YAML bytes. That means apps can supply the contract with embed, os.ReadFile(...), or any other config source.
Embedded YAML is a good default when the contract is versioned with the app and deployed together with it.
package main
import (
_ "embed"
"log/slog"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"github.com/iw4p/spancheck/tracing"
)
//go:embed tracing-conventions.yml
var contractYAML []byte
func setupTracing(logger *slog.Logger, exporter sdktrace.SpanExporter) error {
registry, err := tracing.LoadRegistry(contractYAML)
if err != nil {
return err
}
processor := tracing.NewValidationProcessor(registry, logger)
tp := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(processor),
sdktrace.WithBatcher(exporter),
)
otel.SetTracerProvider(tp)
return nil
}package main
import (
"log/slog"
"os"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"github.com/iw4p/spancheck/tracing"
)
func setupTracing(logger *slog.Logger, exporter sdktrace.SpanExporter) error {
contractYAML, err := os.ReadFile("tracing-conventions.yml")
if err != nil {
return err
}
registry, err := tracing.LoadRegistry(contractYAML)
if err != nil {
return err
}
processor := tracing.NewValidationProcessor(registry, logger)
tp := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(processor),
sdktrace.WithBatcher(exporter),
)
otel.SetTracerProvider(tp)
return nil
}Example tracing-conventions.yml:
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- Unknown span names produce a warning with type
unknown_span - Missing required attributes produce a warning with type
missing_required_attribute - Startup contract errors are returned normally from
LoadContract(...)andLoadRegistry(...) - Existing
tracer.Start(...)callsites do not need wrappers or changes
Run the sample app:
go run ./demoThe demo embeds its own contract file and shows both valid spans and runtime warnings for contract violations.
Run tests:
go test ./...