Skip to content

Latest commit

 

History

History
157 lines (117 loc) · 3.26 KB

File metadata and controls

157 lines (117 loc) · 3.26 KB

spancheck

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.

Flow

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
Loading

Package

Public import path:

import "github.com/iw4p/spancheck/tracing"

Usage

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.

Option 1: Embed YAML

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
}

Option 2: Read YAML from file

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

Runtime behavior

  • 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(...) and LoadRegistry(...)
  • Existing tracer.Start(...) callsites do not need wrappers or changes

Demo

Run the sample app:

go run ./demo

The demo embeds its own contract file and shows both valid spans and runtime warnings for contract violations.

Development

Run tests:

go test ./...