Skip to content

Instantly share code, notes, and snippets.

@hlubek
Last active September 17, 2024 12:48
Show Gist options
  • Save hlubek/76db48a94a49a8b196ba556e0690f955 to your computer and use it in GitHub Desktop.
Save hlubek/76db48a94a49a8b196ba556e0690f955 to your computer and use it in GitHub Desktop.
Testing OpenTelemetry in Go with test helpers
package handler
// Handler handles commands
type Handler struct {
db *sql.DB
instrumentation instrumentation
}
type Deps struct {
MeterProvider metric.MeterProvider
}
func NewHandler(db *sql.DB, deps Deps) *Handler {
return &Handler{
db: db,
instrumentation: initInstrumentation(deps.MeterProvider),
}
}
type instrumentation struct {
loginSuccessCounter metric.Int64Counter
loginFailedCounter metric.Int64Counter
}
func initInstrumentation(provider metric.MeterProvider) instrumentation {
if provider == nil {
provider = noop.NewMeterProvider()
}
meter := provider.Meter("myvendor.mytld/myproject/backend/handler")
return instrumentation{
loginSuccessCounter: mustInstrument(meter.Int64Counter(
"login.success.counter",
metric.WithDescription("Number of successful logins."),
metric.WithUnit("{call}"),
)),
loginFailedCounter: mustInstrument(meter.Int64Counter(
"login.failed.counter",
metric.WithDescription("Number of failed logins."),
metric.WithUnit("{call}"),
)),
}
}
func mustInstrument[T any](instrument T, err error) T {
if err != nil {
panic(err)
}
return instrument
}
package handler
func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) {
// ... do the actual authentication logic
if err != nil {
h.instrumentation.loginFailedCounter.Add(ctx, 1)
return err
}
h.instrumentation.loginSuccessCounter.Add(ctx, 1)
}
package handler_test
import (
test_telemetry "myvendor.mytld/myproject/backend/test/telemetry"
)
func TestHandler_Login_Success(t *testing.T) {
db := test_db.CreateTestDatabase(t)
metricsReader, meterProvider := test_telemetry.SetupTestMeter(t)
// Pass test meterProvider to handler for initializing the instruments
h := handler.NewHandler(d, handler.Deps{MeterProvider: meterProvider})
err := h.Login(context.Background(), domain.LoginCmd{
// ...
})
require.NoError(t, err)
test_telemetry.AssertMeterCounter(t, metricsReader, "myvendor.mytld/myproject/backend/handler", "login.success.counter", 1)
}
package telemetry
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)
func SetupTestMeter(t *testing.T) (*sdkmetric.ManualReader, metric.MeterProvider) {
t.Helper()
reader := sdkmetric.NewManualReader()
provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))
t.Cleanup(func() {
err := provider.Shutdown(context.Background())
assert.NoError(t, err)
})
return reader, provider
}
func AssertMeterCounter(t *testing.T, reader sdkmetric.Reader, scope, name string, want int64) {
t.Helper()
metricsData := metricdata.ResourceMetrics{}
err := reader.Collect(context.Background(), &metricsData)
require.NoError(t, err)
scopeMetrics, found := find(metricsData.ScopeMetrics, func(m metricdata.ScopeMetrics) bool {
return m.Scope.Name == scope
})
if !found {
t.Fatalf("metrics for scope %q not found", scope)
}
metrics, found := find(scopeMetrics.Metrics, func(m metricdata.Metrics) bool {
return m.Name == name
})
if !found {
t.Fatalf("metrics for name %q not found", name)
}
agg, found := metrics.Data.(metricdata.Sum[int64])
if !found {
t.Fatalf("metrics for name %q is not a counter", name)
}
var sum int64
for _, dp := range agg.DataPoints {
sum += dp.Value
}
assert.Equal(t, want, sum, "sum of metric %q in scope %q", name, scope)
}
func find[T any](slice []T, predicate func(T) bool) (T, bool) {
for _, item := range slice {
if predicate(item) {
return item, true
}
}
var zero T
return zero, false
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment