Documentation
¶
Overview ¶
Package testing provides utilities for testing OpenTelemetry instrumentation in GoBricks applications.
This package offers in-memory exporters and helpers for asserting spans and metrics in unit tests without requiring external collectors or backends.
Usage:
// Install a test trace provider globally; the helper restores the previous // provider (and propagator) in t.Cleanup. Never Shutdown a provider you // installed: otel's DEFAULT delegating provider binds to the first one // installed in the binary and never rebinds (internal/global/state.go // sync.Once), so shutting yours down can silence otel.Tracer calls made // through that restored default provider afterwards. (While your provider // is the current global, calls reach it directly — the hazard is what // happens after you put the default back.) tp := InstallTestTraceProvider(t) // Run your code that creates spans tracer := tp.TestTracer() _, span := tracer.Start(context.Background(), "test-span") span.End() // Assert spans spans := tp.Exporter.GetSpans() require.Len(t, spans, 1) AssertSpanName(t, spans[0], "test-span")
Index ¶
- Constants
- func AssertExceptionTypeOnly(t TB, span *tracetest.SpanStub, wantType string)
- func AssertMetricCount(t *testing.T, rm metricdata.ResourceMetrics, expected int)
- func AssertMetricDescription(t *testing.T, rm metricdata.ResourceMetrics, metricName, expectedDesc string)
- func AssertMetricExists(t *testing.T, rm metricdata.ResourceMetrics, metricName string)
- func AssertMetricValue(t *testing.T, rm metricdata.ResourceMetrics, metricName string, ...)
- func AssertNoExceptionEvent(t TB, span *tracetest.SpanStub)
- func AssertNoSpanMarkers(t TB, span *tracetest.SpanStub, markers ...string)
- func AssertSpanAttribute(t *testing.T, span *tracetest.SpanStub, key string, expected any)
- func AssertSpanError(t *testing.T, span *tracetest.SpanStub, expectedDesc string)
- func AssertSpanName(t *testing.T, span *tracetest.SpanStub, expected string)
- func AssertSpanStatus(t *testing.T, span *tracetest.SpanStub, expectedCode codes.Code)
- func AssertSpanStatusDescription(t *testing.T, span *tracetest.SpanStub, expectedDesc string)
- func FindMetric(rm metricdata.ResourceMetrics, metricName string) *metricdata.Metrics
- func GetMetricHistogramCount(rm metricdata.ResourceMetrics, metricName string) (uint64, error)
- func GetMetricSumValue(rm metricdata.ResourceMetrics, metricName string) (any, error)
- func HistogramExemplars[N int64 | float64](t TB, rm metricdata.ResourceMetrics, metricName string) []metricdata.Exemplar[N]
- func SumExemplars[N int64 | float64](t TB, rm metricdata.ResourceMetrics, metricName string) []metricdata.Exemplar[N]
- type SpanCollector
- func (sc *SpanCollector) AssertCount(expected int) *SpanCollector
- func (sc *SpanCollector) AssertEmpty() *SpanCollector
- func (sc *SpanCollector) First() tracetest.SpanStub
- func (sc *SpanCollector) Get(index int) tracetest.SpanStub
- func (sc *SpanCollector) Len() int
- func (sc *SpanCollector) WithAttribute(key string, value any) *SpanCollector
- func (sc *SpanCollector) WithName(name string) *SpanCollector
- type TB
- type TestMeterProvider
- type TestTraceProvider
Examples ¶
Constants ¶
const LeakCanary = "gobricks-span-leak-canary-9f13"
LeakCanary is the string an ADR-083 test plants inside an error message before driving a framework span sink, so one constant decides what every such test looks for. Neither the name nor the value is credential-shaped: this file ships in the module, so a `password=`-style literal would match the generic-password rule of every secret scanner run against a consumer that vendors go-bricks, and a `Secret`-prefixed identifier trips gosec G101 here.
const ( // TestTracerName is the default tracer name used in observability tests, // avoiding a hardcoded "test" string literal at each call site. TestTracerName = "test" )
Variables ¶
This section is empty.
Functions ¶
func AssertExceptionTypeOnly ¶ added in v0.61.0
AssertExceptionTypeOnly asserts that span carries exactly one exception event, that it names wantType under exception.type, and that it carries NOTHING else — exception.message above all (ADR-083).
func AssertMetricCount ¶
func AssertMetricCount(t *testing.T, rm metricdata.ResourceMetrics, expected int)
AssertMetricCount asserts the total number of metrics collected.
func AssertMetricDescription ¶
func AssertMetricDescription(t *testing.T, rm metricdata.ResourceMetrics, metricName, expectedDesc string)
AssertMetricDescription asserts the description of a metric.
func AssertMetricExists ¶
func AssertMetricExists(t *testing.T, rm metricdata.ResourceMetrics, metricName string)
AssertMetricExists asserts that a metric with the given name exists.
func AssertMetricValue ¶
func AssertMetricValue(t *testing.T, rm metricdata.ResourceMetrics, metricName string, expectedValue any)
AssertMetricValue finds a metric by name and asserts its value. For Sum and Gauge metrics (counters, up-down counters, gauges), it asserts the first data point value. For Histogram metrics, it asserts the count.
func AssertNoExceptionEvent ¶ added in v0.61.0
AssertNoExceptionEvent is the negative of AssertExceptionTypeOnly: a span that recorded no error must carry no exception event.
func AssertNoSpanMarkers ¶ added in v0.61.0
AssertNoSpanMarkers asserts that no marker reaches ANY span sink: the status description, the span name, the span attributes, the event names, or the event attributes — keys as well as values, since a key is exported too.
func AssertSpanAttribute ¶
AssertSpanAttribute asserts that a span has a specific attribute with the expected value.
func AssertSpanError ¶
AssertSpanError asserts that a span has an error status with the expected description.
func AssertSpanName ¶
AssertSpanName asserts the name of a span.
func AssertSpanStatus ¶
AssertSpanStatus asserts the status of a span.
func AssertSpanStatusDescription ¶
AssertSpanStatusDescription asserts the status description of a span.
func FindMetric ¶
func FindMetric(rm metricdata.ResourceMetrics, metricName string) *metricdata.Metrics
FindMetric finds a metric by name in the ResourceMetrics. Returns nil if not found.
func GetMetricHistogramCount ¶
func GetMetricHistogramCount(rm metricdata.ResourceMetrics, metricName string) (uint64, error)
GetMetricHistogramCount gets the count for a Histogram metric. Returns error if metric not found or wrong type.
func GetMetricSumValue ¶
func GetMetricSumValue(rm metricdata.ResourceMetrics, metricName string) (any, error)
GetMetricSumValue gets the sum value for a Sum[int64] or Sum[float64] metric. Returns error if metric not found or wrong type.
func HistogramExemplars ¶ added in v0.60.0
func HistogramExemplars[N int64 | float64](t TB, rm metricdata.ResourceMetrics, metricName string) []metricdata.Exemplar[N]
HistogramExemplars returns the exemplars attached to every data point of a histogram metric.
func SumExemplars ¶ added in v0.60.0
func SumExemplars[N int64 | float64](t TB, rm metricdata.ResourceMetrics, metricName string) []metricdata.Exemplar[N]
SumExemplars is HistogramExemplars for a counter.
Types ¶
type SpanCollector ¶
type SpanCollector struct {
// contains filtered or unexported fields
}
SpanCollector provides a fluent API for filtering and asserting on captured spans.
Example ¶
ExampleSpanCollector demonstrates filtering spans by name and attributes. Note: In actual tests, use NewSpanCollector with a testing.T parameter for assertions.
package main
import (
"context"
"fmt"
"go.opentelemetry.io/otel/attribute"
obtest "github.com/gaborage/go-bricks/observability/testing"
)
const (
httpMethodAttr = "http.method"
dbSystemAttr = "db.system"
databaseQuerySpan = "database-query"
)
func main() {
tp := obtest.NewTestTraceProvider()
defer tp.Shutdown(context.Background())
tracer := tp.Tracer("example")
// Create multiple spans with different attributes
_, span1 := tracer.Start(context.Background(), databaseQuerySpan)
span1.SetAttributes(attribute.String(dbSystemAttr, "postgresql"))
span1.End()
_, span2 := tracer.Start(context.Background(), "http-request")
span2.SetAttributes(attribute.String(httpMethodAttr, "POST"))
span2.End()
_, span3 := tracer.Start(context.Background(), databaseQuerySpan)
span3.SetAttributes(attribute.String(dbSystemAttr, "mysql"))
span3.End()
// Get spans and count them by filtering
spans := tp.Exporter.GetSpans()
// Count database query spans
dbCount := 0
pgCount := 0
for i := range spans {
if spans[i].Name == databaseQuerySpan {
dbCount++
// Check for PostgreSQL attribute
for _, attr := range spans[i].Attributes {
if attr.Key == attribute.Key(dbSystemAttr) && attr.Value.AsString() == "postgresql" {
pgCount++
break
}
}
}
}
fmt.Printf("Database queries: %d\n", dbCount)
fmt.Printf("PostgreSQL queries: %d\n", pgCount)
}
Output: Database queries: 2 PostgreSQL queries: 1
func NewSpanCollector ¶
func NewSpanCollector(t *testing.T, exporter *tracetest.InMemoryExporter) *SpanCollector
NewSpanCollector creates a span collector from an in-memory exporter.
func (*SpanCollector) AssertCount ¶
func (sc *SpanCollector) AssertCount(expected int) *SpanCollector
AssertCount asserts the number of collected spans.
func (*SpanCollector) AssertEmpty ¶
func (sc *SpanCollector) AssertEmpty() *SpanCollector
AssertEmpty asserts that the collection is empty.
func (*SpanCollector) First ¶
func (sc *SpanCollector) First() tracetest.SpanStub
First returns the first span in the collection. Fails the test if the collection is empty.
func (*SpanCollector) Get ¶
func (sc *SpanCollector) Get(index int) tracetest.SpanStub
Get returns the span at the given index. Fails the test if the index is out of bounds.
func (*SpanCollector) Len ¶
func (sc *SpanCollector) Len() int
Len returns the number of collected spans.
func (*SpanCollector) WithAttribute ¶
func (sc *SpanCollector) WithAttribute(key string, value any) *SpanCollector
WithAttribute filters spans by attribute key-value pair and returns a new collector.
func (*SpanCollector) WithName ¶
func (sc *SpanCollector) WithName(name string) *SpanCollector
WithName filters spans by name and returns a new collector.
type TB ¶ added in v0.60.0
TB is what the exemplar helpers need from *testing.T. It is an interface so a caller that already abstracts over the test handle can pass its own.
type TestMeterProvider ¶
type TestMeterProvider struct {
*sdkmetric.MeterProvider
Reader *sdkmetric.ManualReader
}
TestMeterProvider wraps the SDK MeterProvider and manual reader for testing.
func InstallTestMeterProvider ¶ added in v0.64.0
func InstallTestMeterProvider(t *testing.T) *TestMeterProvider
InstallTestMeterProvider creates a test meter provider, installs it as the global meter provider, and registers a t.Cleanup that restores the previous provider. It leaves the text-map propagator alone.
Like InstallTestTraceProvider it never calls Shutdown: otel's default delegating provider binds to the FIRST provider installed in the binary via a sync.Once and never rebinds, so shutting an installed provider down would silently stop every later otel.Meter call that routes through that delegate.
The OTel globals are process-wide, so the install-to-cleanup window is not safe to overlap: do not call this from a t.Parallel test, and do not nest it inside another install whose cleanup runs later. Cleanups unwind LIFO, so sequential nesting restores correctly, but two overlapping installs can capture each other's provider and restore the wrong one.
Example:
mp := InstallTestMeterProvider(t)
counter, _ := otel.Meter("test").Int64Counter("test.counter")
counter.Add(context.Background(), 1)
rm := mp.Collect(t)
func NewTestMeterProvider ¶
func NewTestMeterProvider() *TestMeterProvider
NewTestMeterProvider creates a MeterProvider with a manual reader for testing. The manual reader allows collecting metrics on-demand for assertions without periodic exports.
Example:
mp := NewTestMeterProvider()
// To install it globally, use InstallTestMeterProvider(t), which restores
// the previous provider in t.Cleanup; never Shutdown a provider you
// installed (see the package doc — otel's delegate binds once).
meter := mp.Meter("test")
counter, _ := meter.Int64Counter("test.counter")
counter.Add(context.Background(), 1)
// Collect metrics for assertions
rm := mp.Collect(t)
Example ¶
ExampleNewTestMeterProvider demonstrates how to use the test meter provider to assert metrics created by your code.
package main
import (
"context"
"fmt"
"go.opentelemetry.io/otel"
obtest "github.com/gaborage/go-bricks/observability/testing"
)
func main() {
// Create a test meter provider with manual reader
mp := obtest.NewTestMeterProvider()
// Set as global provider (or use directly), and restore the previous one when
// done. Do NOT shut mp down — see ExampleNewTestTraceProvider (#1093).
prev := otel.GetMeterProvider()
defer otel.SetMeterProvider(prev)
otel.SetMeterProvider(mp)
// Your code that records metrics
meter := mp.Meter("example-service")
counter, _ := meter.Int64Counter("requests.count") // Metric options can be added here
counter.Add(context.Background(), 1) // Attributes can be added to metric recordings
counter.Add(context.Background(), 2)
// Collect metrics for assertions
// In real usage, you would pass the testing.T instance
// rm := mp.Collect(t)
fmt.Println("Metrics collected successfully")
}
Output: Metrics collected successfully
func (*TestMeterProvider) Collect ¶
func (tmp *TestMeterProvider) Collect(t *testing.T) metricdata.ResourceMetrics
Collect reads all metrics from the provider and returns them as ResourceMetrics. This is a convenience wrapper around Reader.Collect() with error handling.
type TestTraceProvider ¶
type TestTraceProvider struct {
*sdktrace.TracerProvider
Exporter *tracetest.InMemoryExporter
}
TestTraceProvider wraps the SDK TracerProvider and in-memory exporter for testing.
func InstallTestTraceProvider ¶ added in v0.64.0
func InstallTestTraceProvider(t *testing.T) *TestTraceProvider
InstallTestTraceProvider creates a test trace provider, installs it as the global tracer provider together with a W3C trace-context propagator, and registers a t.Cleanup that restores the previous provider and propagator.
It never calls Shutdown, on the provider it created or any other: otel's default delegating provider binds to the FIRST provider installed in the binary via a sync.Once and never rebinds, so a later otel.Tracer call made through that restored default still routes into the first-installed provider. Shutting one down would make those calls record nothing, with no error. Restoring the previous provider is safe; shutting one down is not.
The OTel globals are process-wide, so the install-to-cleanup window is not safe to overlap: do not call this from a t.Parallel test, and do not nest it inside another install whose cleanup runs later. Cleanups unwind LIFO, so sequential nesting restores correctly, but two overlapping installs can capture each other's provider and restore the wrong one.
Example:
tp := InstallTestTraceProvider(t) _, span := tp.TestTracer().Start(context.Background(), "operation") span.End() spans := tp.Exporter.GetSpans()
func NewTestTraceProvider ¶
func NewTestTraceProvider() *TestTraceProvider
NewTestTraceProvider creates a TracerProvider with an in-memory exporter for testing. The returned provider captures all spans in memory for assertion without sending them to an external backend.
Example:
tp := NewTestTraceProvider() // To install it globally, use InstallTestTraceProvider(t), which restores // the previous provider in t.Cleanup; never Shutdown a provider you // installed (see the package doc — otel's delegate binds once). // Later, get spans for assertions spans := tp.Exporter.GetSpans()
Example ¶
ExampleNewTestTraceProvider demonstrates how to use the test trace provider to assert spans created by your code.
package main
import (
"context"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
obtest "github.com/gaborage/go-bricks/observability/testing"
)
const httpMethodAttr = "http.method"
func main() {
// Create a test trace provider with in-memory exporter
tp := obtest.NewTestTraceProvider()
// Set as global provider (or use directly), and restore the previous one when
// done. Do NOT shut tp down: otel's default delegating provider binds to the
// first provider installed in the binary and never rebinds
// (internal/global/state.go sync.Once, #1093), so shutting tp down can silence
// otel.Tracer calls made through that restored default afterwards.
prev := otel.GetTracerProvider()
defer otel.SetTracerProvider(prev)
otel.SetTracerProvider(tp)
// Your code that creates spans
tracer := tp.Tracer("example-service")
_, span := tracer.Start(context.Background(), "process-request")
span.SetAttributes(
attribute.String(httpMethodAttr, "GET"),
attribute.String("http.url", "/api/users"),
attribute.Int("http.status_code", 200),
)
span.End()
// Get captured spans for assertions
spans := tp.Exporter.GetSpans()
fmt.Printf("Captured %d spans\n", len(spans))
}
Output: Captured 1 spans
func (*TestTraceProvider) TestTracer ¶ added in v0.18.0
func (ttp *TestTraceProvider) TestTracer() trace.Tracer
TestTracer returns a tracer with the standard test name. This is a convenience method that eliminates the need to pass TestTracerName to the Tracer() method in every test.
Example:
tp := NewTestTraceProvider() tracer := tp.TestTracer() _, span := tracer.Start(context.Background(), "operation")