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:
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"trip-planner/internal/metrics"
|
||||
"trip-planner/internal/airports"
|
||||
"trip-planner/internal/routing"
|
||||
"trip-planner/internal/storage"
|
||||
@@ -54,7 +55,7 @@ func newMockHandlerContext() *HandlerContext {
|
||||
// Create Yandex client
|
||||
yandexClient := yandex.NewClient("test-key")
|
||||
|
||||
return NewHandlerContext(redisClient, router, yandexClient)
|
||||
return NewHandlerContext(redisClient, router, yandexClient, metrics.New())
|
||||
}
|
||||
|
||||
func TestHandlerCityAutocomplete(t *testing.T) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"trip-planner/internal/cache"
|
||||
"trip-planner/internal/metrics"
|
||||
"trip-planner/internal/routing"
|
||||
"trip-planner/internal/yandex"
|
||||
)
|
||||
@@ -15,9 +16,10 @@ import (
|
||||
func main() {
|
||||
redisClient := initRedis()
|
||||
router := routing.NewGraph()
|
||||
yandexClient := yandex.NewClient("default-key")
|
||||
m := metrics.New()
|
||||
yandexClient := yandex.NewClient("default-key", yandex.WithMetrics(m))
|
||||
|
||||
handlerCtx := NewHandlerContext(redisClient, router, yandexClient)
|
||||
handlerCtx := NewHandlerContext(redisClient, router, yandexClient, m)
|
||||
|
||||
// Cache warm-up: load city directory into Redis cache
|
||||
// ensures the API functions correctly on cold start and after cache expiry
|
||||
@@ -29,6 +31,7 @@ func main() {
|
||||
http.HandleFunc("/v1/routes/", makeHandler(RouteGeoJSON, handlerCtx))
|
||||
http.HandleFunc("/v1/stations/", makeHandler(StationStatus, handlerCtx))
|
||||
http.HandleFunc("/internal/admin/stations/", makeHandler(AdminStationStatus, handlerCtx))
|
||||
http.HandleFunc("/metrics", makeHandler(MetricsHandler, handlerCtx))
|
||||
|
||||
log.Println("Trip Planner API starting on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
|
||||
Reference in New Issue
Block a user