OpenTelemetry instrumentation for Go tests. Make your tests observable and traceable by streaming spans, metrics, and log events to any OTLP collector.
- Automatic span creation for tests and subtests
- Log interception (
t.Log→ OTEL span events) - Metrics recording (duration histogram, test counters)
- Setup/teardown tracing
- Manual span creation for operations under test
- OTLP export to any compatible collector
go get github.com/monkescience/spectraInitialize once in TestMain, then wrap each test with sp.New(t). The
examples below assume a package-level sp *spectra.Spectra so every test in
the package can use it.
package mypkg_test
import (
"log"
"os"
"testing"
"github.com/monkescience/spectra"
)
var sp *spectra.Spectra
func TestMain(m *testing.M) {
var err error
sp, err = spectra.Init(
spectra.WithServiceName("my-service-tests"),
spectra.WithEndpoint("grpc://localhost:4317"),
spectra.WithInsecure(), // skip TLS verification for local collectors
)
if err != nil {
log.Fatalf("spectra init: %v", err)
}
defer sp.Shutdown()
os.Exit(m.Run())
}func TestFeature(t *testing.T) {
st, err := sp.New(t)
if err != nil {
t.Fatalf("spectra: %v", err)
}
// Logs become span events
st.Log("starting test")
// Add custom attributes to the test span
st.SetAttributes(attribute.String("feature", "login"))
// Subtests become child spans
st.Run("validates_input", func(st *spectra.T) {
// test code...
})
}func TestDatabaseQuery(t *testing.T) {
st, err := sp.New(t)
if err != nil {
t.Fatalf("spectra: %v", err)
}
// Create a child span for the operation you're testing
ctx, span := st.StartSpan("db-query")
defer span.End()
_, err = db.Query(ctx, "SELECT ...")
if err != nil {
t.Fatalf("query: %v", err)
}
}func TestWithFixtures(t *testing.T) {
st, err := sp.New(t)
if err != nil {
t.Fatalf("spectra: %v", err)
}
// Setup gets its own span
st.Setup(func(ctx context.Context) {
seedDatabase(ctx)
})
// Teardown runs on cleanup with its own span
st.Teardown(func(ctx context.Context) {
cleanupDatabase(ctx)
})
st.Run("query", func(st *spectra.T) {
// test with seeded data...
})
}By default, spectra keeps its providers local so it does not interfere with
code under test that reads otel.GetTracerProvider(). If your code relies
on the global providers being spectra's, opt in:
sp, err := spectra.Init(
spectra.WithServiceName("my-service-tests"),
spectra.WithEndpoint("grpc://localhost:4317"),
spectra.WithSetGlobalProviders(), // previous globals are restored on Shutdown
)| Option | Description |
|---|---|
WithServiceName(name) |
Service name for telemetry (required) |
WithEndpoint(endpoint) |
OTLP collector endpoint with scheme (required) |
WithInsecure() |
gRPC: disable TLS; HTTPS: skip cert verification |
WithShutdownTimeout(d) |
Graceful shutdown timeout (default: 5s) |
WithLogger(logger) |
*slog.Logger for spectra's own operational messages (default: slog.Default()) |
WithTracerProvider(tp) |
Use an existing trace.TracerProvider instead of creating one |
WithSetGlobalProviders() |
Install spectra's providers as OTEL globals (off by default) |
WithoutTraces() |
Disable trace collection |
WithoutMetrics() |
Disable metrics collection |
WithoutLogs() |
Disable log capture as span events |
The endpoint must include a scheme:
| Scheme | Protocol | TLS |
|---|---|---|
grpc://host:port |
gRPC | Yes — use WithInsecure() to disable |
http://host:port |
HTTP | No |
https://host:port |
HTTPS | Yes — use WithInsecure() to skip cert verification |
Spectra surfaces a small set of sentinel errors you can branch on with
errors.Is:
| Error | When | Resolution |
|---|---|---|
ErrMissingServiceName |
spectra.Init called without WithServiceName |
Pass WithServiceName("…") |
ErrMissingEndpoint |
spectra.Init called without WithEndpoint |
Pass WithEndpoint("grpc://…") |
ErrInvalidEndpoint |
Endpoint missing a scheme | Use grpc://, http://, or https:// |
ErrNotInitialized |
sp.New(t) called before spectra.Init() or on a nil *Spectra |
Call spectra.Init() in TestMain first |
ErrAlreadyShutdown |
Operations attempted after sp.Shutdown() |
Ensure tests run before shutdown |
- Test span per
sp.New()call - Child spans for subtests via
st.Run() - Setup/teardown spans
- Custom spans via
st.StartSpan() - Span status reflects test pass/fail/skip
| Metric | Type | Description |
|---|---|---|
test.duration |
Histogram | Test execution time in seconds |
test.count |
Counter | Number of tests by status (pass/fail/skip) |
All t.Log(), t.Error(), t.Fatal(), and t.Skip() calls are captured as span events with appropriate severity levels.
MIT