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

@@ -5,10 +5,12 @@ import (
"fmt"
"net/http"
"strings"
"time"
"github.com/go-redis/redis/v8"
"trip-planner/internal/cache"
"trip-planner/internal/metrics"
"trip-planner/internal/routing"
"trip-planner/internal/storage"
"trip-planner/internal/yandex"
@@ -16,12 +18,14 @@ import (
// HandlerContext holds the dependencies for API handlers.
type HandlerContext struct {
Cache cache.Cache
Redis *redis.Client
Router *routing.Graph
Yandex *yandex.Client
SearchCache *routing.SearchCacheService
Preferences *cache.Preferences
Cache cache.Cache
Redis *redis.Client
Router *routing.Graph
Yandex *yandex.Client
SearchCache *routing.SearchCacheService
Preferences *cache.Preferences
Metrics *metrics.Metrics
SearchStart time.Time
}
// stationStatusResponse represents the response for station status.
@@ -183,6 +187,9 @@ func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
return
}
// Record search start time
start := time.Now()
// Read ranking mode from query parameters (for UI controls)
rankingMode := r.URL.Query().Get("ranking_mode")
@@ -201,6 +208,10 @@ func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
// Run Pareto-optimal route search using the graph
results := hc.Router.FindRoutesPareto(req.FromCityID, req.ToCityID, opts)
// Record search duration
duration := time.Since(start).Nanoseconds()
hc.Metrics.RecordSearch(duration)
// Build response routes
routeResponses := make([]routeSearchRoute, 0, len(results))
for _, route := range results {
@@ -598,15 +609,23 @@ func AddSearchHistory(hc *HandlerContext, w http.ResponseWriter, r *http.Request
})
}
// metricsHandler handles GET /metrics and returns all observability metrics as JSON.
func MetricsHandler(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(hc.Metrics.GetMetricsJSON())
}
// NewHandlerContext creates a new HandlerContext with initialized services.
func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client) *HandlerContext {
cacheStore := cache.NewCacheStore(redisClient)
func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client, m *metrics.Metrics) *HandlerContext {
cacheStore := cache.NewCacheStore(redisClient, m)
return &HandlerContext{
Cache: cacheStore,
Redis: redisClient,
Router: router,
Yandex: yandex,
SearchCache: routing.NewSearchCacheService(cache.NewCacheAside(cacheStore), yandex),
Cache: cacheStore,
Redis: redisClient,
Router: router,
Yandex: yandex,
SearchCache: routing.NewSearchCacheService(cacheStore, yandex, m),
Preferences: cache.NewPreferences(cacheStore),
Metrics: m,
SearchStart: time.Now(),
}
}