registry/internal/telemetry/metrics_test.go
pree-dew d237fc90f8
Add OpenTelemetry metrics instrumentation (#290)
## Motivation and Context
- It is important to have basic observability in place so that
availability issues can be figured out easily

## How Has This Been Tested?
- Registration of metrics is tested
- Prometheus endpoint `/metrics` is included in test cases to test the
metrics exposed
- Test locally to understand any regression.

## Breaking Changes
- No

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [ ] I have added or updated documentation as needed

## Additional context
- Telemetry implementation is currently done by keeping metrics in mind
as of now, other types of telemetry can be added on top of it later.
- Instrumentation takes care of these problems already:
- Cardinality control by limiting latency buckets and not including
labels that grows with throughput.
  - Latency buckets are added as per SLA guidelines
  - Each metric prefixed with `mcp` to easily identify.
- Pulling mechanism is used since the idea is to scrape metrics via an
agent like vmagent, prometheus agent or otel-collector.
- Metrics follow standard approach of defining counters, histogram and
gauges as per opentelemetry guidelines.
2025-08-21 14:23:37 +01:00

84 lines
2 KiB
Go

package telemetry_test
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/exporters/prometheus"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/noop"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
"github.com/modelcontextprotocol/registry/internal/telemetry"
)
func TestNewMetrics(t *testing.T) {
tests := []struct {
name string
meter metric.Meter
expectedMetric string
expectedError error
}{
{
name: "Valid Meter",
meter: noop.NewMeterProvider().Meter("test"),
expectedMetric: "http_requests_total",
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metrics, err := telemetry.NewMetrics(tt.meter)
// Ensure the metric is registered correctly
assert.NoError(t, err)
assert.NotNil(t, metrics)
assert.NotNil(t, metrics.Requests)
})
}
}
func TestNewPrometheusMeterProvider(t *testing.T) {
tests := []struct {
name string
wantErr bool
mockPrometheus func() (*prometheus.Exporter, error)
expectedError error
}{
{
name: "Successful creation of meter provider",
wantErr: false,
mockPrometheus: func() (*prometheus.Exporter, error) {
return &prometheus.Exporter{
Reader: sdkmetric.NewManualReader(),
}, nil
},
},
{
name: "Error on resource creation",
wantErr: true,
expectedError: errors.New("error creating prometheus resource"),
mockPrometheus: func() (*prometheus.Exporter, error) {
return nil, errors.New("error creating prometheus resource")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res := resource.NewSchemaless() // Use an empty resource for testing.
exp, _ := tt.mockPrometheus()
mp, err := telemetry.NewPrometheusMeterProvider(res, exp)
if tt.wantErr {
assert.NotNil(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, mp)
}
})
}
}