CI / CI (push) Successful in 3m20s
## Summary Three issues in one PR. Implements Cache-Augmented Generation (CAG) support in the api-gw. --- ### #44 — Memory ServiceAdapter: standing-context resource - Code-registers `memory` adapter with `standing-context` resource (`POST /memory/standing-context`) - Fixes dispatcher bug: method-level `Auth.Capability` override now respected (was always using verb-derived capability) - Auth: `memory:read` required via method-level override; `memory:write` for ingest - `MEMORY_UPSTREAM_URL` env configures upstream ### #45 — Continuous batching: warmup deprioritisation + per-backend semaphore - In-memory bounded queue per backend: `VLLM_MAX_QUEUE_DEPTH` slots (default 20) - Concurrency limiter: `VLLM_MAX_NUM_SEQS` slots (default 4) - `X-Poimen-Warmup: true` → sleep `WARMUP_DELAY_MS` (default 500ms) before queue entry, strip header, add `X-Priority: low` - Queue full → 503 immediately, no goroutine pile-up - Metrics: `warmup_requests_delayed_total`, `queue_full_requests_total` at `/metrics` ### #46 — Cache-aware routing: rendezvous hash + prefix cache hit logging - `addresses: [...]` YAML field for multi-backend models (backward-compat with `address`) - `X-Poimen-Cache-Key` header → rendezvous-hash backend selection (same key = same GPU) - No cache key → round-robin - Failed backend excluded 30s; next-highest-score fallback - `vllm-num-cached-tokens` response header captured → `vllm_prefix_cache_hit_tokens_total` counter + structured log --- ## Verification ``` go build ./... ✅ go vet ./... ✅ go test -race ./... ✅ (all packages, 0 races) ``` ## Reviewer verdicts - reviewer-sre: SIGN-OFF - reviewer-quality: SIGN-OFF ## Non-blocking findings (🟡🔵 only) - `getOrCreateTransport` not goroutine-safe by design; pre-population invariant documented - `time.Sleep(warmupDelay)` holds goroutine; context-cancel alternative left as TODO - OTel trace_id not propagated into CAG log events (existing gap) Closes #44 Closes #45 Closes #46 --------- Co-authored-by: Admin Bot <[email protected]> Reviewed-on: #47
209 lines
5.1 KiB
Go
209 lines
5.1 KiB
Go
// Package logging provides structured JSON logging for the gateway.
|
|
// All logs are emitted as single-line JSON records.
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Level represents a log level.
|
|
type Level int
|
|
|
|
const (
|
|
LevelDebug Level = iota
|
|
LevelInfo
|
|
LevelWarn
|
|
LevelError
|
|
)
|
|
|
|
// Logger provides structured logging with sensitive data redaction.
|
|
type Logger struct {
|
|
level Level
|
|
out io.Writer
|
|
}
|
|
|
|
// New creates a new structured logger with the given level writing to out.
|
|
func New(level Level, out io.Writer) *Logger {
|
|
if out == nil {
|
|
out = os.Stderr
|
|
}
|
|
return &Logger{level: level, out: out}
|
|
}
|
|
|
|
// ParseLevel parses a log level string (debug, info, warn, error).
|
|
func ParseLevel(s string) Level {
|
|
switch strings.ToLower(s) {
|
|
case "debug":
|
|
return LevelDebug
|
|
case "info":
|
|
return LevelInfo
|
|
case "warn":
|
|
return LevelWarn
|
|
case "error":
|
|
return LevelError
|
|
default:
|
|
return LevelInfo
|
|
}
|
|
}
|
|
|
|
// LogRecord is a single structured log entry.
|
|
type LogRecord struct {
|
|
Timestamp string `json:"timestamp"`
|
|
Level string `json:"level"`
|
|
Message string `json:"message"`
|
|
Route string `json:"route,omitempty"`
|
|
Upstream string `json:"upstream,omitempty"`
|
|
Method string `json:"method,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Status int `json:"status,omitempty"`
|
|
Duration string `json:"duration,omitempty"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Extra map[string]string `json:"extra,omitempty"`
|
|
}
|
|
|
|
// RequestLog logs a request with response information.
|
|
func (l *Logger) RequestLog(route, upstream, method, path string, status int, duration time.Duration, reason string) {
|
|
if l.level > LevelInfo {
|
|
return
|
|
}
|
|
record := LogRecord{
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
|
Level: "info",
|
|
Message: "request",
|
|
Route: route,
|
|
Upstream: upstream,
|
|
Method: method,
|
|
Path: path,
|
|
Status: status,
|
|
Duration: duration.String(),
|
|
Reason: reason,
|
|
}
|
|
l.emit(record)
|
|
}
|
|
|
|
// Infof logs an info-level message.
|
|
func (l *Logger) Infof(msg string, fields map[string]string) {
|
|
if l.level > LevelInfo {
|
|
return
|
|
}
|
|
record := LogRecord{
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
|
Level: "info",
|
|
Message: msg,
|
|
Extra: fields,
|
|
}
|
|
l.emit(record)
|
|
}
|
|
|
|
// Warnf logs a warn-level message.
|
|
func (l *Logger) Warnf(msg string, fields map[string]string) {
|
|
if l.level > LevelWarn {
|
|
return
|
|
}
|
|
record := LogRecord{
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
|
Level: "warn",
|
|
Message: msg,
|
|
Extra: fields,
|
|
}
|
|
l.emit(record)
|
|
}
|
|
|
|
// Errorf logs an error-level message.
|
|
func (l *Logger) Errorf(msg string, err error, fields map[string]string) {
|
|
if l.level > LevelError {
|
|
return
|
|
}
|
|
errStr := ""
|
|
if err != nil {
|
|
errStr = err.Error()
|
|
}
|
|
record := LogRecord{
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
|
Level: "error",
|
|
Message: msg,
|
|
Error: errStr,
|
|
Extra: fields,
|
|
}
|
|
l.emit(record)
|
|
}
|
|
|
|
// emit writes a record as a single JSON line.
|
|
func (l *Logger) emit(record LogRecord) {
|
|
data, err := json.Marshal(record)
|
|
if err != nil {
|
|
log.Printf("failed to marshal log record: %v", err)
|
|
return
|
|
}
|
|
_, _ = l.out.Write(append(data, '\n'))
|
|
}
|
|
|
|
// global is the singleton logger instance.
|
|
var global *Logger
|
|
|
|
// Init initializes the global logger with the given level.
|
|
// If not called, defaults to Info level on stderr.
|
|
func Init(level Level, out io.Writer) {
|
|
global = New(level, out)
|
|
}
|
|
|
|
// ensure global is initialized
|
|
func ensure() {
|
|
if global == nil {
|
|
global = New(LevelInfo, os.Stderr)
|
|
}
|
|
}
|
|
|
|
// RequestLog logs a request with response information using the global logger.
|
|
func RequestLog(route, upstream, method, path string, status int, duration time.Duration, reason string) {
|
|
ensure()
|
|
global.RequestLog(route, upstream, method, path, status, duration, reason)
|
|
}
|
|
|
|
// Infof logs an info message using the global logger.
|
|
func Infof(msg string, fields map[string]string) {
|
|
ensure()
|
|
global.Infof(msg, fields)
|
|
}
|
|
|
|
// Warnf logs a warn message using the global logger.
|
|
func Warnf(msg string, fields map[string]string) {
|
|
ensure()
|
|
global.Warnf(msg, fields)
|
|
}
|
|
|
|
// Errorf logs an error message using the global logger.
|
|
func Errorf(msg string, err error, fields map[string]string) {
|
|
ensure()
|
|
global.Errorf(msg, err, fields)
|
|
}
|
|
|
|
// FromContext retrieves the logger from a context, or returns the global logger.
|
|
func FromContext(ctx context.Context) *Logger {
|
|
ensure()
|
|
if l, ok := ctx.Value("logger").(*Logger); ok {
|
|
return l
|
|
}
|
|
return global
|
|
}
|
|
|
|
// WithContext returns a new context with the logger attached.
|
|
func WithContext(ctx context.Context, l *Logger) context.Context {
|
|
return context.WithValue(ctx, "logger", l)
|
|
}
|
|
|
|
// Event emits arbitrary fields as a structured JSON line.
|
|
// Use when field values include non-string types (numerics, floats).
|
|
func Event(fields map[string]interface{}) {
|
|
ensure()
|
|
data, _ := json.Marshal(fields)
|
|
_, _ = global.out.Write(append(data, '\n'))
|
|
}
|