mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-08 11:30:32 +01:00
Compare commits
1 Commits
feat/servi
...
tvats-pkg-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
411d3e64a4 |
99
pkg/errors/v2/code.go
Normal file
99
pkg/errors/v2/code.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Code is a dotted, hierarchical identifier registered at process start. It
|
||||
// encodes domain (subsystem), op (verb), optional sub (qualifier), and a
|
||||
// terminal reason. Codes are values; two Codes with the same string are equal
|
||||
// by value and safe to compare with ==.
|
||||
type Code struct{ s string }
|
||||
|
||||
// String returns the dotted code as it appears on the wire. Empty for the
|
||||
// zero value.
|
||||
func (c Code) String() string { return c.s }
|
||||
|
||||
// codePattern allows 2-4 dotted segments, each starting with a lowercase
|
||||
// letter and continuing with [a-z0-9_]. One segment is too broad (use a
|
||||
// domain prefix); five or more means the domain should be split.
|
||||
var codePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$`)
|
||||
|
||||
// Meta is the per-code default envelope applied by constructors before
|
||||
// per-call options. Every field has a natural per-code default — an auth
|
||||
// code always wants Reauthenticate, every documented code wants its docs
|
||||
// URL — so the registry is the right place to declare them once.
|
||||
type Meta struct {
|
||||
Category Category
|
||||
Fault Fault
|
||||
Retry Retry
|
||||
Remediation Remediation
|
||||
Refs map[RefKind]string
|
||||
}
|
||||
|
||||
// Retry tells the caller how and when to retry. After is meaningful only
|
||||
// when Policy == RetryAfter.
|
||||
type Retry struct {
|
||||
Policy RetryPolicy
|
||||
After time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
registryMu sync.RWMutex
|
||||
registry = map[string]Meta{}
|
||||
)
|
||||
|
||||
// Register installs a code with its default Meta and returns the Code value.
|
||||
// It panics on a malformed code string or a duplicate registration — both
|
||||
// indicate a programming error that must be caught at boot, not at first
|
||||
// failure.
|
||||
//
|
||||
// Call from the owning domain's package init or top-level var block:
|
||||
//
|
||||
// var CodeUnknownFunction = errors.Register("query.parse.unknown_function", errors.Meta{
|
||||
// Category: errors.CategoryInvalidInput,
|
||||
// Fault: errors.FaultCaller,
|
||||
// Retry: errors.Retry{Policy: errors.RetryAfterFix},
|
||||
// })
|
||||
func Register(s string, meta Meta) Code {
|
||||
if !codePattern.MatchString(s) {
|
||||
panic("errors/v2: malformed code: " + s)
|
||||
}
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
if _, ok := registry[s]; ok {
|
||||
panic("errors/v2: duplicate code: " + s)
|
||||
}
|
||||
registry[s] = meta
|
||||
return Code{s: s}
|
||||
}
|
||||
|
||||
// MetaOf returns the Meta a code was registered with. Returns the zero Meta
|
||||
// and false for unregistered or zero codes.
|
||||
func MetaOf(c Code) (Meta, bool) {
|
||||
if c.s == "" {
|
||||
return Meta{}, false
|
||||
}
|
||||
registryMu.RLock()
|
||||
defer registryMu.RUnlock()
|
||||
m, ok := registry[c.s]
|
||||
return m, ok
|
||||
}
|
||||
|
||||
// registerOrGet is the internal idempotent register used by adapters that
|
||||
// may see the same code (e.g. legacy.<v1>) more than once across the process
|
||||
// lifetime. It panics on malformed codes — duplicate codes silently keep the
|
||||
// existing Meta.
|
||||
func registerOrGet(s string, meta Meta) Code {
|
||||
if !codePattern.MatchString(s) {
|
||||
panic("errors/v2: malformed code: " + s)
|
||||
}
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
if _, ok := registry[s]; !ok {
|
||||
registry[s] = meta
|
||||
}
|
||||
return Code{s: s}
|
||||
}
|
||||
93
pkg/errors/v2/enums.go
Normal file
93
pkg/errors/v2/enums.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package v2
|
||||
|
||||
// The enums in this file are closed sets. Each value is a package-level var of
|
||||
// an unexported-field struct, so external code cannot synthesize new values —
|
||||
// it must reference one of the defined ones. String() returns the stable
|
||||
// snake_case wire name; once shipped, those names are append-only.
|
||||
|
||||
// Category groups errors by what kind of failure occurred. It is the coarsest
|
||||
// branch-worthy axis and is intended to be a superset of gRPC status codes
|
||||
// extended with cases SigNoz cares about (e.g. license issues land under
|
||||
// FailedDependency or ResourceExhausted depending on context).
|
||||
type Category struct{ s string }
|
||||
|
||||
func (c Category) String() string { return c.s }
|
||||
|
||||
var (
|
||||
CategoryInvalidInput = Category{"invalid_input"} // request was malformed or violated a documented constraint.
|
||||
CategoryNotFound = Category{"not_found"} // referenced resource does not exist.
|
||||
CategoryAlreadyExists = Category{"already_exists"} // resource creation conflicts with an existing one.
|
||||
CategoryConflict = Category{"conflict"} // concurrent modification or state mismatch (e.g. stale revision).
|
||||
CategoryPrecondition = Category{"precondition"} // a required precondition (system or caller-asserted) was not met.
|
||||
CategoryUnauthenticated = Category{"unauthenticated"} // credentials are missing or invalid.
|
||||
CategoryForbidden = Category{"forbidden"} // authenticated but not authorized for this action.
|
||||
CategoryResourceExhausted = Category{"resource_exhausted"} // quota, rate limit, or other budget exceeded.
|
||||
CategoryFailedDependency = Category{"failed_dependency"} // an upstream service we depend on failed (db, license, etc.).
|
||||
CategoryUnavailable = Category{"unavailable"} // service is temporarily down; retry with backoff.
|
||||
CategoryTimeout = Category{"timeout"} // deadline exceeded before the operation completed.
|
||||
CategoryCanceled = Category{"canceled"} // caller or context canceled the operation.
|
||||
CategoryUnimplemented = Category{"unimplemented"} // operation is not supported (or not yet) by this server.
|
||||
CategoryDataLoss = Category{"data_loss"} // unrecoverable data corruption or loss detected.
|
||||
CategoryInternal = Category{"internal"} // bug — invariant broken; should not occur in normal operation.
|
||||
)
|
||||
|
||||
// Fault attributes responsibility. An agent uses this to decide whether to
|
||||
// fix the request (Caller), retry/escalate (Server, Upstream), or page a
|
||||
// human (Operator).
|
||||
type Fault struct{ s string }
|
||||
|
||||
func (f Fault) String() string { return f.s }
|
||||
|
||||
var (
|
||||
FaultCaller = Fault{"caller"}
|
||||
FaultServer = Fault{"server"}
|
||||
FaultUpstream = Fault{"upstream"}
|
||||
FaultOperator = Fault{"operator"}
|
||||
)
|
||||
|
||||
// RetryPolicy tells the caller how to behave on retry. Backoff implies the
|
||||
// caller should use its own backoff schedule; After means honor Retry.After
|
||||
// exactly; AfterFix and AfterAuth signal that retry is pointless until the
|
||||
// caller fixes the request or re-authenticates.
|
||||
type RetryPolicy struct{ s string }
|
||||
|
||||
func (r RetryPolicy) String() string { return r.s }
|
||||
|
||||
var (
|
||||
RetryNever = RetryPolicy{"never"}
|
||||
RetryImmediate = RetryPolicy{"immediate"}
|
||||
RetryBackoff = RetryPolicy{"backoff"}
|
||||
RetryAfter = RetryPolicy{"after"}
|
||||
RetryAfterFix = RetryPolicy{"after_fix"}
|
||||
RetryAfterAuth = RetryPolicy{"after_auth"}
|
||||
)
|
||||
|
||||
// Remediation names the single recommended next action. It does not execute.
|
||||
type Remediation struct{ s string }
|
||||
|
||||
func (r Remediation) String() string { return r.s }
|
||||
|
||||
var (
|
||||
RemediationNone = Remediation{"none"}
|
||||
RemediationFixInput = Remediation{"fix_input"}
|
||||
RemediationReauthenticate = Remediation{"reauthenticate"}
|
||||
RemediationWaitAndRetry = Remediation{"wait_and_retry"}
|
||||
RemediationFailover = Remediation{"failover"}
|
||||
RemediationContactOperator = Remediation{"contact_operator"}
|
||||
RemediationFileBug = Remediation{"file_bug"}
|
||||
RemediationUpgradeLicense = Remediation{"upgrade_license"}
|
||||
)
|
||||
|
||||
// RefKind classifies a reference URL attached to the error.
|
||||
type RefKind struct{ s string }
|
||||
|
||||
func (r RefKind) String() string { return r.s }
|
||||
|
||||
var (
|
||||
RefDocs = RefKind{"docs"}
|
||||
RefRunbook = RefKind{"runbook"}
|
||||
RefDashboard = RefKind{"dashboard"}
|
||||
RefTrace = RefKind{"trace"}
|
||||
RefSource = RefKind{"source"}
|
||||
RefIssue = RefKind{"issue"}
|
||||
)
|
||||
248
pkg/errors/v2/error.go
Normal file
248
pkg/errors/v2/error.go
Normal file
@@ -0,0 +1,248 @@
|
||||
// Package v2 is the redesigned pkg/errors.
|
||||
//
|
||||
// Every branch-worthy field on the Error struct is a closed enum and every
|
||||
// variable part is a typed key/value. The intent is to make errors first-class
|
||||
// data for programmatic consumers — SDK clients, UI surfaces, alerting, and
|
||||
// LLM agents — without sacrificing human readability.
|
||||
//
|
||||
// Domain and op are encoded into Code (e.g. "query.parse.unknown_function")
|
||||
// rather than carried as separate struct fields. Frames[0] is the
|
||||
// authoritative call-site location, captured at construction time.
|
||||
package v2
|
||||
|
||||
import (
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Error is the redesigned error value. *Error is the canonical form passed
|
||||
// around — the zero value is unused, construct via New / Newf / Wrap / Wrapf.
|
||||
//
|
||||
// Frames are intentionally not a struct field: resolving captured PCs into
|
||||
// func/file/line is the dominant construction cost, so we capture PCs eagerly
|
||||
// at construction time (so the snapshot is faithful to the call site) and
|
||||
// resolve them lazily via Frames() only when something actually inspects them.
|
||||
type Error struct {
|
||||
// WHAT
|
||||
Category Category
|
||||
Code Code
|
||||
Title string
|
||||
Detail string
|
||||
|
||||
// WHY / WHO
|
||||
Cause error
|
||||
Fault Fault
|
||||
|
||||
// WHAT NEXT
|
||||
Retry Retry
|
||||
Remediation Remediation
|
||||
Refs map[RefKind]string
|
||||
|
||||
// CONTEXT
|
||||
Attrs map[string]any
|
||||
TraceID string
|
||||
SpanID string
|
||||
|
||||
// stack is the captured PCs plus a memoized []Frame; never read directly,
|
||||
// always go through Frames().
|
||||
stack *frameStack
|
||||
}
|
||||
|
||||
// Frames returns the captured stack, resolved to func/file/line on first
|
||||
// access. Frames[0] is the constructor's caller. Safe for concurrent use.
|
||||
func (e *Error) Frames() []Frame {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.stack.frames()
|
||||
}
|
||||
|
||||
// New creates an Error for a registered Code. Defaults from the registered
|
||||
// Meta are applied first; opts override per call site.
|
||||
func New(code Code, title string, opts ...Option) *Error {
|
||||
e := &Error{Code: code, Title: title, stack: captureStack(3)}
|
||||
applyMeta(e)
|
||||
for _, opt := range opts {
|
||||
opt(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Newf is New with fmt.Sprintf-style formatting for the title.
|
||||
func Newf(code Code, format string, args ...any) *Error {
|
||||
e := &Error{Code: code, Title: fmt.Sprintf(format, args...), stack: captureStack(3)}
|
||||
applyMeta(e)
|
||||
return e
|
||||
}
|
||||
|
||||
// Wrap creates an Error that wraps cause. The new error's Title is the
|
||||
// caller-supplied title (not the cause's message), so Error() reports what
|
||||
// went wrong at this layer — the cause is reachable via Unwrap.
|
||||
func Wrap(cause error, code Code, title string, opts ...Option) *Error {
|
||||
e := &Error{Code: code, Title: title, Cause: cause, stack: captureStack(3)}
|
||||
applyMeta(e)
|
||||
for _, opt := range opts {
|
||||
opt(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Wrapf is Wrap with fmt.Sprintf-style formatting for the title.
|
||||
func Wrapf(cause error, code Code, format string, args ...any) *Error {
|
||||
e := &Error{Code: code, Title: fmt.Sprintf(format, args...), Cause: cause, stack: captureStack(3)}
|
||||
applyMeta(e)
|
||||
return e
|
||||
}
|
||||
|
||||
// applyMeta copies default values from the registered Meta into a fresh
|
||||
// Error. It runs before per-call options so options win.
|
||||
func applyMeta(e *Error) {
|
||||
meta, ok := MetaOf(e.Code)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if (e.Category == Category{}) {
|
||||
e.Category = meta.Category
|
||||
}
|
||||
if (e.Fault == Fault{}) {
|
||||
e.Fault = meta.Fault
|
||||
}
|
||||
if (e.Retry == Retry{}) {
|
||||
e.Retry = meta.Retry
|
||||
}
|
||||
if (e.Remediation == Remediation{}) {
|
||||
e.Remediation = meta.Remediation
|
||||
}
|
||||
if len(meta.Refs) > 0 {
|
||||
if e.Refs == nil {
|
||||
e.Refs = make(map[RefKind]string, len(meta.Refs))
|
||||
}
|
||||
for k, v := range meta.Refs {
|
||||
if _, exists := e.Refs[k]; !exists {
|
||||
e.Refs[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns the Title (the message specifically attached at this wrap
|
||||
// site), not the cause's message. This fixes the v1 surprise where Error()
|
||||
// returned the wrapped cause's text.
|
||||
func (e *Error) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return e.Title
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped cause, enabling errors.Is / errors.As.
|
||||
func (e *Error) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter.
|
||||
//
|
||||
// %s, %v → Title only
|
||||
// %+v → full chain: code, title, frames, attrs, recursive cause
|
||||
func (e *Error) Format(f fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
_, _ = io.WriteString(f, e.Title)
|
||||
case 'v':
|
||||
if f.Flag('+') {
|
||||
_, _ = io.WriteString(f, e.fullString())
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(f, e.Title)
|
||||
case 'q':
|
||||
fmt.Fprintf(f, "%q", e.Title)
|
||||
default:
|
||||
fmt.Fprintf(f, "%%!%c(*errors/v2.Error)", verb)
|
||||
}
|
||||
}
|
||||
|
||||
// fullString produces the %+v rendering. Format is intentionally
|
||||
// human-readable rather than machine-parseable; consumers that want structure
|
||||
// should marshal to JSON.
|
||||
func (e *Error) fullString() string {
|
||||
var b strings.Builder
|
||||
e.appendFull(&b, 0)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (e *Error) appendFull(b *strings.Builder, depth int) {
|
||||
indent := strings.Repeat(" ", depth)
|
||||
fmt.Fprintf(b, "%s[%s] %s\n", indent, e.Code.s, e.Title)
|
||||
if e.Detail != "" {
|
||||
fmt.Fprintf(b, "%s detail: %s\n", indent, e.Detail)
|
||||
}
|
||||
if len(e.Attrs) > 0 {
|
||||
// Stable key order for deterministic output.
|
||||
keys := make([]string, 0, len(e.Attrs))
|
||||
for k := range e.Attrs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
fmt.Fprintf(b, "%s attrs:\n", indent)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(b, "%s %s=%v\n", indent, k, e.Attrs[k])
|
||||
}
|
||||
}
|
||||
if frames := e.Frames(); len(frames) > 0 {
|
||||
fmt.Fprintf(b, "%s frames:\n", indent)
|
||||
for _, fr := range frames {
|
||||
fmt.Fprintf(b, "%s %s\n%s %s:%s\n", indent, fr.Func, indent, fr.File, strconv.Itoa(fr.Line))
|
||||
}
|
||||
}
|
||||
if e.Cause != nil {
|
||||
fmt.Fprintf(b, "%scaused by:\n", indent)
|
||||
var ce *Error
|
||||
if stderrors.As(e.Cause, &ce) && ce != nil {
|
||||
ce.appendFull(b, depth+1)
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s %s\n", indent, e.Cause.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AsError extracts a *Error from anywhere in err's wrap chain. It is the
|
||||
// common shortcut around errors.As for code that always wants this package's
|
||||
// type.
|
||||
func AsError(err error) (*Error, bool) {
|
||||
if err == nil {
|
||||
return nil, false
|
||||
}
|
||||
var e *Error
|
||||
if stderrors.As(err, &e) {
|
||||
return e, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Is reports whether err or any error in its chain has the given Code.
|
||||
// Convenience wrapper that's friendlier than errors.As at call sites that
|
||||
// only care about code identity.
|
||||
func Is(err error, code Code) bool {
|
||||
e, ok := AsError(err)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for e != nil {
|
||||
if e.Code == code {
|
||||
return true
|
||||
}
|
||||
next, ok := AsError(e.Cause)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
e = next
|
||||
}
|
||||
return false
|
||||
}
|
||||
90
pkg/errors/v2/example.go
Normal file
90
pkg/errors/v2/example.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package v2
|
||||
|
||||
// This file is a self-contained walkthrough of how a domain integrates with
|
||||
// pkg/errors/v2. It mirrors what a real pkg/<domain>/errors.go looks like in
|
||||
// practice — registering codes, constructing typed errors at failure sites,
|
||||
// and consuming them at API boundaries. The "example.*" namespace is reserved
|
||||
// for these demo codes so they never collide with a real domain's
|
||||
// registrations.
|
||||
|
||||
// 1. Register codes at package init time. Each Register call panics on
|
||||
// malformed code or duplicate registration, so misconfiguration is caught
|
||||
// at process boot, not at first failure.
|
||||
var (
|
||||
// A caller-fault, fix-the-input error: rejected before any work happens.
|
||||
exampleCodeInvalidQuery = Register("example.query.invalid_filter", Meta{
|
||||
Category: CategoryInvalidInput,
|
||||
Fault: FaultCaller,
|
||||
Remediation: RemediationFixInput,
|
||||
Retry: Retry{Policy: RetryAfterFix},
|
||||
Refs: map[RefKind]string{
|
||||
RefDocs: "https://signoz.io/docs/query/filters",
|
||||
},
|
||||
})
|
||||
|
||||
// A quota error: the caller's request was well-formed but their plan
|
||||
// doesn't allow it. The recommended remediation is structural (upgrade),
|
||||
// not "try again later."
|
||||
exampleCodeQuotaExceeded = Register("example.billing.quota_exceeded", Meta{
|
||||
Category: CategoryResourceExhausted,
|
||||
Fault: FaultCaller,
|
||||
Remediation: RemediationUpgradeLicense,
|
||||
Retry: Retry{Policy: RetryNever},
|
||||
})
|
||||
)
|
||||
|
||||
// 2. Construct errors at the failure site. Notice that variable parts of
|
||||
// the message (the offending field, the limits) live in typed Attrs, not in
|
||||
// the title prose — a downstream agent can read them without parsing English.
|
||||
func exampleRejectInvalidFilter(field string) *Error {
|
||||
return New(exampleCodeInvalidQuery, "filter is not supported",
|
||||
WithAttr("field", field),
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Consume errors at the API boundary. Branching on Category gives the
|
||||
// HTTP status; Retry tells an SDK how to behave; Fault drives logging
|
||||
// classification (caller errors are warnings, server/upstream errors page).
|
||||
func exampleClassifyForHTTP(err error) (status int, retry RetryPolicy) {
|
||||
e, ok := AsError(err)
|
||||
if !ok {
|
||||
return 500, RetryNever
|
||||
}
|
||||
switch e.Category {
|
||||
case CategoryInvalidInput, CategoryPrecondition:
|
||||
status = 400
|
||||
case CategoryUnauthenticated:
|
||||
status = 401
|
||||
case CategoryForbidden:
|
||||
status = 403
|
||||
case CategoryNotFound:
|
||||
status = 404
|
||||
case CategoryConflict, CategoryAlreadyExists:
|
||||
status = 409
|
||||
case CategoryResourceExhausted:
|
||||
status = 429
|
||||
case CategoryUnavailable, CategoryTimeout:
|
||||
status = 503
|
||||
case CategoryUnimplemented:
|
||||
status = 501
|
||||
default:
|
||||
status = 500
|
||||
}
|
||||
return status, e.Retry.Policy
|
||||
}
|
||||
|
||||
// 4. Identify a specific failure mode by Code. Is walks the cause chain so
|
||||
// a wrapper at the HTTP layer still matches when the root cause was raised
|
||||
// deep in the call graph.
|
||||
func exampleIsQuotaExceeded(err error) bool {
|
||||
return Is(err, exampleCodeQuotaExceeded)
|
||||
}
|
||||
|
||||
// The example helpers are reference-only: they exist to document call-site
|
||||
// patterns, not to be called from anywhere in the binary. This anchor keeps
|
||||
// them visible to readers (and the linter) without exporting demo code.
|
||||
var _ = []any{
|
||||
exampleRejectInvalidFilter,
|
||||
exampleClassifyForHTTP,
|
||||
exampleIsQuotaExceeded,
|
||||
}
|
||||
65
pkg/errors/v2/frame.go
Normal file
65
pkg/errors/v2/frame.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Frame is a single line in the call stack. Frames[0] is the constructor's
|
||||
// caller — the authoritative "where this error came from" — and downstream
|
||||
// consumers can filter (e.g. "frames inside our code") without regex
|
||||
// reparsing of a pre-formatted stack string.
|
||||
type Frame struct {
|
||||
Func string `json:"func,omitempty"`
|
||||
File string `json:"file,omitempty"`
|
||||
Line int `json:"line,omitempty"`
|
||||
}
|
||||
|
||||
// frameStack carries the PCs captured at construction plus the resolved
|
||||
// []Frame slice, behind a sync.Once. Resolving frames into func/file/line is
|
||||
// expensive (runtime.CallersFrames walks the symbol table); the vast majority
|
||||
// of errors are constructed and never inspected, so we only pay that cost
|
||||
// when a consumer actually asks for frames (Frames()/MarshalJSON/%+v).
|
||||
//
|
||||
// The PC capture itself is cheap and happens at construction so that
|
||||
// Frames[0] is a faithful "where" record of the original call site.
|
||||
type frameStack struct {
|
||||
pcs []uintptr
|
||||
|
||||
once sync.Once
|
||||
resolved []Frame
|
||||
}
|
||||
|
||||
// captureStack is called by every constructor. skip drops runtime.Callers,
|
||||
// captureStack itself, and the constructor frame so that the first PC is the
|
||||
// user code that invoked the constructor.
|
||||
func captureStack(skip int) *frameStack {
|
||||
const depth = 32
|
||||
pcs := make([]uintptr, depth)
|
||||
n := runtime.Callers(skip, pcs)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
return &frameStack{pcs: pcs[:n:n]}
|
||||
}
|
||||
|
||||
// frames resolves the captured PCs into []Frame. The resolution is memoized
|
||||
// — concurrent calls are safe and only one of them does the work.
|
||||
func (s *frameStack) frames() []Frame {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
s.once.Do(func() {
|
||||
cf := runtime.CallersFrames(s.pcs)
|
||||
out := make([]Frame, 0, len(s.pcs))
|
||||
for {
|
||||
f, more := cf.Next()
|
||||
out = append(out, Frame{Func: f.Function, File: f.File, Line: f.Line})
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.resolved = out
|
||||
})
|
||||
return s.resolved
|
||||
}
|
||||
175
pkg/errors/v2/http.go
Normal file
175
pkg/errors/v2/http.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// CodeUnknown is the sentinel returned when AsJSON / AsURLValues are called
|
||||
// on a non-*Error. A consumer that sees this on the wire should read it as
|
||||
// "the producer did not raise a v2 Error and we projected it through the
|
||||
// fallback path" — i.e. somewhere upstream is still using std errors or v1.
|
||||
var CodeUnknown = Register("unknown.unset", Meta{
|
||||
Category: CategoryInternal,
|
||||
Fault: FaultServer,
|
||||
Retry: Retry{Policy: RetryNever},
|
||||
})
|
||||
|
||||
// JSON is the wire envelope for an Error. It is intentionally a superset of
|
||||
// v1's pkg/errors.JSON: SDK clients that only read v1's {code, message, url,
|
||||
// errors[]} keep working, while v2 consumers can branch on the new typed
|
||||
// fields (category, fault, retry, remediation, attrs, refs, cause).
|
||||
type JSON struct {
|
||||
Code string `json:"code" required:"true"`
|
||||
Title string `json:"title" required:"true"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Fault string `json:"fault,omitempty"`
|
||||
Retry *RetryJSON `json:"retry,omitempty"`
|
||||
Remediation string `json:"remediation,omitempty"`
|
||||
Attrs map[string]any `json:"attrs,omitempty"`
|
||||
Refs map[string]string `json:"refs,omitempty"`
|
||||
Frames []Frame `json:"frames,omitempty"`
|
||||
TraceID string `json:"trace_id,omitempty"`
|
||||
SpanID string `json:"span_id,omitempty"`
|
||||
Cause *CauseJSON `json:"cause,omitempty"`
|
||||
}
|
||||
|
||||
// RetryJSON renders Retry as an object so consumers can branch on policy
|
||||
// before consulting AfterMS. AfterMS is omitted unless policy is "after".
|
||||
type RetryJSON struct {
|
||||
Policy string `json:"policy"`
|
||||
AfterMS int64 `json:"after_ms,omitempty"`
|
||||
}
|
||||
|
||||
// CauseJSON is the thin recursive shape for a cause chain. Only code, title,
|
||||
// and a nested cause are guaranteed — producers may add more, consumers must
|
||||
// not rely on it.
|
||||
type CauseJSON struct {
|
||||
Code string `json:"code,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Cause *CauseJSON `json:"cause,omitempty"`
|
||||
}
|
||||
|
||||
// AsJSON projects any error onto the v2 wire envelope. If cause is a
|
||||
// *Error (anywhere in its wrap chain) every field is filled from it;
|
||||
// otherwise the result is a CodeUnknown envelope with Title=cause.Error()
|
||||
// so the wire shape is always valid and never panics.
|
||||
func AsJSON(cause error) *JSON {
|
||||
if cause == nil {
|
||||
return nil
|
||||
}
|
||||
e, ok := AsError(cause)
|
||||
if !ok {
|
||||
return &JSON{
|
||||
Code: CodeUnknown.s,
|
||||
Title: cause.Error(),
|
||||
Category: CategoryInternal.s,
|
||||
Fault: FaultServer.s,
|
||||
}
|
||||
}
|
||||
return errorToJSON(e)
|
||||
}
|
||||
|
||||
func errorToJSON(e *Error) *JSON {
|
||||
out := &JSON{
|
||||
Code: e.Code.s,
|
||||
Title: e.Title,
|
||||
Detail: e.Detail,
|
||||
Category: e.Category.s,
|
||||
Fault: e.Fault.s,
|
||||
Remediation: e.Remediation.s,
|
||||
Attrs: e.Attrs,
|
||||
TraceID: e.TraceID,
|
||||
SpanID: e.SpanID,
|
||||
}
|
||||
if (e.Retry.Policy != RetryPolicy{}) {
|
||||
out.Retry = &RetryJSON{Policy: e.Retry.Policy.s}
|
||||
if e.Retry.Policy == RetryAfter && e.Retry.After > 0 {
|
||||
out.Retry.AfterMS = e.Retry.After.Milliseconds()
|
||||
}
|
||||
}
|
||||
if len(e.Refs) > 0 {
|
||||
out.Refs = make(map[string]string, len(e.Refs))
|
||||
for k, v := range e.Refs {
|
||||
out.Refs[k.s] = v
|
||||
}
|
||||
}
|
||||
if frames := e.Frames(); len(frames) > 0 {
|
||||
out.Frames = frames
|
||||
}
|
||||
if e.Cause != nil {
|
||||
out.Cause = causeToJSON(e.Cause)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func causeToJSON(err error) *CauseJSON {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if e, ok := err.(*Error); ok {
|
||||
c := &CauseJSON{Code: e.Code.s, Title: e.Title}
|
||||
if e.Cause != nil {
|
||||
c.Cause = causeToJSON(e.Cause)
|
||||
}
|
||||
return c
|
||||
}
|
||||
// Non-*Error leaf: only Title is set, no Code.
|
||||
return &CauseJSON{Title: err.Error()}
|
||||
}
|
||||
|
||||
// AsURLValues projects an error onto a flat url.Values, matching v1's shape
|
||||
// for callers (e.g. OAuth/SSO redirects) that smuggle errors back through a
|
||||
// query string. Complex fields (attrs, refs, retry, frames, cause) are
|
||||
// JSON-marshaled into a single value rather than spread across multiple
|
||||
// keys, since query strings have no good representation for nested data.
|
||||
func AsURLValues(cause error) url.Values {
|
||||
j := AsJSON(cause)
|
||||
if j == nil {
|
||||
return url.Values{}
|
||||
}
|
||||
v := url.Values{
|
||||
"code": {j.Code},
|
||||
"title": {j.Title},
|
||||
}
|
||||
if j.Detail != "" {
|
||||
v.Set("detail", j.Detail)
|
||||
}
|
||||
if j.Category != "" {
|
||||
v.Set("category", j.Category)
|
||||
}
|
||||
if j.Fault != "" {
|
||||
v.Set("fault", j.Fault)
|
||||
}
|
||||
if j.Remediation != "" {
|
||||
v.Set("remediation", j.Remediation)
|
||||
}
|
||||
if j.TraceID != "" {
|
||||
v.Set("trace_id", j.TraceID)
|
||||
}
|
||||
if j.SpanID != "" {
|
||||
v.Set("span_id", j.SpanID)
|
||||
}
|
||||
if j.Retry != nil {
|
||||
if b, err := json.Marshal(j.Retry); err == nil {
|
||||
v.Set("retry", string(b))
|
||||
}
|
||||
}
|
||||
if len(j.Refs) > 0 {
|
||||
if b, err := json.Marshal(j.Refs); err == nil {
|
||||
v.Set("refs", string(b))
|
||||
}
|
||||
}
|
||||
if len(j.Attrs) > 0 {
|
||||
if b, err := json.Marshal(j.Attrs); err == nil {
|
||||
v.Set("attrs", string(b))
|
||||
}
|
||||
}
|
||||
if j.Cause != nil {
|
||||
if b, err := json.Marshal(j.Cause); err == nil {
|
||||
v.Set("cause", string(b))
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
85
pkg/errors/v2/options.go
Normal file
85
pkg/errors/v2/options.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package v2
|
||||
|
||||
import "time"
|
||||
|
||||
// Option mutates an Error during construction. Options are applied after the
|
||||
// registered Meta defaults so a per-call WithFault wins over the code's
|
||||
// default Fault.
|
||||
type Option func(*Error)
|
||||
|
||||
// WithTitle overrides the title (used when Newf's formatted string is not
|
||||
// what you want, or after a Wrap that took its title from the cause).
|
||||
func WithTitle(s string) Option { return func(e *Error) { e.Title = s } }
|
||||
|
||||
// WithDetail adds a long, user-safe explanation. Detail must never include
|
||||
// raw cause text; the cause is already in the chain.
|
||||
func WithDetail(s string) Option { return func(e *Error) { e.Detail = s } }
|
||||
|
||||
// WithCategory overrides the registered Category.
|
||||
func WithCategory(c Category) Option { return func(e *Error) { e.Category = c } }
|
||||
|
||||
// WithFault overrides the registered Fault.
|
||||
func WithFault(f Fault) Option { return func(e *Error) { e.Fault = f } }
|
||||
|
||||
// WithRetry overrides the registered Retry.
|
||||
func WithRetry(r Retry) Option { return func(e *Error) { e.Retry = r } }
|
||||
|
||||
// WithRetryAfter is a convenience for the common RetryAfter case.
|
||||
func WithRetryAfter(d time.Duration) Option {
|
||||
return func(e *Error) { e.Retry = Retry{Policy: RetryAfter, After: d} }
|
||||
}
|
||||
|
||||
// WithRemediation overrides the registered Remediation.
|
||||
//
|
||||
// Convention for "did you mean" hints: stash a []string under
|
||||
// Attrs["suggestions"], ranked best-first. Each element should be a complete,
|
||||
// copy-pasteable replacement — not an explanation of what went wrong (use
|
||||
// WithDetail for that). Once 3-4 domains adopt the convention identically,
|
||||
// promote to a first-class field.
|
||||
func WithRemediation(r Remediation) Option { return func(e *Error) { e.Remediation = r } }
|
||||
|
||||
// WithRef adds (or replaces) a single reference URL keyed by kind.
|
||||
func WithRef(kind RefKind, url string) Option {
|
||||
return func(e *Error) {
|
||||
if e.Refs == nil {
|
||||
e.Refs = make(map[RefKind]string, 1)
|
||||
}
|
||||
e.Refs[kind] = url
|
||||
}
|
||||
}
|
||||
|
||||
// WithAttr sets a single typed attribute. Prefer typed per-domain helpers
|
||||
// (e.g. WithQueryAttrs(q Query)) over raw WithAttr at call sites — they keep
|
||||
// the attr keys consistent and let the compiler reject typos.
|
||||
func WithAttr(key string, value any) Option {
|
||||
return func(e *Error) {
|
||||
if e.Attrs == nil {
|
||||
e.Attrs = make(map[string]any, 1)
|
||||
}
|
||||
e.Attrs[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// WithAttrs merges a map of attributes; later keys win.
|
||||
func WithAttrs(attrs map[string]any) Option {
|
||||
return func(e *Error) {
|
||||
if len(attrs) == 0 {
|
||||
return
|
||||
}
|
||||
if e.Attrs == nil {
|
||||
e.Attrs = make(map[string]any, len(attrs))
|
||||
}
|
||||
for k, v := range attrs {
|
||||
e.Attrs[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithTrace stamps the error with OTel trace and span IDs so the JSON
|
||||
// response can link back to the originating span.
|
||||
func WithTrace(traceID, spanID string) Option {
|
||||
return func(e *Error) {
|
||||
e.TraceID = traceID
|
||||
e.SpanID = spanID
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,40 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
errors "github.com/SigNoz/signoz/pkg/errors/v2"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/havingexpression/grammar"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
)
|
||||
|
||||
// HAVING-expression validator codes. All three are caller-fault, fix-the-input
|
||||
// errors — the user wrote an expression we cannot turn into SQL — so retry
|
||||
// is pointless until the expression itself changes.
|
||||
var (
|
||||
codeHavingStringLiteral = errors.Register("querybuilder.having.string_literal", errors.Meta{
|
||||
Category: errors.CategoryInvalidInput,
|
||||
Fault: errors.FaultCaller,
|
||||
Retry: errors.Retry{Policy: errors.RetryAfterFix},
|
||||
Remediation: errors.RemediationFixInput,
|
||||
})
|
||||
codeHavingInvalidReference = errors.Register("querybuilder.having.invalid_reference", errors.Meta{
|
||||
Category: errors.CategoryInvalidInput,
|
||||
Fault: errors.FaultCaller,
|
||||
Retry: errors.Retry{Policy: errors.RetryAfterFix},
|
||||
Remediation: errors.RemediationFixInput,
|
||||
})
|
||||
codeHavingSyntaxError = errors.Register("querybuilder.having.syntax_error", errors.Meta{
|
||||
Category: errors.CategoryInvalidInput,
|
||||
Fault: errors.FaultCaller,
|
||||
Retry: errors.Retry{Policy: errors.RetryAfterFix},
|
||||
Remediation: errors.RemediationFixInput,
|
||||
})
|
||||
)
|
||||
|
||||
// havingExpressionRewriteVisitor walks the parse tree of a HavingExpression in a single
|
||||
// pass, simultaneously rewriting user-facing references to their SQL column names and
|
||||
// collecting any references that could not be resolved.
|
||||
@@ -281,10 +306,10 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
|
||||
// This is checked before invalid references so that "contains string literals" takes
|
||||
// priority when a bare string literal is also an unresolvable operand.
|
||||
if v.hasStringLiteral {
|
||||
return "", errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
return "", errors.New(codeHavingStringLiteral,
|
||||
"`Having` expression contains string literals",
|
||||
).WithAdditional("Aggregator results are numeric")
|
||||
errors.WithDetail("Aggregator results are numeric"),
|
||||
)
|
||||
}
|
||||
|
||||
if len(v.invalid) > 0 {
|
||||
@@ -294,7 +319,10 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
|
||||
validKeys = append(validKeys, k)
|
||||
}
|
||||
sort.Strings(validKeys)
|
||||
additional := []string{"Valid references are: [" + strings.Join(validKeys, ", ") + "]"}
|
||||
opts := []errors.Option{
|
||||
errors.WithAttr("invalid_refs", v.invalid),
|
||||
errors.WithAttr("valid_refs", validKeys),
|
||||
}
|
||||
if len(v.invalid) == 1 {
|
||||
inv := v.invalid[0]
|
||||
// Only suggest for plain identifier typos, not for unresolved function
|
||||
@@ -303,15 +331,13 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
|
||||
// a simple string substitution produce a corrupt expression.
|
||||
isFuncCall := strings.Contains(original, inv+"(")
|
||||
if match, dist := closestMatch(inv, validKeys); !isFuncCall && !strings.Contains(match, "(") && dist <= 3 {
|
||||
corrected := strings.ReplaceAll(original, inv, match)
|
||||
additional = append(additional, "Suggestion: `"+corrected+"`")
|
||||
opts = append(opts, errors.WithAttr("suggestions", []string{strings.ReplaceAll(original, inv, match)}))
|
||||
}
|
||||
}
|
||||
return "", errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"Invalid references in `Having` expression: [%s]",
|
||||
strings.Join(v.invalid, ", "),
|
||||
).WithAdditional(additional...)
|
||||
return "", errors.New(codeHavingInvalidReference,
|
||||
fmt.Sprintf("Invalid references in `Having` expression: [%s]", strings.Join(v.invalid, ", ")),
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
// Layer 3 – ANTLR syntax errors. We parse the original expression, so error messages
|
||||
@@ -328,17 +354,20 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
|
||||
if detail == "" {
|
||||
detail = "check the expression syntax"
|
||||
}
|
||||
additional := []string{detail}
|
||||
opts := []errors.Option{
|
||||
errors.WithDetail(detail),
|
||||
errors.WithAttr("syntax_errors", msgs),
|
||||
}
|
||||
// For single-error expressions, try to produce an actionable suggestion.
|
||||
if len(allSyntaxErrors) == 1 {
|
||||
if s := havingSuggestion(allSyntaxErrors[0], original); s != "" {
|
||||
additional = append(additional, "Suggestion: `"+s+"`")
|
||||
opts = append(opts, errors.WithAttr("suggestions", []string{s}))
|
||||
}
|
||||
}
|
||||
return "", errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
return "", errors.New(codeHavingSyntaxError,
|
||||
"Syntax error in `Having` expression",
|
||||
).WithAdditional(additional...)
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
Reference in New Issue
Block a user