chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:33 +08:00
commit e071084ebe
982 changed files with 160368 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
# Prometheus Wrapper
The `prometheus` wrapper package exposes standard request metrics (request
count, latency, errors) for go-micro services and clients, so they can be
scraped by a Prometheus server with zero extra boilerplate.
Resolves [micro/go-micro#2893](https://github.com/micro/go-micro/issues/2893).
## Installation
```go
import prom "go-micro.dev/v5/wrapper/monitoring/prometheus"
```
## Exported Metrics
All metrics are labelled with `service`, `endpoint` and `status`
(`"success"` or `"fail"`). Labels are kept small on purpose to avoid
blowing up Prometheus memory.
| Metric | Type | Description |
|---------------------------------|-----------|---------------------------------------------|
| `micro_request_total` | Counter | Total number of requests handled. |
| `micro_request_duration_seconds`| Histogram | Request latency distribution (seconds). |
The `micro` prefix can be overridden with `prom.ServiceName("myapp")`.
## Basic Usage
```go
import (
"go-micro.dev/v5"
prom "go-micro.dev/v5/wrapper/monitoring/prometheus"
)
func main() {
service := micro.NewService(
micro.Name("example.service"),
micro.WrapHandler(prom.NewHandlerWrapper()),
micro.WrapClient(prom.NewClientWrapper()),
micro.WrapSubscriber(prom.NewSubscriberWrapper()),
)
service.Init()
if err := service.Run(); err != nil {
panic(err)
}
}
```
To expose the metrics to Prometheus, serve the default `promhttp` handler
on a side HTTP endpoint:
```go
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
go func() {
http.Handle("/metrics", promhttp.Handler())
_ = http.ListenAndServe(":9100", nil)
}()
```
Then point Prometheus at it:
```yaml
scrape_configs:
- job_name: 'example.service'
static_configs:
- targets: ['localhost:9100']
```
## Wrappers
| Constructor | Wraps | Notes |
|---------------------------|-------------------------|--------------------------------------------|
| `NewHandlerWrapper` | `server.HandlerWrapper` | Incoming RPC handlers. |
| `NewSubscriberWrapper` | `server.SubscriberWrapper` | Event subscribers (uses topic as endpoint). |
| `NewCallWrapper` | `client.CallWrapper` | Outgoing unary RPC calls only. |
| `NewClientWrapper` | `client.Wrapper` | Outgoing `Call` **and** `Publish`. |
`NewClientWrapper` is the right choice when you want metrics for both
`Call` and `Publish`; use `NewCallWrapper` if you only care about unary
calls and want lower overhead.
## Configuration
All constructors accept functional options:
```go
prom.NewHandlerWrapper(
prom.ServiceName("myapp"), // metric name prefix
prom.Namespace("prod"), // Prometheus namespace
prom.Subsystem("api"), // Prometheus subsystem
prom.ConstLabels(prometheus.Labels{"dc": "eu-1"}), // labels on every metric
prom.Buckets([]float64{0.005, 0.05, 0.5, 1, 5}), // latency buckets
prom.Registerer(myRegistry), // custom registerer
)
```
Defaults:
- `ServiceName`: `"micro"`
- `Buckets`: `prometheus.DefBuckets`
- `Registerer`: `prometheus.DefaultRegisterer`
## Reusing Collectors
Creating multiple wrappers with the same options (e.g. `NewHandlerWrapper`
and `NewClientWrapper` together) is safe: the collectors are cached per
`(name, namespace, subsystem)` triple and `AlreadyRegisteredError` from
Prometheus is handled transparently, so the existing collector is reused.
## Testing
The package ships with unit tests that use a fresh `prometheus.Registry`
per test to keep assertions isolated:
```bash
go test ./wrapper/monitoring/prometheus/...
```
## License
Apache 2.0
+95
View File
@@ -0,0 +1,95 @@
package prometheus
import (
"sync"
"github.com/prometheus/client_golang/prometheus"
)
// metrics bundles the counters/histograms used by the wrappers.
// A metrics value is keyed by (name + namespace + subsystem) so that
// multiple wrappers created with the same options share the same
// collectors instead of failing with AlreadyRegisteredError.
type metrics struct {
requestTotal *prometheus.CounterVec
requestDuration *prometheus.HistogramVec
}
// metricLabels are the labels we use on every metric. They intentionally
// stay on a small, low-cardinality set: high-cardinality labels (e.g. the
// full request body) must not end up in Prometheus.
var metricLabels = []string{"service", "endpoint", "status"}
var (
metricsMu sync.Mutex
metricsCache = map[string]*metrics{}
)
// getMetrics returns a cached metrics bundle for the given options, creating
// and registering it on first use. Collectors that were already registered
// on the underlying Registerer (e.g. because a user constructed two wrappers
// with identical options) are reused transparently.
func getMetrics(opts Options) *metrics {
key := opts.Name + "\x00" + opts.Namespace + "\x00" + opts.Subsystem
metricsMu.Lock()
defer metricsMu.Unlock()
if m, ok := metricsCache[key]; ok {
return m
}
counter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Name: opts.Name + "_request_total",
Help: "How many go-micro requests processed, partitioned by service, endpoint and status.",
ConstLabels: opts.ConstLabels,
},
metricLabels,
)
histogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Name: opts.Name + "_request_duration_seconds",
Help: "Histogram of go-micro request latencies in seconds, partitioned by service, endpoint and status.",
ConstLabels: opts.ConstLabels,
Buckets: opts.Buckets,
},
metricLabels,
)
m := &metrics{
requestTotal: register(opts.Registerer, counter).(*prometheus.CounterVec),
requestDuration: register(opts.Registerer, histogram).(*prometheus.HistogramVec),
}
metricsCache[key] = m
return m
}
// register registers c on r. If an identical collector is already registered
// (AlreadyRegisteredError), the existing collector is returned so that the
// wrapper can be constructed more than once without panicking.
func register(r prometheus.Registerer, c prometheus.Collector) prometheus.Collector {
if err := r.Register(c); err != nil {
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
return are.ExistingCollector
}
// Any other registration error is a programming mistake (e.g.
// inconsistent label dimensions) and should surface loudly.
panic(err)
}
return c
}
// status returns "success" or "fail" depending on whether err is nil.
// Using a fixed, low-cardinality set keeps Prometheus memory bounded.
func status(err error) string {
if err != nil {
return "fail"
}
return "success"
}
+99
View File
@@ -0,0 +1,99 @@
// Package prometheus provides a go-micro wrapper that exposes standard
// request/response metrics (request count, latency, errors) to Prometheus.
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
)
// Options holds configuration for the Prometheus wrapper.
type Options struct {
// Name is the metric name prefix (Prometheus "name").
// Default: "micro".
Name string
// Namespace for the Prometheus metrics.
// Default: "" (empty).
Namespace string
// Subsystem for the Prometheus metrics.
// Default: "" (empty).
Subsystem string
// ConstLabels are labels applied to every metric.
ConstLabels prometheus.Labels
// Buckets defines the histogram buckets (seconds) for latency.
// When nil, prometheus.DefBuckets is used.
Buckets []float64
// Registerer is used to register the metrics.
// When nil, prometheus.DefaultRegisterer is used.
Registerer prometheus.Registerer
}
// Option applies a single configuration value.
type Option func(*Options)
// ServiceName sets the metric name prefix (Prometheus "name").
func ServiceName(name string) Option {
return func(o *Options) {
o.Name = name
}
}
// Namespace sets the Prometheus namespace for metrics.
func Namespace(namespace string) Option {
return func(o *Options) {
o.Namespace = namespace
}
}
// Subsystem sets the Prometheus subsystem for metrics.
func Subsystem(subsystem string) Option {
return func(o *Options) {
o.Subsystem = subsystem
}
}
// ConstLabels sets labels applied to every metric.
func ConstLabels(labels prometheus.Labels) Option {
return func(o *Options) {
o.ConstLabels = labels
}
}
// Buckets sets the histogram buckets (in seconds) for latency metrics.
func Buckets(buckets []float64) Option {
return func(o *Options) {
o.Buckets = buckets
}
}
// Registerer sets the Prometheus registerer used to register metrics.
// When unset, prometheus.DefaultRegisterer is used.
func Registerer(r prometheus.Registerer) Option {
return func(o *Options) {
o.Registerer = r
}
}
// newOptions builds Options from the provided Option functions, applying
// sensible defaults.
func newOptions(opts ...Option) Options {
options := Options{
Name: "micro",
Buckets: prometheus.DefBuckets,
Registerer: prometheus.DefaultRegisterer,
}
for _, o := range opts {
o(&options)
}
if options.Registerer == nil {
options.Registerer = prometheus.DefaultRegisterer
}
if len(options.Buckets) == 0 {
options.Buckets = prometheus.DefBuckets
}
return options
}
+89
View File
@@ -0,0 +1,89 @@
package prometheus
import (
"context"
"time"
"go-micro.dev/v6/client"
"go-micro.dev/v6/registry"
"go-micro.dev/v6/server"
)
// NewHandlerWrapper returns a server.HandlerWrapper that records Prometheus
// metrics (request count and latency) for every incoming RPC handled by the
// server.
func NewHandlerWrapper(opts ...Option) server.HandlerWrapper {
m := getMetrics(newOptions(opts...))
return func(fn server.HandlerFunc) server.HandlerFunc {
return func(ctx context.Context, req server.Request, rsp interface{}) error {
start := time.Now()
err := fn(ctx, req, rsp)
observe(m, req.Service(), req.Endpoint(), err, start)
return err
}
}
}
// NewSubscriberWrapper returns a server.SubscriberWrapper that records
// Prometheus metrics for every message delivered to a subscriber.
func NewSubscriberWrapper(opts ...Option) server.SubscriberWrapper {
m := getMetrics(newOptions(opts...))
return func(fn server.SubscriberFunc) server.SubscriberFunc {
return func(ctx context.Context, msg server.Message) error {
start := time.Now()
err := fn(ctx, msg)
observe(m, "subscriber", msg.Topic(), err, start)
return err
}
}
}
// NewCallWrapper returns a client.CallWrapper that records Prometheus
// metrics for every outgoing RPC issued by the client.
func NewCallWrapper(opts ...Option) client.CallWrapper {
m := getMetrics(newOptions(opts...))
return func(fn client.CallFunc) client.CallFunc {
return func(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
start := time.Now()
err := fn(ctx, node, req, rsp, opts)
observe(m, req.Service(), req.Endpoint(), err, start)
return err
}
}
}
// NewClientWrapper returns a client.Wrapper that records Prometheus metrics
// for every Call and Publish issued on the wrapped client. Use it when you
// need metrics for Publish as well as Call; NewCallWrapper only covers Call.
func NewClientWrapper(opts ...Option) client.Wrapper {
m := getMetrics(newOptions(opts...))
return func(c client.Client) client.Client {
return &clientWrapper{Client: c, metrics: m}
}
}
type clientWrapper struct {
client.Client
metrics *metrics
}
func (w *clientWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
start := time.Now()
err := w.Client.Call(ctx, req, rsp, opts...)
observe(w.metrics, req.Service(), req.Endpoint(), err, start)
return err
}
func (w *clientWrapper) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
start := time.Now()
err := w.Client.Publish(ctx, p, opts...)
observe(w.metrics, "publisher", p.Topic(), err, start)
return err
}
// observe records a single request into the counter and histogram.
func observe(m *metrics, service, endpoint string, err error, start time.Time) {
st := status(err)
m.requestTotal.WithLabelValues(service, endpoint, st).Inc()
m.requestDuration.WithLabelValues(service, endpoint, st).Observe(time.Since(start).Seconds())
}
@@ -0,0 +1,206 @@
package prometheus
import (
"context"
"errors"
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"go-micro.dev/v6/client"
"go-micro.dev/v6/codec"
"go-micro.dev/v6/registry"
"go-micro.dev/v6/server"
)
// mockServerRequest is a minimal implementation of server.Request.
type mockServerRequest struct {
service string
endpoint string
}
func (r *mockServerRequest) Service() string { return r.service }
func (r *mockServerRequest) Method() string { return r.endpoint }
func (r *mockServerRequest) Endpoint() string { return r.endpoint }
func (r *mockServerRequest) ContentType() string { return "" }
func (r *mockServerRequest) Header() map[string]string { return nil }
func (r *mockServerRequest) Body() interface{} { return nil }
func (r *mockServerRequest) Read() ([]byte, error) { return nil, nil }
func (r *mockServerRequest) Codec() codec.Reader { return nil }
func (r *mockServerRequest) Stream() bool { return false }
// mockMessage implements server.Message for subscriber tests.
type mockMessage struct {
topic string
}
func (m *mockMessage) Topic() string { return m.topic }
func (m *mockMessage) Payload() interface{} { return nil }
func (m *mockMessage) ContentType() string { return "" }
func (m *mockMessage) Header() map[string]string { return nil }
func (m *mockMessage) Body() []byte { return nil }
func (m *mockMessage) Codec() codec.Reader { return nil }
// mockClientRequest is a minimal implementation of client.Request.
type mockClientRequest struct {
service string
endpoint string
}
func (r *mockClientRequest) Service() string { return r.service }
func (r *mockClientRequest) Method() string { return r.endpoint }
func (r *mockClientRequest) Endpoint() string { return r.endpoint }
func (r *mockClientRequest) ContentType() string { return "" }
func (r *mockClientRequest) Body() interface{} { return nil }
func (r *mockClientRequest) Codec() codec.Writer { return nil }
func (r *mockClientRequest) Stream() bool { return false }
// isolatedOpts returns wrapper options pinned to a fresh registry so each
// test starts with its own counters. We also vary Name so cached metrics
// bundles don't bleed between tests.
func isolatedOpts(name string) []Option {
reg := prometheus.NewRegistry()
return []Option{ServiceName(name), Registerer(reg)}
}
func counterValue(t *testing.T, vec *prometheus.CounterVec, labels ...string) float64 {
t.Helper()
c, err := vec.GetMetricWithLabelValues(labels...)
if err != nil {
t.Fatalf("GetMetricWithLabelValues: %v", err)
}
var m dto.Metric
if err := c.Write(&m); err != nil {
t.Fatalf("Write: %v", err)
}
return m.GetCounter().GetValue()
}
func histogramCount(t *testing.T, vec *prometheus.HistogramVec, labels ...string) uint64 {
t.Helper()
obs, err := vec.GetMetricWithLabelValues(labels...)
if err != nil {
t.Fatalf("GetMetricWithLabelValues: %v", err)
}
h, ok := obs.(prometheus.Histogram)
if !ok {
t.Fatalf("expected Histogram, got %T", obs)
}
var m dto.Metric
if err := h.Write(&m); err != nil {
t.Fatalf("Write: %v", err)
}
return m.GetHistogram().GetSampleCount()
}
func TestHandlerWrapperSuccess(t *testing.T) {
opts := isolatedOpts("test_handler_success")
wrap := NewHandlerWrapper(opts...)
called := false
handler := wrap(func(ctx context.Context, req server.Request, rsp interface{}) error {
called = true
return nil
})
err := handler(context.Background(), &mockServerRequest{service: "svc", endpoint: "Foo.Bar"}, nil)
if err != nil {
t.Fatalf("handler returned error: %v", err)
}
if !called {
t.Fatal("inner handler was not called")
}
m := getMetrics(newOptions(opts...))
if got := counterValue(t, m.requestTotal, "svc", "Foo.Bar", "success"); got != 1 {
t.Errorf("success counter = %v, want 1", got)
}
if got := histogramCount(t, m.requestDuration, "svc", "Foo.Bar", "success"); got != 1 {
t.Errorf("histogram count = %v, want 1", got)
}
}
func TestHandlerWrapperFailure(t *testing.T) {
opts := isolatedOpts("test_handler_failure")
wrap := NewHandlerWrapper(opts...)
boom := errors.New("boom")
handler := wrap(func(ctx context.Context, req server.Request, rsp interface{}) error {
return boom
})
err := handler(context.Background(), &mockServerRequest{service: "svc", endpoint: "Foo.Bar"}, nil)
if !errors.Is(err, boom) {
t.Fatalf("error not propagated, got %v", err)
}
m := getMetrics(newOptions(opts...))
if got := counterValue(t, m.requestTotal, "svc", "Foo.Bar", "fail"); got != 1 {
t.Errorf("fail counter = %v, want 1", got)
}
}
func TestSubscriberWrapper(t *testing.T) {
opts := isolatedOpts("test_subscriber")
wrap := NewSubscriberWrapper(opts...)
sub := wrap(func(ctx context.Context, msg server.Message) error {
return nil
})
if err := sub(context.Background(), &mockMessage{topic: "events"}); err != nil {
t.Fatalf("subscriber returned error: %v", err)
}
m := getMetrics(newOptions(opts...))
if got := counterValue(t, m.requestTotal, "subscriber", "events", "success"); got != 1 {
t.Errorf("subscriber counter = %v, want 1", got)
}
}
func TestCallWrapperSuccess(t *testing.T) {
opts := isolatedOpts("test_call")
wrap := NewCallWrapper(opts...)
cf := wrap(func(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
return nil
})
err := cf(context.Background(), &registry.Node{}, &mockClientRequest{service: "svc", endpoint: "Foo.Bar"}, nil, client.CallOptions{})
if err != nil {
t.Fatalf("call returned error: %v", err)
}
m := getMetrics(newOptions(opts...))
if got := counterValue(t, m.requestTotal, "svc", "Foo.Bar", "success"); got != 1 {
t.Errorf("call counter = %v, want 1", got)
}
}
func TestRegisterAlreadyRegistered(t *testing.T) {
// Creating two wrappers against the same Registerer with the same
// options must not panic, and the second one must reuse the already
// registered collector.
reg := prometheus.NewRegistry()
opts := []Option{ServiceName("test_dup"), Registerer(reg)}
_ = NewHandlerWrapper(opts...)
_ = NewHandlerWrapper(opts...)
// After resetting the cache we must still not panic, which proves the
// AlreadyRegisteredError branch in register() is exercised.
metricsMu.Lock()
delete(metricsCache, "test_dup\x00\x00")
metricsMu.Unlock()
_ = NewHandlerWrapper(opts...)
}
func TestStatusHelper(t *testing.T) {
if status(nil) != "success" {
t.Error("nil error should yield success")
}
if status(errors.New("x")) != "fail" {
t.Error("non-nil error should yield fail")
}
}