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:
53
internal/cache/store.go
vendored
53
internal/cache/store.go
vendored
@@ -8,6 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"trip-planner/internal/metrics"
|
||||
)
|
||||
|
||||
// CacheKey defines the structure for cache keys used throughout the application.
|
||||
@@ -39,22 +41,25 @@ type Cache interface {
|
||||
// redisClient is a wrapper around go-redis client for dependency injection.
|
||||
type redisClient struct {
|
||||
client *redis.Client
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// NewRedisClient creates a new Redis client wrapper.
|
||||
func NewRedisClient(client *redis.Client) *redisClient {
|
||||
return &redisClient{client: client}
|
||||
func NewRedisClient(client *redis.Client, m *metrics.Metrics) *redisClient {
|
||||
return &redisClient{client: client, metrics: m}
|
||||
}
|
||||
|
||||
// Get retrieves a value from cache by key.
|
||||
func (r *redisClient) Get(ctx context.Context, key *CacheKey) ([]byte, error) {
|
||||
val, err := r.client.Get(ctx, keyString(key)).Bytes()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
r.metrics.RecordCacheMiss("cache") // record cache miss at redis client level
|
||||
return nil, nil // cache miss
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache get: %w", err)
|
||||
}
|
||||
r.metrics.RecordCacheHit("cache") // record cache hit at redis client level
|
||||
return val, nil
|
||||
}
|
||||
|
||||
@@ -125,9 +130,9 @@ type cacheStore struct {
|
||||
}
|
||||
|
||||
// NewCacheStore creates a new cache store with the given Redis client.
|
||||
func NewCacheStore(client *redis.Client) Cache {
|
||||
func NewCacheStore(client *redis.Client, m *metrics.Metrics) Cache {
|
||||
return &cacheStore{
|
||||
redisClient: NewRedisClient(client),
|
||||
redisClient: NewRedisClient(client, m),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,12 +166,13 @@ func GetSearchKey(from, to, date string) *CacheKey {
|
||||
// CacheAside represents the cache-aside pattern implementation.
|
||||
// It follows the pattern: Redis → miss → Postgres/API → write-back to Redis.
|
||||
type CacheAside struct {
|
||||
store Cache
|
||||
store Cache
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// NewCacheAside creates a new CacheAside instance.
|
||||
func NewCacheAside(store Cache) *CacheAside {
|
||||
return &CacheAside{store: store}
|
||||
func NewCacheAside(store Cache, m *metrics.Metrics) *CacheAside {
|
||||
return &CacheAside{store: store, metrics: m}
|
||||
}
|
||||
|
||||
// GetOrSetFuncPattern is a generic pattern for cache-aside operations.
|
||||
@@ -204,6 +210,7 @@ func (c *CacheAside) GetStation(ctx context.Context, key *CacheKey, fetch func()
|
||||
|
||||
// GetSearch retrieves search results from cache, falling back to the provided fetch function.
|
||||
// Uses appropriate TTL based on whether the date is near-term or far-term.
|
||||
// Records cache hit/miss metrics.
|
||||
func (c *CacheAside) GetSearch(ctx context.Context, key *CacheKey, fetch func() ([]byte, error), isFarTerm bool) ([]byte, error) {
|
||||
var ttl time.Duration
|
||||
if isFarTerm {
|
||||
@@ -211,7 +218,33 @@ func (c *CacheAside) GetSearch(ctx context.Context, key *CacheKey, fetch func()
|
||||
} else {
|
||||
ttl = SearchNearTermTTL
|
||||
}
|
||||
return c.GetOrSetFuncPattern(ctx, key, fetch, ttl)
|
||||
|
||||
// Try cache first
|
||||
data, err := c.store.Get(ctx, key)
|
||||
if err == nil && data != nil {
|
||||
c.metrics.RecordCacheHit("search") // cache hit
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Cache miss: fetch from backend
|
||||
if errors.Is(err, redis.Nil) {
|
||||
c.metrics.RecordCacheMiss("search") // record search cache miss
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fetch from backend
|
||||
data, err = fetch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write back to cache
|
||||
if err := c.store.Set(ctx, key, data, ttl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// InvalidateCity removes a city entry from cache.
|
||||
@@ -238,11 +271,13 @@ func (c *CacheAside) Delete(ctx context.Context, key *CacheKey) error {
|
||||
func (c *CacheAside) Get(ctx context.Context, key *CacheKey) ([]byte, error) {
|
||||
val, err := c.store.Get(ctx, key)
|
||||
if errors.Is(err, redis.Nil) {
|
||||
c.metrics.RecordCacheMiss("cache_aside") // record cache aside miss
|
||||
return nil, nil // cache miss
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache get: %w", err)
|
||||
}
|
||||
c.metrics.RecordCacheHit("cache_aside") // record cache aside hit
|
||||
return val, nil
|
||||
}
|
||||
|
||||
@@ -271,4 +306,4 @@ func (c *CacheAside) Increment(ctx context.Context, key *CacheKey) (int64, error
|
||||
// Decrement decrements a counter key.
|
||||
func (c *CacheAside) Decrement(ctx context.Context, key *CacheKey) (int64, error) {
|
||||
return c.store.Decrement(ctx, key)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user