agent-orchestrator/backend/internal/telemetrymeta/errors.go

93 lines
1.9 KiB
Go

package telemetrymeta
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/apierr"
)
// ErrorKindAndCode extracts a telemetry-safe error category and optional code.
func ErrorKindAndCode(err error) (kind, code string) {
kind = "internal"
var apiErr *apierr.Error
if errors.As(err, &apiErr) {
return ErrorKind(apiErr.Kind), apiErr.Code
}
return kind, ""
}
// ErrorKind maps API error kinds to coarse telemetry-safe categories.
func ErrorKind(kind apierr.Kind) string {
switch kind {
case apierr.KindInvalid:
return "invalid"
case apierr.KindNotFound:
return "not_found"
case apierr.KindConflict:
return "conflict"
default:
return "internal"
}
}
// PanicKind classifies panic payloads without exporting their raw contents.
func PanicKind(rec any) string {
switch rec.(type) {
case error:
return "error"
case string:
return "string"
default:
return "other"
}
}
// StatusFamily returns a telemetry-friendly HTTP status bucket like 5xx.
func StatusFamily(status int) string {
if status < 100 || status > 999 {
return "unknown"
}
return fmt.Sprintf("%dxx", status/100)
}
// RoutePattern returns the chi route template when available, else the URL path.
func RoutePattern(r *http.Request) string {
if r == nil {
return ""
}
if rc := chi.RouteContext(r.Context()); rc != nil {
if pattern := strings.TrimSpace(rc.RoutePattern()); pattern != "" {
return pattern
}
}
if r.URL == nil {
return ""
}
return r.URL.Path
}
// Fingerprint returns a short stable digest for grouping similar failures.
func Fingerprint(parts ...string) string {
h := sha256.New()
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
_, _ = h.Write([]byte(part))
_, _ = h.Write([]byte{0})
}
sum := hex.EncodeToString(h.Sum(nil))
if len(sum) > 16 {
return sum[:16]
}
return sum
}