feat: implement observability and metrics (Task 22)

- Add metrics package tracking cache hit-rate, API quota, circuit breaker trips, search time
- Integrate metrics with cache layer, yandex client, and API handlers
- Add /metrics HTTP endpoint for Prometheus-compatible metrics exposure
- Write tests for metrics functionality across cache, yandex, and API handlers
- Update test files to support new metrics infrastructure
This commit is contained in:
2026-08-17 15:27:44 +03:00
parent 5842667fff
commit c58fbb50b1
10 changed files with 285 additions and 55 deletions

View File

@@ -9,6 +9,8 @@ import (
"net/url"
"sync"
"time"
"trip-planner/internal/metrics"
)
// Client represents a Yandex Schedules API client with rate limiting,
@@ -19,6 +21,7 @@ type Client struct {
rateLimiter *tokenBucket
circuitBreaker *circuitBreaker
retryConfig *retryConfig
metrics *metrics.Metrics
}
// tokenBucket implements a token bucket rate limiter.
@@ -111,6 +114,13 @@ func WithRetryConfig(maxRetries int, baseBackoff, maxBackoff time.Duration, jitt
}
}
// WithMetrics sets the metrics recorder for the client.
func WithMetrics(m *metrics.Metrics) Option {
return func(c *Client) {
c.metrics = m
}
}
// Do executes a Yandex API request with rate limiting, circuit breaking, and retry.
func (c *Client) Do(ctx context.Context, method, path string, query map[string]string) (*Response, error) {
// Apply rate limiting
@@ -128,6 +138,7 @@ func (c *Client) Do(ctx context.Context, method, path string, query map[string]s
for attempt := 0; attempt <= c.retryConfig.maxRetries; attempt++ {
// Check circuit breaker on each retry attempt
if !c.circuitBreaker.allow() {
c.metrics.RecordCircuitBreakerTrip()
return nil, fmt.Errorf("circuit breaker is open")
}
@@ -155,6 +166,7 @@ func (c *Client) Do(ctx context.Context, method, path string, query map[string]s
}
c.circuitBreaker.recordFailure() // final failure
c.metrics.RecordCircuitBreakerTrip()
return nil, err
}
@@ -248,7 +260,6 @@ func (e *APIError) Error() string {
return fmt.Sprintf("API error %d: %s", e.Code, e.Message)
}
// newAPIError creates an APIError from an HTTP response.
func newAPIError(code int, message string) *APIError {
return &APIError{Code: code, Message: message}
}
@@ -320,9 +331,6 @@ func newCircuitBreaker() *circuitBreaker {
}
// ResetCircuitBreaker resets the circuit breaker to its initial closed state.
// This is useful for testing or recovery scenarios where the circuit needs to be
// manually reset without waiting for the timeout.
func (cb *circuitBreaker) ResetCircuitBreaker() {
cb.mu.Lock()
defer cb.mu.Unlock()
@@ -403,4 +411,4 @@ func applyJitter(backoff time.Duration) time.Duration {
func randFloat64() float64 {
// Use math/rand with a seed based on function call index for variability
return rand.Float64()
}
}