diff --git a/apps/guardrails-service/container/src/go.mod b/apps/guardrails-service/container/src/go.mod index f6366412ce..71b800f6fc 100644 --- a/apps/guardrails-service/container/src/go.mod +++ b/apps/guardrails-service/container/src/go.mod @@ -3,7 +3,7 @@ module github.com/akto-api-security/guardrails-service go 1.25.0 require ( - github.com/akto-api-security/akto-endpoint-shield v0.0.0-20260713080310-159f6158fffd + github.com/akto-api-security/akto-endpoint-shield v0.0.0-20260715100314-2175c200e52e github.com/gin-gonic/gin v1.12.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/redis/go-redis/v9 v9.20.0 @@ -132,6 +132,6 @@ require ( ) replace ( - github.com/akto-api-security/akto-endpoint-shield => github.com/akto-api-security/akto-gateway/mcp-endpoint-shield v0.0.0-20260714070857-320b24164575 + github.com/akto-api-security/akto-endpoint-shield => github.com/akto-api-security/akto-gateway/mcp-endpoint-shield v0.0.0-20260715100314-2175c200e52e github.com/shoenig/go-m1cpu => ./internal/stub/go-m1cpu ) diff --git a/apps/guardrails-service/container/src/go.sum b/apps/guardrails-service/container/src/go.sum index 01fcbcb4b6..4124fb1a36 100644 --- a/apps/guardrails-service/container/src/go.sum +++ b/apps/guardrails-service/container/src/go.sum @@ -29,8 +29,8 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= -github.com/akto-api-security/akto-gateway/mcp-endpoint-shield v0.0.0-20260714070857-320b24164575 h1:YRoCMr+zF+LslTvx/mPnXC0h6WyOF1uNPUuYq3zrUVY= -github.com/akto-api-security/akto-gateway/mcp-endpoint-shield v0.0.0-20260714070857-320b24164575/go.mod h1:xygh4yYStZhELBqnSAgC/eflshcDFacvKQz2ZF5bFwY= +github.com/akto-api-security/akto-gateway/mcp-endpoint-shield v0.0.0-20260715100314-2175c200e52e h1:bHBIYh0EDH3depP0elagmoiiE21P3hqDn9HKmeBzvBo= +github.com/akto-api-security/akto-gateway/mcp-endpoint-shield v0.0.0-20260715100314-2175c200e52e/go.mod h1:xygh4yYStZhELBqnSAgC/eflshcDFacvKQz2ZF5bFwY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/apps/guardrails-service/container/src/main.go b/apps/guardrails-service/container/src/main.go index ea4daa758e..8078ff1c58 100644 --- a/apps/guardrails-service/container/src/main.go +++ b/apps/guardrails-service/container/src/main.go @@ -10,6 +10,8 @@ import ( "time" + "github.com/akto-api-security/akto-endpoint-shield/mcp/depstats" + "github.com/akto-api-security/akto-endpoint-shield/mcp/guardcache" "github.com/akto-api-security/akto-endpoint-shield/utils" "github.com/akto-api-security/guardrails-service/handlers" "github.com/akto-api-security/guardrails-service/pkg/auth" @@ -74,13 +76,17 @@ func main() { } // metricsFlushLoop periodically drains guardrail metrics and pushes them to the -// db-abstractor every 60s: per-account latency plus instance-level -// CPU/memory/goroutine gauges. It blocks forever, so start it with `go`. +// db-abstractor every 60s: per-account latency, instance-level CPU/memory/ +// goroutine gauges, full-text semantic-cache hit/miss counts, and client-observed +// call health (errors, latency) for the two downstream dependencies — agent-guard +// (includes the Anonymize scanner) and the embedder (combined across the cache's +// single-text lookup and intentguard's batched calls). It blocks forever, so +// start it with `go`. func metricsFlushLoop(dbClient *dbabstractor.Client, logger *zap.Logger, acc *metrics.Accumulator) { sysSampler, err := metrics.NewSystemSampler() if err != nil { // Non-fatal: the service still validates without instance metrics. - logger.Warn("System metrics sampler unavailable; CPU/memory gauges disabled", zap.Error(err)) + logger.Warn("System metrics sampler unavailable; CPU/memory/cache/dependency gauges disabled", zap.Error(err)) } ticker := time.NewTicker(60 * time.Second) @@ -89,10 +95,18 @@ func metricsFlushLoop(dbClient *dbabstractor.Client, logger *zap.Logger, acc *me for accountId, batch := range acc.DrainAll() { flushMetrics(dbClient, logger, accountId, batch) } - // Instance-level CPU/memory/goroutine gauges, attributed to the - // service token's account (see metrics.SystemSampler). + // Instance-level CPU/memory/goroutine gauges, cache hit/miss counts, + // and downstream-dependency call health, attributed to the service + // token's account (see metrics.SystemSampler). if sysSampler != nil { - if sysBatch := sysSampler.Sample(); len(sysBatch) > 0 { + sysBatch := sysSampler.Sample() + cacheStats := guardcache.DrainStats() + sysBatch = append(sysBatch, sysSampler.CacheStats(cacheStats.HitsExact, cacheStats.HitsFuzzy, cacheStats.Misses)...) + agStats := depstats.AgentGuard.Drain() + sysBatch = append(sysBatch, sysSampler.DependencyStats("GUARDRAIL_AGENT_GUARD", agStats.Calls, agStats.Errors, agStats.AvgLatencyMs)...) + embStats := depstats.Embedder.Drain() + sysBatch = append(sysBatch, sysSampler.DependencyStats("GUARDRAIL_EMBEDDER", embStats.Calls, embStats.Errors, embStats.AvgLatencyMs)...) + if len(sysBatch) > 0 { flushMetrics(dbClient, logger, strconv.FormatInt(sysBatch[0].AccountId, 10), sysBatch) } } diff --git a/apps/guardrails-service/container/src/pkg/metrics/system_metrics.go b/apps/guardrails-service/container/src/pkg/metrics/system_metrics.go index 3e9a924b23..09a0e932b1 100644 --- a/apps/guardrails-service/container/src/pkg/metrics/system_metrics.go +++ b/apps/guardrails-service/container/src/pkg/metrics/system_metrics.go @@ -41,19 +41,27 @@ func NewSystemSampler() (*SystemSampler, error) { return &SystemSampler{proc: p, accountId: accID, instanceId: host}, nil } -// gauge builds a MetricData for one instance-level reading. -func (s *SystemSampler) gauge(metricId string, value float64, now int64) MetricData { +// metric builds a MetricData for one instance-level reading of the given type. +func (s *SystemSampler) metric(metricId, metricType string, value float64, now int64) MetricData { return MetricData{ MetricId: metricId, Value: value, Timestamp: now, - MetricType: "GAUGE", + MetricType: metricType, ModuleType: moduleTypeAgentGateway, AccountId: s.accountId, InstanceId: s.instanceId, } } +func (s *SystemSampler) gauge(metricId string, value float64, now int64) MetricData { + return s.metric(metricId, "GAUGE", value, now) +} + +func (s *SystemSampler) sum(metricId string, value float64, now int64) MetricData { + return s.metric(metricId, "SUM", value, now) +} + // Sample reads current CPU%, resident memory and goroutine count. It is meant to // be called once per flush tick; the long-lived process handle lets Percent // report usage over the interval since the previous call. @@ -75,3 +83,32 @@ func (s *SystemSampler) Sample() []MetricData { return out } + +// CacheStats builds SUM metrics for the full-text semantic-cache hit/miss +// counts accumulated since the last call — a delta (e.g. from +// guardcache.DrainStats), not a running total, matching MetricType SUM. +// Hits are split by match type: exact (literal repeat) vs fuzzy (embedding +// similarity match), since they represent different cache behavior. +func (s *SystemSampler) CacheStats(hitsExact, hitsFuzzy, misses uint64) []MetricData { + now := time.Now().Unix() + return []MetricData{ + s.sum("GUARDRAIL_CACHE_HIT_EXACT", float64(hitsExact), now), + s.sum("GUARDRAIL_CACHE_HIT_FUZZY", float64(hitsFuzzy), now), + s.sum("GUARDRAIL_CACHE_MISS", float64(misses), now), + } +} + +// DependencyStats builds call-health metrics for one downstream dependency +// (agent-guard or the embedder), as observed by this client — network transit +// time and connection-level failures included, which the dependency's own +// server-side metrics can't see. calls/errors are deltas since the last call +// (e.g. from a depstats.Tracker.Drain), matching MetricType SUM; avgLatencyMs +// is this window's mean, matching MetricType GAUGE. +func (s *SystemSampler) DependencyStats(prefix string, calls, errors uint64, avgLatencyMs float64) []MetricData { + now := time.Now().Unix() + return []MetricData{ + s.sum(prefix+"_CALLS", float64(calls), now), + s.sum(prefix+"_ERRORS", float64(errors), now), + s.gauge(prefix+"_LATENCY", avgLatencyMs, now), + } +}