Add obsx library
This commit is contained in:
87
AGENTS.md
Normal file
87
AGENTS.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# AGENTS.md — obsx
|
||||
|
||||
Universal guide for AI coding agents working with this codebase.
|
||||
|
||||
## Overview
|
||||
|
||||
`git.codelab.vc/pkg/obsx` is a Go observability library providing Prometheus metrics and OpenTelemetry tracing setup with consistent defaults and isolated registries.
|
||||
|
||||
## Package map
|
||||
|
||||
```
|
||||
obsx/ Root — tracing, metrics, config
|
||||
├── doc.go Package documentation
|
||||
├── tracer.go SetupTracer, StartSpan, TracerConfig
|
||||
├── metrics.go Metrics factory, NewMetrics, Counter/Histogram/Gauge, Handler
|
||||
└── config.go (empty — configs are co-located with their components)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ obsx │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌───────────────┴───────────────┐
|
||||
│ │
|
||||
┌──────┴──────┐ ┌──────┴──────┐
|
||||
│ Tracing │ │ Metrics │
|
||||
└──────┬──────┘ └──────┬──────┘
|
||||
│ │
|
||||
SetupTracer() NewMetrics()
|
||||
│ │
|
||||
▼ ▼
|
||||
OTLP gRPC exporter prometheus.Registry
|
||||
TracerProvider (process + Go collectors)
|
||||
(global via otel.Set) │
|
||||
│ Counter / Histogram / Gauge
|
||||
▼ │
|
||||
StartSpan() Handler() → /metrics
|
||||
```
|
||||
|
||||
## Common tasks
|
||||
|
||||
### Add a new metric type (e.g., Summary)
|
||||
|
||||
1. Add a method to `Metrics` struct in `metrics.go`
|
||||
2. Create the Prometheus collector with `m.cfg.Namespace` / `m.cfg.Subsystem`
|
||||
3. Register via `m.registry.MustRegister()`
|
||||
4. Return the collector
|
||||
|
||||
### Change the trace exporter (e.g., HTTP instead of gRPC)
|
||||
|
||||
1. Replace `otlptracegrpc.New` in `SetupTracer` with the desired exporter
|
||||
2. Update `TracerConfig` fields if the new exporter needs different options
|
||||
3. Keep the same return signature (`shutdown func(context.Context) error`)
|
||||
|
||||
### Add custom resource attributes to traces
|
||||
|
||||
1. Append additional `resource.WithAttributes(...)` options to the `attrs` slice in `SetupTracer`
|
||||
2. Use `semconv` constants where possible
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **SetupTracer sets the global provider**: calling it multiple times overwrites the previous provider; the old provider is not shut down automatically
|
||||
- **Isolated registry**: `NewMetrics` creates its own `prometheus.Registry`, not the global default. Metrics registered here will not appear in `promhttp.Handler()` — use `m.Handler()` instead
|
||||
- **Sampler default**: if `TracerConfig.Sampler` is 0 or negative, it defaults to 1.0 (sample everything), not 0
|
||||
- **Insecure gRPC**: `SetupTracer` always uses `otlptracegrpc.WithInsecure()` — no TLS for the OTLP endpoint
|
||||
- **StartSpan tracer name**: uses the hardcoded tracer name `"obsx"` — all spans created via this helper share the same instrumentation scope
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
go build ./... # compile
|
||||
go test ./... # all tests
|
||||
go test -race ./... # tests with race detector
|
||||
go test -v -run TestName ./... # single test
|
||||
go vet ./... # static analysis
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Struct-based Config** with `defaults()` method for zero-value defaults
|
||||
- **Isolated Prometheus registry** — each `Metrics` instance has its own registry
|
||||
- **stdlib only** testing — no testify, no gomock
|
||||
- **No functional options** — direct struct configuration
|
||||
- **Shutdown function** — `SetupTracer` returns a shutdown callback, caller is responsible for calling it
|
||||
42
CLAUDE.md
Normal file
42
CLAUDE.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# CLAUDE.md — obsx
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
go build ./... # compile
|
||||
go test ./... # all tests
|
||||
go test -race ./... # tests with race detector
|
||||
go test -v -run TestName ./... # single test
|
||||
go vet ./... # static analysis
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Module**: `git.codelab.vc/pkg/obsx`, Go 1.24, depends on OpenTelemetry and Prometheus client_golang
|
||||
- **Single package** `obsx`
|
||||
|
||||
### Core patterns
|
||||
|
||||
- **SetupTracer** — creates OTLP gRPC exporter, builds `sdktrace.TracerProvider` with batcher and ratio sampler, sets it as global `otel.TracerProvider`
|
||||
- **StartSpan** — convenience function using `otel.Tracer("obsx")` to start spans via the global provider
|
||||
- **TracerConfig** — service name, version, OTLP endpoint, sampling ratio; `defaults()` sets `Sampler` to 1.0 if unset
|
||||
- **Metrics** — factory that creates Prometheus `CounterVec`, `HistogramVec`, `GaugeVec` with consistent namespace/subsystem
|
||||
- **NewMetrics** — creates an isolated `prometheus.Registry` pre-loaded with process and Go collectors
|
||||
- **Handler()** — returns `promhttp.HandlerFor` using the isolated registry
|
||||
- **Registry()** — exposes the underlying `*prometheus.Registry` for direct access
|
||||
|
||||
### Tracer lifecycle
|
||||
|
||||
- `SetupTracer` returns `shutdown func(context.Context) error` — must be called to flush pending spans
|
||||
- Shutdown is the `TracerProvider.Shutdown` method directly
|
||||
|
||||
## Conventions
|
||||
|
||||
- Struct-based configs (`TracerConfig`, `MetricsConfig`) with `defaults()` methods
|
||||
- Isolated Prometheus registry — not the global default, avoids conflicts in tests and multi-tenant setups
|
||||
- `nil` buckets in `Histogram()` defaults to `prometheus.DefBuckets`
|
||||
- No functional options — direct struct configuration only
|
||||
|
||||
## See also
|
||||
|
||||
- `AGENTS.md` — universal AI agent guide with package map and common tasks
|
||||
94
README.md
Normal file
94
README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# obsx
|
||||
|
||||
Observability library for Go providing metrics (Prometheus) and tracing (OpenTelemetry) setup with consistent defaults. Creates a Prometheus registry with process/Go collectors, a metrics factory with namespace/subsystem scoping, and an OTLP gRPC tracer provider with configurable sampling.
|
||||
|
||||
```
|
||||
go get git.codelab.vc/pkg/obsx
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```go
|
||||
// Tracing
|
||||
shutdown, err := obsx.SetupTracer(ctx, obsx.TracerConfig{
|
||||
ServiceName: "my-service",
|
||||
ServiceVersion: "1.0.0",
|
||||
Endpoint: "localhost:4317",
|
||||
Sampler: 1.0, // sample everything
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer shutdown(ctx)
|
||||
|
||||
// Metrics
|
||||
metrics := obsx.NewMetrics(obsx.MetricsConfig{
|
||||
Namespace: "myapp",
|
||||
Subsystem: "http",
|
||||
})
|
||||
reqCounter := metrics.Counter("requests_total", "Total HTTP requests", "method", "status")
|
||||
latency := metrics.Histogram("request_duration_seconds", "Request latency", nil, "method")
|
||||
|
||||
http.Handle("/metrics", metrics.Handler())
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|-------------|
|
||||
| `SetupTracer` | Initializes an OpenTelemetry tracer provider with OTLP gRPC exporter, sets it as the global provider. |
|
||||
| `StartSpan` | Convenience wrapper that starts a span using the global tracer provider. |
|
||||
| `Metrics` | Factory for creating Prometheus metrics with consistent namespace/subsystem naming. |
|
||||
| `TracerConfig` | Configures the tracer: service name, version, OTLP endpoint, sampling ratio. |
|
||||
| `MetricsConfig` | Configures the metrics factory: namespace and subsystem. |
|
||||
|
||||
## Tracing
|
||||
|
||||
`SetupTracer` creates an OTLP gRPC exporter, builds a tracer provider with batched span export and ratio-based sampling, and registers it as the global `otel.TracerProvider`. Returns a shutdown function that must be called on application exit.
|
||||
|
||||
```go
|
||||
shutdown, err := obsx.SetupTracer(ctx, obsx.TracerConfig{
|
||||
ServiceName: "orders-api",
|
||||
Endpoint: "jaeger:4317",
|
||||
Sampler: 0.1, // sample 10%
|
||||
})
|
||||
defer shutdown(ctx)
|
||||
```
|
||||
|
||||
Start spans with the convenience helper:
|
||||
|
||||
```go
|
||||
ctx, span := obsx.StartSpan(ctx, "process-order")
|
||||
defer span.End()
|
||||
```
|
||||
|
||||
## Metrics
|
||||
|
||||
`NewMetrics` creates a fresh Prometheus registry (isolated from the global default) pre-loaded with process and Go runtime collectors. All metrics created through the factory inherit the configured namespace and subsystem.
|
||||
|
||||
```go
|
||||
m := obsx.NewMetrics(obsx.MetricsConfig{Namespace: "myapp", Subsystem: "db"})
|
||||
|
||||
// Counter
|
||||
queries := m.Counter("queries_total", "Total queries executed", "operation")
|
||||
queries.WithLabelValues("select").Inc()
|
||||
|
||||
// Histogram (nil buckets = prometheus.DefBuckets)
|
||||
latency := m.Histogram("query_duration_seconds", "Query latency", nil, "operation")
|
||||
|
||||
// Gauge
|
||||
conns := m.Gauge("connections_active", "Active connections", "pool")
|
||||
|
||||
// HTTP handler for /metrics endpoint
|
||||
http.Handle("/metrics", m.Handler())
|
||||
```
|
||||
|
||||
Access the underlying registry directly:
|
||||
|
||||
```go
|
||||
registry := m.Registry()
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.24+, [OpenTelemetry Go](https://opentelemetry.io/docs/languages/go/), [Prometheus client_golang](https://github.com/prometheus/client_golang).
|
||||
17
doc.go
Normal file
17
doc.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Package obsx provides observability setup for the platform: metrics (Prometheus)
|
||||
// and tracing (OpenTelemetry).
|
||||
//
|
||||
// # Quick Start
|
||||
//
|
||||
// // Tracing
|
||||
// shutdown, err := obsx.SetupTracer(ctx, obsx.TracerConfig{
|
||||
// ServiceName: "my-service",
|
||||
// Endpoint: "localhost:4317",
|
||||
// })
|
||||
// defer shutdown(ctx)
|
||||
//
|
||||
// // Metrics
|
||||
// metrics := obsx.NewMetrics(obsx.MetricsConfig{Namespace: "myapp"})
|
||||
// reqCounter := metrics.Counter("requests_total", "Total requests", "method", "status")
|
||||
// http.Handle("/metrics", metrics.Handler())
|
||||
package obsx
|
||||
36
go.mod
Normal file
36
go.mod
Normal file
@@ -0,0 +1,36 @@
|
||||
module git.codelab.vc/pkg/obsx
|
||||
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
)
|
||||
75
go.sum
Normal file
75
go.sum
Normal file
@@ -0,0 +1,75 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
78
metrics.go
Normal file
78
metrics.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package obsx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// MetricsConfig configures the metrics factory.
|
||||
type MetricsConfig struct {
|
||||
Namespace string
|
||||
Subsystem string
|
||||
}
|
||||
|
||||
// Metrics is a factory for creating Prometheus metrics with consistent naming.
|
||||
type Metrics struct {
|
||||
cfg MetricsConfig
|
||||
registry *prometheus.Registry
|
||||
}
|
||||
|
||||
// NewMetrics creates a new Metrics factory.
|
||||
func NewMetrics(cfg MetricsConfig) *Metrics {
|
||||
reg := prometheus.NewRegistry()
|
||||
reg.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
|
||||
reg.MustRegister(prometheus.NewGoCollector())
|
||||
return &Metrics{cfg: cfg, registry: reg}
|
||||
}
|
||||
|
||||
// Counter creates and registers a new CounterVec.
|
||||
func (m *Metrics) Counter(name, help string, labels ...string) *prometheus.CounterVec {
|
||||
c := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: m.cfg.Namespace,
|
||||
Subsystem: m.cfg.Subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
}, labels)
|
||||
m.registry.MustRegister(c)
|
||||
return c
|
||||
}
|
||||
|
||||
// Histogram creates and registers a new HistogramVec.
|
||||
func (m *Metrics) Histogram(name, help string, buckets []float64, labels ...string) *prometheus.HistogramVec {
|
||||
if buckets == nil {
|
||||
buckets = prometheus.DefBuckets
|
||||
}
|
||||
h := prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: m.cfg.Namespace,
|
||||
Subsystem: m.cfg.Subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
Buckets: buckets,
|
||||
}, labels)
|
||||
m.registry.MustRegister(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// Gauge creates and registers a new GaugeVec.
|
||||
func (m *Metrics) Gauge(name, help string, labels ...string) *prometheus.GaugeVec {
|
||||
g := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: m.cfg.Namespace,
|
||||
Subsystem: m.cfg.Subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
}, labels)
|
||||
m.registry.MustRegister(g)
|
||||
return g
|
||||
}
|
||||
|
||||
// Handler returns an http.Handler for the /metrics endpoint.
|
||||
func (m *Metrics) Handler() http.Handler {
|
||||
return promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{})
|
||||
}
|
||||
|
||||
// Registry returns the underlying Prometheus registry.
|
||||
func (m *Metrics) Registry() *prometheus.Registry {
|
||||
return m.registry
|
||||
}
|
||||
64
metrics_test.go
Normal file
64
metrics_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package obsx
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMetrics_Counter(t *testing.T) {
|
||||
m := NewMetrics(MetricsConfig{Namespace: "test"})
|
||||
c := m.Counter("requests_total", "Total requests", "method")
|
||||
c.WithLabelValues("GET").Inc()
|
||||
c.WithLabelValues("GET").Inc()
|
||||
c.WithLabelValues("POST").Inc()
|
||||
|
||||
body := scrape(t, m)
|
||||
if !strings.Contains(body, `test_requests_total{method="GET"} 2`) {
|
||||
t.Errorf("expected GET counter=2, got:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `test_requests_total{method="POST"} 1`) {
|
||||
t.Errorf("expected POST counter=1, got:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetrics_Histogram(t *testing.T) {
|
||||
m := NewMetrics(MetricsConfig{Namespace: "test"})
|
||||
h := m.Histogram("duration_seconds", "Duration", nil, "op")
|
||||
h.WithLabelValues("query").Observe(0.1)
|
||||
|
||||
body := scrape(t, m)
|
||||
if !strings.Contains(body, "test_duration_seconds") {
|
||||
t.Errorf("expected histogram, got:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetrics_Gauge(t *testing.T) {
|
||||
m := NewMetrics(MetricsConfig{Namespace: "test"})
|
||||
g := m.Gauge("connections", "Active connections", "pool")
|
||||
g.WithLabelValues("main").Set(42)
|
||||
|
||||
body := scrape(t, m)
|
||||
if !strings.Contains(body, `test_connections{pool="main"} 42`) {
|
||||
t.Errorf("expected gauge=42, got:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetrics_Handler(t *testing.T) {
|
||||
m := NewMetrics(MetricsConfig{})
|
||||
handler := m.Handler()
|
||||
if handler == nil {
|
||||
t.Fatal("Handler returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func scrape(t *testing.T, m *Metrics) string {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/metrics", nil)
|
||||
m.Handler().ServeHTTP(w, r)
|
||||
body, _ := io.ReadAll(w.Body)
|
||||
return string(body)
|
||||
}
|
||||
67
tracer.go
Normal file
67
tracer.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package obsx
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// TracerConfig configures the OpenTelemetry tracer.
|
||||
type TracerConfig struct {
|
||||
ServiceName string
|
||||
ServiceVersion string
|
||||
Endpoint string // OTLP gRPC endpoint, e.g. "localhost:4317"
|
||||
Sampler float64 // sampling ratio, default: 1.0 (sample everything)
|
||||
}
|
||||
|
||||
func (c *TracerConfig) defaults() {
|
||||
if c.Sampler <= 0 {
|
||||
c.Sampler = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
// SetupTracer initializes an OpenTelemetry tracer provider with OTLP gRPC exporter.
|
||||
// Returns a shutdown function that must be called on application exit.
|
||||
func SetupTracer(ctx context.Context, cfg TracerConfig) (shutdown func(context.Context) error, err error) {
|
||||
cfg.defaults()
|
||||
|
||||
exporter, err := otlptracegrpc.New(ctx,
|
||||
otlptracegrpc.WithEndpoint(cfg.Endpoint),
|
||||
otlptracegrpc.WithInsecure(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attrs := []resource.Option{
|
||||
resource.WithAttributes(semconv.ServiceName(cfg.ServiceName)),
|
||||
}
|
||||
if cfg.ServiceVersion != "" {
|
||||
attrs = append(attrs, resource.WithAttributes(semconv.ServiceVersion(cfg.ServiceVersion)))
|
||||
}
|
||||
|
||||
res, err := resource.New(ctx, attrs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithBatcher(exporter),
|
||||
sdktrace.WithResource(res),
|
||||
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(cfg.Sampler)),
|
||||
)
|
||||
|
||||
otel.SetTracerProvider(tp)
|
||||
|
||||
return tp.Shutdown, nil
|
||||
}
|
||||
|
||||
// StartSpan starts a new span using the global tracer provider.
|
||||
func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
||||
return otel.Tracer("obsx").Start(ctx, name, opts...)
|
||||
}
|
||||
Reference in New Issue
Block a user