mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-06-05 22:23:50 +00:00
562 lines
12 KiB
Go
562 lines
12 KiB
Go
package metrics
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCounter(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ops func(c *Counter)
|
|
expected int64
|
|
}{
|
|
{
|
|
name: "initial value is zero",
|
|
ops: func(c *Counter) {},
|
|
expected: 0,
|
|
},
|
|
{
|
|
name: "single inc",
|
|
ops: func(c *Counter) {
|
|
c.Inc()
|
|
},
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "multiple inc",
|
|
ops: func(c *Counter) {
|
|
c.Inc()
|
|
c.Inc()
|
|
c.Inc()
|
|
},
|
|
expected: 3,
|
|
},
|
|
{
|
|
name: "add positive",
|
|
ops: func(c *Counter) {
|
|
c.Add(10)
|
|
},
|
|
expected: 10,
|
|
},
|
|
{
|
|
name: "mixed operations",
|
|
ops: func(c *Counter) {
|
|
c.Inc()
|
|
c.Add(5)
|
|
c.Inc()
|
|
},
|
|
expected: 7,
|
|
},
|
|
{
|
|
name: "reset",
|
|
ops: func(c *Counter) {
|
|
c.Add(100)
|
|
c.Reset()
|
|
},
|
|
expected: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := NewCounter("test_counter", "test help", nil)
|
|
tt.ops(c)
|
|
if got := c.Value(); got != tt.expected {
|
|
t.Errorf("Counter.Value() = %d, want %d", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCounterConcurrency(t *testing.T) {
|
|
c := NewCounter("concurrent_counter", "test", nil)
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 100
|
|
incsPerGoroutine := 1000
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < incsPerGoroutine; j++ {
|
|
c.Inc()
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
expected := int64(numGoroutines * incsPerGoroutine)
|
|
if got := c.Value(); got != expected {
|
|
t.Errorf("Counter.Value() = %d, want %d after concurrent increments", got, expected)
|
|
}
|
|
}
|
|
|
|
func TestGauge(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ops func(g *Gauge)
|
|
expected int64
|
|
}{
|
|
{
|
|
name: "initial value is zero",
|
|
ops: func(g *Gauge) {},
|
|
expected: 0,
|
|
},
|
|
{
|
|
name: "set value",
|
|
ops: func(g *Gauge) {
|
|
g.Set(42)
|
|
},
|
|
expected: 42,
|
|
},
|
|
{
|
|
name: "inc",
|
|
ops: func(g *Gauge) {
|
|
g.Inc()
|
|
g.Inc()
|
|
},
|
|
expected: 2,
|
|
},
|
|
{
|
|
name: "dec",
|
|
ops: func(g *Gauge) {
|
|
g.Set(10)
|
|
g.Dec()
|
|
g.Dec()
|
|
},
|
|
expected: 8,
|
|
},
|
|
{
|
|
name: "add positive",
|
|
ops: func(g *Gauge) {
|
|
g.Add(5)
|
|
},
|
|
expected: 5,
|
|
},
|
|
{
|
|
name: "add negative",
|
|
ops: func(g *Gauge) {
|
|
g.Set(10)
|
|
g.Add(-3)
|
|
},
|
|
expected: 7,
|
|
},
|
|
{
|
|
name: "mixed operations",
|
|
ops: func(g *Gauge) {
|
|
g.Set(100)
|
|
g.Inc()
|
|
g.Dec()
|
|
g.Add(-50)
|
|
},
|
|
expected: 50,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewGauge("test_gauge", "test help", nil)
|
|
tt.ops(g)
|
|
if got := g.Value(); got != tt.expected {
|
|
t.Errorf("Gauge.Value() = %d, want %d", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHistogram(t *testing.T) {
|
|
t.Run("default buckets", func(t *testing.T) {
|
|
h := NewHistogram("test_histogram", "test", nil, nil)
|
|
if len(h.buckets) != len(DefaultDurationBuckets) {
|
|
t.Errorf("expected default buckets, got %d buckets", len(h.buckets))
|
|
}
|
|
})
|
|
|
|
t.Run("custom buckets", func(t *testing.T) {
|
|
buckets := []float64{1.0, 5.0, 10.0}
|
|
h := NewHistogram("test_histogram", "test", nil, buckets)
|
|
if len(h.buckets) != 3 {
|
|
t.Errorf("expected 3 buckets, got %d", len(h.buckets))
|
|
}
|
|
})
|
|
|
|
t.Run("buckets are sorted", func(t *testing.T) {
|
|
buckets := []float64{10.0, 1.0, 5.0}
|
|
h := NewHistogram("test_histogram", "test", nil, buckets)
|
|
if h.buckets[0] != 1.0 || h.buckets[1] != 5.0 || h.buckets[2] != 10.0 {
|
|
t.Errorf("buckets not sorted: %v", h.buckets)
|
|
}
|
|
})
|
|
|
|
t.Run("observe values", func(t *testing.T) {
|
|
h := NewHistogram("test_histogram", "test", nil, []float64{1.0, 5.0, 10.0})
|
|
|
|
h.Observe(0.5) // goes to bucket 0 (<=1.0)
|
|
h.Observe(3.0) // goes to bucket 1 (<=5.0)
|
|
h.Observe(7.0) // goes to bucket 2 (<=10.0)
|
|
h.Observe(15.0) // goes to +Inf bucket
|
|
|
|
if h.Count() != 4 {
|
|
t.Errorf("expected count 4, got %d", h.Count())
|
|
}
|
|
})
|
|
|
|
t.Run("observe duration", func(t *testing.T) {
|
|
h := NewHistogram("test_histogram", "test", nil, []float64{0.001, 0.01, 0.1})
|
|
|
|
h.ObserveDuration(500 * time.Microsecond) // 0.0005s, goes to bucket 0
|
|
h.ObserveDuration(5 * time.Millisecond) // 0.005s, goes to bucket 1
|
|
|
|
if h.Count() != 2 {
|
|
t.Errorf("expected count 2, got %d", h.Count())
|
|
}
|
|
})
|
|
|
|
t.Run("sum tracking", func(t *testing.T) {
|
|
h := NewHistogram("test_histogram", "test", nil, []float64{1.0, 5.0, 10.0})
|
|
|
|
h.Observe(1.0)
|
|
h.Observe(2.0)
|
|
h.Observe(3.0)
|
|
|
|
expectedSum := 6.0
|
|
if got := h.Sum(); got != expectedSum {
|
|
t.Errorf("expected sum %f, got %f", expectedSum, got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRegistry(t *testing.T) {
|
|
t.Run("counter registration", func(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
c1 := r.Counter("test_counter", "help", nil)
|
|
c2 := r.Counter("test_counter", "help", nil)
|
|
|
|
if c1 != c2 {
|
|
t.Error("expected same counter instance for same name")
|
|
}
|
|
})
|
|
|
|
t.Run("counter with labels", func(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
labels1 := map[string]string{"method": "get"}
|
|
labels2 := map[string]string{"method": "post"}
|
|
|
|
c1 := r.Counter("http_requests", "help", labels1)
|
|
c2 := r.Counter("http_requests", "help", labels2)
|
|
|
|
if c1 == c2 {
|
|
t.Error("expected different counter instances for different labels")
|
|
}
|
|
})
|
|
|
|
t.Run("gauge registration", func(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
g1 := r.Gauge("test_gauge", "help", nil)
|
|
g2 := r.Gauge("test_gauge", "help", nil)
|
|
|
|
if g1 != g2 {
|
|
t.Error("expected same gauge instance for same name")
|
|
}
|
|
})
|
|
|
|
t.Run("histogram registration", func(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
h1 := r.Histogram("test_histogram", "help", nil, nil)
|
|
h2 := r.Histogram("test_histogram", "help", nil, nil)
|
|
|
|
if h1 != h2 {
|
|
t.Error("expected same histogram instance for same name")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRegistryConcurrency(t *testing.T) {
|
|
r := NewRegistry()
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 100
|
|
|
|
// Concurrent access to registry
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
c := r.Counter("concurrent_test", "test", nil)
|
|
c.Inc()
|
|
|
|
g := r.Gauge("concurrent_gauge", "test", nil)
|
|
g.Inc()
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
c := r.Counter("concurrent_test", "test", nil)
|
|
if c.Value() != int64(numGoroutines) {
|
|
t.Errorf("expected counter value %d, got %d", numGoroutines, c.Value())
|
|
}
|
|
}
|
|
|
|
func TestRegistryExpose(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
// Add some metrics
|
|
c := r.Counter("test_requests_total", "Total requests", nil)
|
|
c.Add(42)
|
|
|
|
g := r.Gauge("test_connections", "Active connections", nil)
|
|
g.Set(10)
|
|
|
|
h := r.Histogram("test_duration_seconds", "Request duration", nil, []float64{0.1, 0.5, 1.0})
|
|
h.Observe(0.05)
|
|
h.Observe(0.3)
|
|
h.Observe(0.8)
|
|
|
|
output := r.Expose()
|
|
|
|
// Check counter output
|
|
if !strings.Contains(output, "# TYPE test_requests_total counter") {
|
|
t.Error("expected counter type in output")
|
|
}
|
|
if !strings.Contains(output, "test_requests_total 42") {
|
|
t.Error("expected counter value in output")
|
|
}
|
|
|
|
// Check gauge output
|
|
if !strings.Contains(output, "# TYPE test_connections gauge") {
|
|
t.Error("expected gauge type in output")
|
|
}
|
|
if !strings.Contains(output, "test_connections 10") {
|
|
t.Error("expected gauge value in output")
|
|
}
|
|
|
|
// Check histogram output
|
|
if !strings.Contains(output, "# TYPE test_duration_seconds histogram") {
|
|
t.Error("expected histogram type in output")
|
|
}
|
|
if !strings.Contains(output, "test_duration_seconds_bucket") {
|
|
t.Error("expected histogram buckets in output")
|
|
}
|
|
if !strings.Contains(output, "test_duration_seconds_sum") {
|
|
t.Error("expected histogram sum in output")
|
|
}
|
|
if !strings.Contains(output, "test_duration_seconds_count") {
|
|
t.Error("expected histogram count in output")
|
|
}
|
|
}
|
|
|
|
func TestRegistryReset(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
c := r.Counter("test_counter", "test", nil)
|
|
c.Add(100)
|
|
|
|
g := r.Gauge("test_gauge", "test", nil)
|
|
g.Set(50)
|
|
|
|
r.Reset()
|
|
|
|
if c.Value() != 0 {
|
|
t.Errorf("expected counter reset to 0, got %d", c.Value())
|
|
}
|
|
if g.Value() != 0 {
|
|
t.Errorf("expected gauge reset to 0, got %d", g.Value())
|
|
}
|
|
}
|
|
|
|
func TestServerMetrics(t *testing.T) {
|
|
t.Run("creation", func(t *testing.T) {
|
|
m := NewServerMetrics()
|
|
|
|
if m.RequestsTotal == nil {
|
|
t.Error("RequestsTotal should not be nil")
|
|
}
|
|
if m.RequestErrors == nil {
|
|
t.Error("RequestErrors should not be nil")
|
|
}
|
|
if m.RequestDuration == nil {
|
|
t.Error("RequestDuration should not be nil")
|
|
}
|
|
if m.CacheHits == nil {
|
|
t.Error("CacheHits should not be nil")
|
|
}
|
|
if m.CacheMisses == nil {
|
|
t.Error("CacheMisses should not be nil")
|
|
}
|
|
if m.ActiveLSPServers == nil {
|
|
t.Error("ActiveLSPServers should not be nil")
|
|
}
|
|
if m.ParseDuration == nil {
|
|
t.Error("ParseDuration should not be nil")
|
|
}
|
|
if m.ParseErrors == nil {
|
|
t.Error("ParseErrors should not be nil")
|
|
}
|
|
})
|
|
|
|
t.Run("record request success", func(t *testing.T) {
|
|
m := NewServerMetrics()
|
|
|
|
m.RecordRequest(100*time.Millisecond, nil)
|
|
|
|
if m.RequestsTotal.Value() != 1 {
|
|
t.Errorf("expected RequestsTotal 1, got %d", m.RequestsTotal.Value())
|
|
}
|
|
if m.RequestErrors.Value() != 0 {
|
|
t.Errorf("expected RequestErrors 0, got %d", m.RequestErrors.Value())
|
|
}
|
|
if m.RequestDuration.Count() != 1 {
|
|
t.Errorf("expected RequestDuration count 1, got %d", m.RequestDuration.Count())
|
|
}
|
|
})
|
|
|
|
t.Run("record request error", func(t *testing.T) {
|
|
m := NewServerMetrics()
|
|
|
|
m.RecordRequest(50*time.Millisecond, &testError{})
|
|
|
|
if m.RequestsTotal.Value() != 1 {
|
|
t.Errorf("expected RequestsTotal 1, got %d", m.RequestsTotal.Value())
|
|
}
|
|
if m.RequestErrors.Value() != 1 {
|
|
t.Errorf("expected RequestErrors 1, got %d", m.RequestErrors.Value())
|
|
}
|
|
})
|
|
|
|
t.Run("record parse", func(t *testing.T) {
|
|
m := NewServerMetrics()
|
|
|
|
m.RecordParse(10*time.Millisecond, nil)
|
|
m.RecordParse(5*time.Millisecond, &testError{})
|
|
|
|
if m.ParseDuration.Count() != 2 {
|
|
t.Errorf("expected ParseDuration count 2, got %d", m.ParseDuration.Count())
|
|
}
|
|
if m.ParseErrors.Value() != 1 {
|
|
t.Errorf("expected ParseErrors 1, got %d", m.ParseErrors.Value())
|
|
}
|
|
})
|
|
|
|
t.Run("record cache", func(t *testing.T) {
|
|
m := NewServerMetrics()
|
|
|
|
m.RecordCacheHit()
|
|
m.RecordCacheHit()
|
|
m.RecordCacheMiss()
|
|
|
|
if m.CacheHits.Value() != 2 {
|
|
t.Errorf("expected CacheHits 2, got %d", m.CacheHits.Value())
|
|
}
|
|
if m.CacheMisses.Value() != 1 {
|
|
t.Errorf("expected CacheMisses 1, got %d", m.CacheMisses.Value())
|
|
}
|
|
})
|
|
|
|
t.Run("set active LSP servers", func(t *testing.T) {
|
|
m := NewServerMetrics()
|
|
|
|
m.SetActiveLSPServers(5)
|
|
if m.ActiveLSPServers.Value() != 5 {
|
|
t.Errorf("expected ActiveLSPServers 5, got %d", m.ActiveLSPServers.Value())
|
|
}
|
|
|
|
m.SetActiveLSPServers(3)
|
|
if m.ActiveLSPServers.Value() != 3 {
|
|
t.Errorf("expected ActiveLSPServers 3, got %d", m.ActiveLSPServers.Value())
|
|
}
|
|
})
|
|
|
|
t.Run("expose", func(t *testing.T) {
|
|
m := NewServerMetrics()
|
|
m.RecordRequest(100*time.Millisecond, nil)
|
|
|
|
output := m.Expose()
|
|
|
|
if !strings.Contains(output, "mcp_requests_total") {
|
|
t.Error("expected mcp_requests_total in output")
|
|
}
|
|
if !strings.Contains(output, "mcp_request_duration_seconds") {
|
|
t.Error("expected mcp_request_duration_seconds in output")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMetricKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
labels map[string]string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "no labels",
|
|
labels: nil,
|
|
expected: "test_metric",
|
|
},
|
|
{
|
|
name: "single label",
|
|
labels: map[string]string{"method": "get"},
|
|
expected: `test_metric{method="get"}`,
|
|
},
|
|
{
|
|
name: "multiple labels sorted",
|
|
labels: map[string]string{"method": "get", "code": "200"},
|
|
expected: `test_metric{code="200",method="get"}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := metricKey("test_metric", tt.labels)
|
|
if got != tt.expected {
|
|
t.Errorf("metricKey() = %q, want %q", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatLabels(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
labels map[string]string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "no labels",
|
|
labels: nil,
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "single label",
|
|
labels: map[string]string{"method": "get"},
|
|
expected: `{method="get"}`,
|
|
},
|
|
{
|
|
name: "multiple labels sorted",
|
|
labels: map[string]string{"method": "get", "code": "200"},
|
|
expected: `{code="200",method="get"}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := formatLabels(tt.labels)
|
|
if got != tt.expected {
|
|
t.Errorf("formatLabels() = %q, want %q", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// testError is a simple error type for testing
|
|
type testError struct{}
|
|
|
|
func (e *testError) Error() string { return "test error" }
|