Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/guardrails-service/container/src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
4 changes: 2 additions & 2 deletions apps/guardrails-service/container/src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
26 changes: 20 additions & 6 deletions apps/guardrails-service/container/src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
}
}
Loading