Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
75 lines
2.2 KiB
Go
75 lines
2.2 KiB
Go
package guard
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
// gateMetrics are what only the gate sees: the judge, and redactions.
|
|
type gateMetrics struct {
|
|
judgeLatency *prometheus.HistogramVec
|
|
judgeErrors *prometheus.CounterVec
|
|
failOpen prometheus.Counter
|
|
redactions *prometheus.CounterVec
|
|
}
|
|
|
|
func newGateMetrics() *gateMetrics {
|
|
return &gateMetrics{
|
|
judgeLatency: prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
|
Name: "llmgw_guard_judge_latency_seconds",
|
|
Help: "Time a judge model took to answer the guardrails, cached answers excluded.",
|
|
Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5},
|
|
}, []string{"judge_model"}),
|
|
judgeErrors: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "llmgw_guard_judge_errors_total",
|
|
Help: "Judge calls that gave no verdict, by reason: timeout, unavailable, format, no_judge.",
|
|
}, []string{"reason"}),
|
|
failOpen: prometheus.NewCounter(prometheus.CounterOpts{
|
|
Name: "llmgw_guard_fail_open_total",
|
|
Help: "Requests and answers served unjudged because a judge did not answer and the policy fails open.",
|
|
}),
|
|
redactions: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "llmgw_guard_redactions_total",
|
|
Help: "Values the guardrails rewrote as placeholders, by kind.",
|
|
}, []string{"kind"}),
|
|
}
|
|
}
|
|
|
|
// Describe and Collect make the gate a Prometheus collector.
|
|
func (g *Gate) Describe(ch chan<- *prometheus.Desc) {
|
|
for _, c := range g.collectors() {
|
|
c.Describe(ch)
|
|
}
|
|
}
|
|
|
|
func (g *Gate) Collect(ch chan<- prometheus.Metric) {
|
|
for _, c := range g.collectors() {
|
|
c.Collect(ch)
|
|
}
|
|
}
|
|
|
|
func (g *Gate) collectors() []prometheus.Collector {
|
|
m := g.m
|
|
return []prometheus.Collector{m.judgeLatency, m.judgeErrors, m.failOpen, m.redactions}
|
|
}
|
|
|
|
func (m *gateMetrics) judged(model string, took time.Duration, err error) {
|
|
if err == nil {
|
|
m.judgeLatency.WithLabelValues(model).Observe(took.Seconds())
|
|
return
|
|
}
|
|
reason := "unavailable"
|
|
switch {
|
|
case errors.Is(err, context.DeadlineExceeded):
|
|
reason = "timeout"
|
|
case errors.Is(err, errNoJudge):
|
|
reason = "no_judge"
|
|
case errors.Is(err, errJudgeFormat):
|
|
reason = "format"
|
|
}
|
|
m.judgeErrors.WithLabelValues(reason).Inc()
|
|
}
|