Add obsx library

This commit is contained in:
2026-03-23 01:15:21 +03:00
commit 84413772df
10 changed files with 561 additions and 0 deletions

87
AGENTS.md Normal file
View 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
View 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
View 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).

1
config.go Normal file
View File

@@ -0,0 +1 @@
package obsx

17
doc.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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...)
}