mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-27 04:10:28 +01:00
Compare commits
2 Commits
chore/agen
...
tvats-pkg-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edf29e9434 | ||
|
|
0949b251e7 |
@@ -2401,8 +2401,20 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorsResponseerroradditional'
|
||||
type: array
|
||||
invalidReferences:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
message:
|
||||
type: string
|
||||
retry:
|
||||
$ref: '#/components/schemas/ErrorsResponseretryjson'
|
||||
suggestions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -2414,6 +2426,22 @@ components:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
ErrorsResponseretryjson:
|
||||
properties:
|
||||
delay:
|
||||
$ref: '#/components/schemas/TimeDuration'
|
||||
policy:
|
||||
$ref: '#/components/schemas/ErrorsResponseretrypolicy'
|
||||
type: object
|
||||
ErrorsResponseretrypolicy:
|
||||
enum:
|
||||
- never
|
||||
- immediate
|
||||
- backoff
|
||||
- after
|
||||
- after_fix
|
||||
- after_auth
|
||||
type: string
|
||||
FactoryResponse:
|
||||
properties:
|
||||
healthy:
|
||||
|
||||
@@ -2051,6 +2051,19 @@ export interface ErrorsResponseerroradditionalDTO {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export enum ErrorsResponseretrypolicyDTO {
|
||||
never = 'never',
|
||||
immediate = 'immediate',
|
||||
backoff = 'backoff',
|
||||
after = 'after',
|
||||
after_fix = 'after_fix',
|
||||
after_auth = 'after_auth',
|
||||
}
|
||||
export interface ErrorsResponseretryjsonDTO {
|
||||
delay?: TimeDurationDTO;
|
||||
policy?: ErrorsResponseretrypolicyDTO;
|
||||
}
|
||||
|
||||
export interface ErrorsJSONDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -2060,10 +2073,23 @@ export interface ErrorsJSONDTO {
|
||||
* @type array
|
||||
*/
|
||||
errors?: ErrorsResponseerroradditionalDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
invalidReferences?: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message: string;
|
||||
retry?: ErrorsResponseretryjsonDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
suggestions?: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"errors" //nolint:depguard
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
// base is the fundamental struct that implements the error interface.
|
||||
// The order of the struct is 'TCMEUAS'.
|
||||
type base struct {
|
||||
// t denotes the custom type of the error.
|
||||
t typ
|
||||
@@ -25,6 +25,12 @@ type base struct {
|
||||
a []string
|
||||
// s contains the stacktrace captured at error creation time.
|
||||
s fmt.Stringer
|
||||
// r is the retry strategy for the error, if applicable.
|
||||
r *retry
|
||||
// suggestions is a list of user-facing suggestions related to the error, if present.
|
||||
suggestions []string
|
||||
// invalidReferences is a list of references that were invalid and contributed to the error, if present.
|
||||
invalidReferences []string
|
||||
}
|
||||
|
||||
// Stacktrace returns the stacktrace captured at error creation time, formatted as a string.
|
||||
@@ -39,13 +45,16 @@ func (b *base) Stacktrace() string {
|
||||
// and returns a new base error.
|
||||
func (b *base) WithStacktrace(s string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: rawStacktrace(s),
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: rawStacktrace(s),
|
||||
r: b.r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,13 +122,16 @@ func WithAdditionalf(cause error, format string, args ...any) *base {
|
||||
s = original.s
|
||||
}
|
||||
b := &base{
|
||||
t: t,
|
||||
c: c,
|
||||
m: m,
|
||||
e: e,
|
||||
u: u,
|
||||
a: a,
|
||||
s: s,
|
||||
t: t,
|
||||
c: c,
|
||||
m: m,
|
||||
e: e,
|
||||
u: u,
|
||||
a: a,
|
||||
s: s,
|
||||
r: retryOf(cause),
|
||||
suggestions: suggestionsOf(cause),
|
||||
invalidReferences: invalidReferencesOf(cause),
|
||||
}
|
||||
|
||||
return b.WithAdditional(append(a, fmt.Sprintf(format, args...))...)
|
||||
@@ -128,29 +140,113 @@ func WithAdditionalf(cause error, format string, args ...any) *base {
|
||||
// WithUrl adds a url to the base error and returns a new base error.
|
||||
func (b *base) WithUrl(u string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
r: b.r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdditional adds additional messages to the base error and returns a new base error.
|
||||
func (b *base) WithAdditional(a ...string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: a,
|
||||
s: b.s,
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: a,
|
||||
s: b.s,
|
||||
r: b.r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// withRetry adds retry metadata to the base error and returns a new base error.
|
||||
func (b *base) withRetry(r retry) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
r: &r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// WithSuggestions replaces the list of suggestions on the base error.
|
||||
func (b *base) WithSuggestions(suggestions ...string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
r: b.r,
|
||||
suggestions: suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// WithInvalidReferences replaces the list of invalid references on the base error.
|
||||
func (b *base) WithInvalidReferences(invalidReferences ...string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
r: b.r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryNever sets the retry policy to Never.
|
||||
func (b *base) WithRetryNever() *base {
|
||||
return b.withRetry(retry{policy: RetryNever})
|
||||
}
|
||||
|
||||
// WithRetryImmediate sets the retry policy to Immediate.
|
||||
func (b *base) WithRetryImmediate() *base {
|
||||
return b.withRetry(retry{policy: RetryImmediate})
|
||||
}
|
||||
|
||||
// WithRetryBackoff sets the retry policy to Backoff.
|
||||
func (b *base) WithRetryBackoff() *base {
|
||||
return b.withRetry(retry{policy: RetryBackoff})
|
||||
}
|
||||
|
||||
// WithRetryAfter sets the retry policy to After and requires a delay.
|
||||
func (b *base) WithRetryAfter(delay time.Duration) *base {
|
||||
return b.withRetry(newRetryAfter(delay))
|
||||
}
|
||||
|
||||
// WithRetryAfterFix sets the retry policy to AfterFix.
|
||||
func (b *base) WithRetryAfterFix() *base {
|
||||
return b.withRetry(retry{policy: RetryAfterFix})
|
||||
}
|
||||
|
||||
// WithRetryAfterAuth sets the retry policy to AfterAuth.
|
||||
func (b *base) WithRetryAfterAuth() *base {
|
||||
return b.withRetry(retry{policy: RetryAfterAuth})
|
||||
}
|
||||
|
||||
// Unwrapb is a combination of built-in errors.As and type casting.
|
||||
// It finds the first error in cause that matches base,
|
||||
// and if one is found, returns the individual fields of base.
|
||||
@@ -198,57 +294,67 @@ func Is(err error, target error) bool {
|
||||
|
||||
// WrapNotFoundf is a wrapper around Wrapf with TypeNotFound.
|
||||
func WrapNotFoundf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeNotFound, code, format, args...)
|
||||
return Wrapf(cause, TypeNotFound, code, format, args...).withRetry(retry{policy: RetryNever})
|
||||
}
|
||||
|
||||
// NewNotFoundf is a wrapper around Newf with TypeNotFound.
|
||||
func NewNotFoundf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeNotFound, code, format, args...)
|
||||
return Newf(TypeNotFound, code, format, args...).withRetry(retry{policy: RetryNever})
|
||||
}
|
||||
|
||||
// WrapInternalf is a wrapper around Wrapf with TypeInternal.
|
||||
func WrapInternalf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeInternal, code, format, args...)
|
||||
return Wrapf(cause, TypeInternal, code, format, args...).withRetry(retry{policy: RetryNever})
|
||||
}
|
||||
|
||||
// NewInternalf is a wrapper around Newf with TypeInternal.
|
||||
func NewInternalf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeInternal, code, format, args...)
|
||||
return Newf(TypeInternal, code, format, args...).withRetry(retry{policy: RetryNever})
|
||||
}
|
||||
|
||||
// WrapInvalidInputf is a wrapper around Wrapf with TypeInvalidInput.
|
||||
func WrapInvalidInputf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeInvalidInput, code, format, args...)
|
||||
return Wrapf(cause, TypeInvalidInput, code, format, args...).withRetry(retry{policy: RetryAfterFix})
|
||||
}
|
||||
|
||||
// NewInvalidInputf is a wrapper around Newf with TypeInvalidInput.
|
||||
func NewInvalidInputf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeInvalidInput, code, format, args...)
|
||||
}
|
||||
|
||||
// WrapUnexpectedf is a wrapper around Wrapf with TypeUnexpected.
|
||||
func WrapUnexpectedf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeInvalidInput, code, format, args...)
|
||||
}
|
||||
|
||||
// NewUnexpectedf is a wrapper around Newf with TypeUnexpected.
|
||||
func NewUnexpectedf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeInvalidInput, code, format, args...)
|
||||
return Newf(TypeInvalidInput, code, format, args...).withRetry(retry{policy: RetryAfterFix})
|
||||
}
|
||||
|
||||
// NewMethodNotAllowedf is a wrapper around Newf with TypeMethodNotAllowed.
|
||||
func NewMethodNotAllowedf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeMethodNotAllowed, code, format, args...)
|
||||
return Newf(TypeMethodNotAllowed, code, format, args...).withRetry(retry{policy: RetryNever})
|
||||
}
|
||||
|
||||
// WrapTimeoutf is a wrapper around Wrapf with TypeTimeout.
|
||||
func WrapTimeoutf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeTimeout, code, format, args...)
|
||||
return Wrapf(cause, TypeTimeout, code, format, args...).withRetry(retry{policy: RetryBackoff})
|
||||
}
|
||||
|
||||
// NewTimeoutf is a wrapper around Newf with TypeTimeout.
|
||||
func NewTimeoutf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeTimeout, code, format, args...)
|
||||
return Newf(TypeTimeout, code, format, args...).withRetry(retry{policy: RetryBackoff})
|
||||
}
|
||||
|
||||
// WrapUnauthenticatedf is a wrapper around Wrapf with TypeUnauthenticated.
|
||||
func WrapUnauthenticatedf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeUnauthenticated, code, format, args...).withRetry(retry{policy: RetryAfterAuth})
|
||||
}
|
||||
|
||||
// NewUnauthenticatedf is a wrapper around Newf with TypeUnauthenticated.
|
||||
func NewUnauthenticatedf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeUnauthenticated, code, format, args...).withRetry(retry{policy: RetryAfterAuth})
|
||||
}
|
||||
|
||||
// WrapForbiddenf is a wrapper around Wrapf with TypeForbidden.
|
||||
func WrapForbiddenf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeForbidden, code, format, args...).withRetry(retry{policy: RetryNever})
|
||||
}
|
||||
|
||||
// NewForbiddenf is a wrapper around Newf with TypeForbidden.
|
||||
func NewForbiddenf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeForbidden, code, format, args...).withRetry(retry{policy: RetryNever})
|
||||
}
|
||||
|
||||
// Attr returns an slog.Attr with a standardized "exception" key for the given error.
|
||||
@@ -262,3 +368,36 @@ func TypeAttr(err error) attribute.KeyValue {
|
||||
t, _, _, _, _, _ := Unwrapb(err)
|
||||
return attribute.String("error.type", t.String())
|
||||
}
|
||||
|
||||
// RetryAfterOf returns the explicit retry delay
|
||||
func RetryDelayOf(err error) time.Duration {
|
||||
base, ok := err.(*base)
|
||||
if !ok || base.r == nil || base.r.policy != RetryAfter {
|
||||
return 0
|
||||
}
|
||||
return base.r.delay
|
||||
}
|
||||
|
||||
func retryOf(err error) *retry {
|
||||
base, ok := err.(*base)
|
||||
if ok {
|
||||
return base.r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func suggestionsOf(err error) []string {
|
||||
base, ok := err.(*base)
|
||||
if ok {
|
||||
return base.suggestions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func invalidReferencesOf(err error) []string {
|
||||
base, ok := err.(*base)
|
||||
if ok {
|
||||
return base.invalidReferences
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package errors
|
||||
import (
|
||||
"errors" //nolint:depguard
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
@@ -59,6 +61,124 @@ func TestAttr(t *testing.T) {
|
||||
assert.Equal(t, err, attr.Value.Any())
|
||||
}
|
||||
|
||||
func TestWithSuggestions(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithSuggestions("try this")
|
||||
assert.Equal(t, []string{"try this"}, suggestionsOf(err))
|
||||
|
||||
// WithSuggestions replaces the existing list.
|
||||
err = err.WithSuggestions("try this instead")
|
||||
assert.Equal(t, []string{"try this instead"}, suggestionsOf(err))
|
||||
|
||||
// Variadic form replaces with multiple entries.
|
||||
err = err.WithSuggestions("first", "second")
|
||||
assert.Equal(t, []string{"first", "second"}, suggestionsOf(err))
|
||||
}
|
||||
|
||||
func TestWithRetryNever(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryNever()
|
||||
assert.Equal(t, RetryNever, retryOf(err).policy)
|
||||
}
|
||||
|
||||
func TestWithRetryImmediate(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryImmediate()
|
||||
assert.Equal(t, RetryImmediate, retryOf(err).policy)
|
||||
}
|
||||
|
||||
func TestWithRetryBackoff(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryBackoff()
|
||||
assert.Equal(t, RetryBackoff, retryOf(err).policy)
|
||||
}
|
||||
|
||||
func TestWithRetryAfter(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryAfter(5 * time.Microsecond)
|
||||
r := retryOf(err)
|
||||
|
||||
assert.Equal(t, RetryAfter, r.policy)
|
||||
assert.Equal(t, 5, int(r.delay.Microseconds()))
|
||||
}
|
||||
|
||||
func TestWithRetryAfterFix(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryAfterFix()
|
||||
assert.Equal(t, RetryAfterFix, retryOf(err).policy)
|
||||
}
|
||||
|
||||
func TestWithRetryAfterAuth(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryAfterAuth()
|
||||
assert.Equal(t, RetryAfterAuth, retryOf(err).policy)
|
||||
}
|
||||
|
||||
func TestWithInvalidReferences(t *testing.T) {
|
||||
// WithInvalidReferences populates the list.
|
||||
err := New(TypeInvalidInput, MustNewCode("bad_ref"), "bad ref").
|
||||
WithInvalidReferences("queries[0]", "queries[1]")
|
||||
assert.Equal(t, []string{"queries[0]", "queries[1]"}, invalidReferencesOf(err))
|
||||
|
||||
// WithInvalidReferences replaces the entire list on each call.
|
||||
err = err.WithInvalidReferences("queries[2]")
|
||||
assert.Equal(t, []string{"queries[2]"}, invalidReferencesOf(err),
|
||||
"WithInvalidReferences must replace the entire list")
|
||||
}
|
||||
|
||||
func TestAsJSONBaseError(t *testing.T) {
|
||||
err := New(TypeInvalidInput, MustNewCode("bad_input"), "field foo is bad").
|
||||
WithUrl("https://docs/bad_input").
|
||||
WithAdditional("hint1", "hint2").
|
||||
WithSuggestions("try this").
|
||||
WithInvalidReferences("queries[0]")
|
||||
|
||||
j := AsJSON(err)
|
||||
|
||||
assert.Equal(t, "invalid-input", j.Type)
|
||||
assert.Equal(t, "bad_input", j.Code)
|
||||
assert.Equal(t, "field foo is bad", j.Message)
|
||||
assert.Equal(t, "https://docs/bad_input", j.Url)
|
||||
assert.Equal(t, []responseerroradditional{{Message: "hint1"}, {Message: "hint2"}}, j.Errors)
|
||||
|
||||
// InvalidInput auto-applies the after_fix policy via NewInvalidInputf — but
|
||||
// New (bare constructor) does not. The retry block should reflect that.
|
||||
assert.Nil(t, j.Retry, "bare New(...) should not populate a retry block")
|
||||
|
||||
assert.Equal(t, []string{"try this"}, j.Suggestions)
|
||||
assert.Equal(t, []string{"queries[0]"}, j.InvalidReferences)
|
||||
}
|
||||
|
||||
func TestAsJSONRetryBlock(t *testing.T) {
|
||||
t.Run("RetryAfterIncludesDuration", func(t *testing.T) {
|
||||
err := NewTimeoutf(MustNewCode("slow"), "slow").WithRetryAfter(5 * time.Second)
|
||||
j := AsJSON(err)
|
||||
require.NotNil(t, j.Retry)
|
||||
assert.Equal(t, responseretrypolicy(RetryAfter), j.Retry.Policy)
|
||||
assert.Equal(t, "5s", j.Retry.Delay)
|
||||
})
|
||||
|
||||
t.Run("NonAfterPolicyOmitsDurationField", func(t *testing.T) {
|
||||
// NewInvalidInputf auto-applies retryAfterFix via the constructor helper.
|
||||
err := NewInvalidInputf(MustNewCode("bad"), "bad")
|
||||
j := AsJSON(err)
|
||||
require.NotNil(t, j.Retry)
|
||||
assert.Equal(t, responseretrypolicy(RetryAfterFix), j.Retry.Policy)
|
||||
assert.Empty(t, j.Retry.Delay, "delay must be empty when policy != after")
|
||||
})
|
||||
|
||||
t.Run("BareErrorOmitsRetryBlock", func(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("boom"), "boom")
|
||||
j := AsJSON(err)
|
||||
assert.Nil(t, j.Retry, "bare New(...) without WithRetry* must omit retry")
|
||||
})
|
||||
|
||||
t.Run("NonBaseErrorOmitsRetryBlock", func(t *testing.T) {
|
||||
// Stdlib errors carry no retry metadata; AsJSON omits the retry block.
|
||||
j := AsJSON(errors.New("plain stdlib error"))
|
||||
assert.Nil(t, j.Retry, "non-base errors must omit the retry block")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsJSONOptionalFieldsOmittedWhenEmpty(t *testing.T) {
|
||||
j := AsJSON(New(TypeInternal, MustNewCode("boom"), "boom"))
|
||||
assert.Nil(t, j.Suggestions, "no suggestions set => Suggestions must be nil so json omitempty drops it")
|
||||
assert.Nil(t, j.InvalidReferences, "no invalid references set => InvalidReferences must be nil so json omitempty drops it")
|
||||
}
|
||||
|
||||
func TestWithStacktrace(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "panic").WithStacktrace("custom stack trace")
|
||||
|
||||
|
||||
@@ -3,13 +3,38 @@ package errors
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JSON struct {
|
||||
Code string `json:"code" required:"true"`
|
||||
Message string `json:"message" required:"true"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Errors []responseerroradditional `json:"errors,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Code string `json:"code" required:"true"`
|
||||
Message string `json:"message" required:"true"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Errors []responseerroradditional `json:"errors,omitempty"`
|
||||
Retry *responseretryjson `json:"retry,omitempty"`
|
||||
Suggestions []string `json:"suggestions,omitempty"`
|
||||
InvalidReferences []string `json:"invalidReferences,omitempty"`
|
||||
}
|
||||
|
||||
type responseretryjson struct {
|
||||
Policy responseretrypolicy `json:"policy"`
|
||||
Delay time.Duration `json:"delay,omitempty"`
|
||||
}
|
||||
|
||||
type responseretrypolicy string
|
||||
|
||||
func (r responseretrypolicy) String() string { return string(r) }
|
||||
|
||||
func (responseretrypolicy) Enum() []any {
|
||||
return []any{
|
||||
RetryNever,
|
||||
RetryImmediate,
|
||||
RetryBackoff,
|
||||
RetryAfter,
|
||||
RetryAfterFix,
|
||||
RetryAfterAuth,
|
||||
}
|
||||
}
|
||||
|
||||
type responseerroradditional struct {
|
||||
@@ -18,18 +43,30 @@ type responseerroradditional struct {
|
||||
|
||||
func AsJSON(cause error) *JSON {
|
||||
// See if this is an instance of the base error or not
|
||||
_, c, m, _, u, a := Unwrapb(cause)
|
||||
t, c, m, _, u, a := Unwrapb(cause)
|
||||
|
||||
rea := make([]responseerroradditional, len(a))
|
||||
for k, v := range a {
|
||||
rea[k] = responseerroradditional{v}
|
||||
}
|
||||
|
||||
var retry *responseretryjson
|
||||
if r := retryOf(cause); r != nil {
|
||||
retry = &responseretryjson{Policy: responseretrypolicy(r.policy)}
|
||||
if r.policy == RetryAfter {
|
||||
retry.Delay = r.delay
|
||||
}
|
||||
}
|
||||
|
||||
return &JSON{
|
||||
Code: c.String(),
|
||||
Message: m,
|
||||
Url: u,
|
||||
Errors: rea,
|
||||
Type: t.String(),
|
||||
Code: c.String(),
|
||||
Message: m,
|
||||
Url: u,
|
||||
Errors: rea,
|
||||
Retry: retry,
|
||||
Suggestions: suggestionsOf(cause),
|
||||
InvalidReferences: invalidReferencesOf(cause),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
pkg/errors/retry.go
Normal file
31
pkg/errors/retry.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type RetryPolicy string
|
||||
|
||||
const (
|
||||
RetryNever RetryPolicy = "never" // retry with the same inputs cannot succeed.
|
||||
RetryImmediate RetryPolicy = "immediate" // retry without waiting.
|
||||
RetryBackoff RetryPolicy = "backoff" // caller picks its own backoff schedule.
|
||||
RetryAfter RetryPolicy = "after" // honor Retry.After exactly (the producer knows the wait).
|
||||
RetryAfterFix RetryPolicy = "after_fix" // retry pointless until the caller fixes the request.
|
||||
RetryAfterAuth RetryPolicy = "after_auth" // retry pointless until the caller re-authenticates.
|
||||
)
|
||||
|
||||
// retry pairs a RetryPolicy with the canonical gRPC RetryInfo detail.
|
||||
// info is non-nil only when policy == RetryAfter.
|
||||
type retry struct {
|
||||
policy RetryPolicy
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// newRetryAfter builds a retry value carrying a gRPC RetryInfo with the given delay.
|
||||
func newRetryAfter(d time.Duration) retry {
|
||||
return retry{
|
||||
policy: RetryAfter,
|
||||
delay: d,
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,7 @@ var (
|
||||
TypeForbidden = typ{"forbidden"}
|
||||
TypeCanceled = typ{"canceled"}
|
||||
TypeTimeout = typ{"timeout"}
|
||||
TypeUnexpected = typ{"unexpected"} // Generic mismatch of expectations
|
||||
TypeFatal = typ{"fatal"} // Unrecoverable failure (e.g. panic)
|
||||
TypeFatal = typ{"fatal"} // Unrecoverable failure (e.g. panic)
|
||||
TypeLicenseUnavailable = typ{"license-unavailable"}
|
||||
TypeTooManyRequests = typ{"too-many-requests"}
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
@@ -121,6 +123,14 @@ func Error(rw http.ResponseWriter, cause error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Retry-After carries the explicit delay declared via
|
||||
// errors.WithRetryAfter. Set it before WriteHeader so headers go on the wire.
|
||||
d := errors.RetryDelayOf(cause)
|
||||
if d.Seconds() > 0 {
|
||||
rw.Header().Set("Retry-After", strconv.Itoa(int(math.Ceil(d.Seconds()))))
|
||||
|
||||
}
|
||||
|
||||
rw.WriteHeader(httpCode)
|
||||
_, _ = rw.Write(body)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -97,13 +99,13 @@ func TestError(t *testing.T) {
|
||||
name: "AlreadyExists",
|
||||
statusCode: http.StatusConflict,
|
||||
err: errors.New(errors.TypeAlreadyExists, errors.MustNewCode("already_exists"), "already exists").WithUrl("https://already_exists"),
|
||||
expected: []byte(`{"status":"error","error":{"code":"already_exists","message":"already exists","url":"https://already_exists"}}`),
|
||||
expected: []byte(`{"status":"error","error":{"type":"already-exists","code":"already_exists","message":"already exists","url":"https://already_exists"}}`),
|
||||
},
|
||||
"/unauthenticated": {
|
||||
name: "Unauthenticated",
|
||||
statusCode: http.StatusUnauthorized,
|
||||
err: errors.New(errors.TypeUnauthenticated, errors.MustNewCode("not_allowed"), "not allowed").WithUrl("https://unauthenticated").WithAdditional("a1", "a2"),
|
||||
expected: []byte(`{"status":"error","error":{"code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`),
|
||||
expected: []byte(`{"status":"error","error":{"type":"unauthenticated","code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -145,3 +147,79 @@ func TestError(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestErrorRetryAfterHeader verifies that the HTTP Retry-After header is set
|
||||
// when (and only when) the error declares an explicit WithRetryAfter delay.
|
||||
// Other retry policies (backoff, after_fix, after_auth, never) and bare errors
|
||||
// without any policy must NOT emit the header — clients ignore non-numeric or
|
||||
// missing values, but emitting one wrongly would mislead retry libraries.
|
||||
func TestErrorRetryAfterHeader(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
name string
|
||||
err error
|
||||
wantRetryAfter string // expected header value; "" means header must be absent
|
||||
wantBodyContains string // substring that must appear in the JSON body
|
||||
wantBodyNotContains string // substring that must NOT appear in the JSON body
|
||||
}{
|
||||
"/with_retry_after_5s": {
|
||||
name: "ExplicitDelay5Seconds",
|
||||
err: errors.New(errors.TypeTooManyRequests, errors.MustNewCode("rate_limited"), "slow down").WithRetryAfter(5 * time.Second),
|
||||
wantRetryAfter: "5",
|
||||
wantBodyContains: `"retry":{"policy":"after","delay":5000000000}`,
|
||||
},
|
||||
"/with_retry_after_subsecond": {
|
||||
name: "SubSecondRoundsUp",
|
||||
err: errors.New(errors.TypeTooManyRequests, errors.MustNewCode("rate_limited"), "slow down").WithRetryAfter(500 * time.Millisecond),
|
||||
wantRetryAfter: "1", // ceiling-rounded
|
||||
wantBodyContains: `"delay":500000000`,
|
||||
},
|
||||
"/timeout_uses_backoff": {
|
||||
name: "BackoffPolicyNoHeader",
|
||||
err: errors.NewTimeoutf(errors.MustNewCode("slow_query"), "query timed out"),
|
||||
wantRetryAfter: "",
|
||||
wantBodyContains: `"retry":{"policy":"backoff"}`,
|
||||
},
|
||||
"/invalid_input_after_fix": {
|
||||
name: "AfterFixPolicyNoHeader",
|
||||
err: errors.NewInvalidInputf(errors.MustNewCode("bad_field"), "bad field"),
|
||||
wantRetryAfter: "",
|
||||
wantBodyContains: `"retry":{"policy":"after_fix"}`,
|
||||
},
|
||||
"/bare_no_policy": {
|
||||
name: "BareErrorNoHeaderNoRetryBlock",
|
||||
err: errors.New(errors.TypeInternal, errors.MustNewCode("boom"), "boom"),
|
||||
wantRetryAfter: "",
|
||||
wantBodyContains: `"code":"boom"`,
|
||||
wantBodyNotContains: `"retry"`,
|
||||
},
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if tc, ok := testCases[req.URL.Path]; ok {
|
||||
Error(rw, tc.err)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
for path, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := http.Get(srv.URL + path)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, res.Body.Close()) }()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantRetryAfter, res.Header.Get("Retry-After"),
|
||||
"Retry-After header for %s", tc.name)
|
||||
if tc.wantBodyContains != "" {
|
||||
assert.Contains(t, string(body), tc.wantBodyContains,
|
||||
"body should contain %q for %s", tc.wantBodyContains, tc.name)
|
||||
}
|
||||
if tc.wantBodyNotContains != "" {
|
||||
assert.NotContains(t, string(body), tc.wantBodyNotContains,
|
||||
"body should NOT contain %q for %s", tc.wantBodyNotContains, tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,15 +202,15 @@ func (handler *handler) exportRawDataJSONL(rowChan <-chan *qbtypes.RawRow, errCh
|
||||
}
|
||||
jsonBytes, err := json.Marshal(row.Data)
|
||||
if err != nil {
|
||||
return false, errors.NewUnexpectedf(errors.CodeInternal, "error marshaling JSON: %s", err)
|
||||
return false, errors.NewInternalf(errors.CodeInternal, "error marshaling JSON: %s", err)
|
||||
}
|
||||
totalBytes += uint64(len(jsonBytes)) + 1
|
||||
|
||||
if _, err := writer.Write(jsonBytes); err != nil {
|
||||
return false, errors.NewUnexpectedf(errors.CodeInternal, "error writing JSON: %s", err)
|
||||
return false, errors.NewInternalf(errors.CodeInternal, "error writing JSON: %s", err)
|
||||
}
|
||||
if _, err := writer.Write([]byte("\n")); err != nil {
|
||||
return false, errors.NewUnexpectedf(errors.CodeInternal, "error writing JSON newline: %s", err)
|
||||
return false, errors.NewInternalf(errors.CodeInternal, "error writing JSON newline: %s", err)
|
||||
}
|
||||
|
||||
if totalBytes > MaxExportBytesLimit {
|
||||
|
||||
@@ -74,7 +74,7 @@ func (module *getter) ListDeprecatedUsersByOrgID(ctx context.Context, orgID valu
|
||||
roleNames := userIDToRoleNames[user.ID]
|
||||
|
||||
if len(roleNames) == 0 {
|
||||
return nil, errors.Newf(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found for user: %s", user.ID.String())
|
||||
return nil, errors.Newf(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found for user: %s", user.ID.String())
|
||||
}
|
||||
|
||||
role := authtypes.SigNozManagedRoleToExistingLegacyRole[roleNames[0]]
|
||||
@@ -113,11 +113,11 @@ func (module *getter) GetDeprecatedUserByOrgIDAndID(ctx context.Context, orgID v
|
||||
}
|
||||
|
||||
if len(userRoles) == 0 {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
}
|
||||
|
||||
if userRoles[0].Role == nil {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
}
|
||||
|
||||
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
|
||||
@@ -141,11 +141,11 @@ func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.Deprecate
|
||||
}
|
||||
|
||||
if len(userRoles) == 0 {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
}
|
||||
|
||||
if userRoles[0].Role == nil {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
}
|
||||
|
||||
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
|
||||
@@ -211,7 +211,7 @@ func (module *getter) GetRolesByUserID(ctx context.Context, userID valuer.UUID)
|
||||
|
||||
for _, ur := range userRoles {
|
||||
if ur.Role == nil {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ func getOperators(ops []pipelinetypes.PipelineOperator) ([]pipelinetypes.Pipelin
|
||||
|
||||
func processSeverityParser(operator *pipelinetypes.PipelineOperator) error {
|
||||
if operator.Type != "severity_parser" {
|
||||
return errors.NewUnexpectedf(CodeInvalidOperatorType, "operator type received %s", operator.Type)
|
||||
return errors.NewInternalf(CodeInvalidOperatorType, "operator type received %s", operator.Type)
|
||||
}
|
||||
|
||||
parseFromNotNilCheck, err := fieldNotNilCheck(operator.ParseFrom)
|
||||
@@ -236,7 +236,7 @@ func processSeverityParser(operator *pipelinetypes.PipelineOperator) error {
|
||||
// processJSONParser converts simple JSON parser operator into multiple operators for JSONMapping of default variables
|
||||
func processJSONParser(parent *pipelinetypes.PipelineOperator) ([]pipelinetypes.PipelineOperator, error) {
|
||||
if parent.Type != "json_parser" {
|
||||
return nil, errors.NewUnexpectedf(CodeInvalidOperatorType, "operator type received %s", parent.Type)
|
||||
return nil, errors.NewInternalf(CodeInvalidOperatorType, "operator type received %s", parent.Type)
|
||||
}
|
||||
|
||||
parseFromNotNilCheck, err := fieldNotNilCheck(parent.ParseFrom)
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewContextWithClaims(ctx context.Context, claims Claims) context.Context {
|
||||
func ClaimsFromContext(ctx context.Context) (Claims, error) {
|
||||
claims, ok := ctx.Value(claimsKey{}).(Claims)
|
||||
if !ok {
|
||||
return Claims{}, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
|
||||
return Claims{}, errors.NewUnauthenticatedf(errors.CodeUnauthenticated, "unauthenticated")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
@@ -42,7 +42,7 @@ func NewContextWithAccessToken(ctx context.Context, accessToken string) context.
|
||||
func AccessTokenFromContext(ctx context.Context) (string, error) {
|
||||
accessToken, ok := ctx.Value(accessTokenKey{}).(string)
|
||||
if !ok {
|
||||
return "", errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
|
||||
return "", errors.NewUnauthenticatedf(errors.CodeUnauthenticated, "unauthenticated")
|
||||
}
|
||||
|
||||
return accessToken, nil
|
||||
@@ -55,7 +55,7 @@ func NewContextWithAPIKey(ctx context.Context, apiKey string) context.Context {
|
||||
func APIKeyFromContext(ctx context.Context) (string, error) {
|
||||
apiKey, ok := ctx.Value(apiKeyKey{}).(string)
|
||||
if !ok {
|
||||
return "", errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
|
||||
return "", errors.NewUnauthenticatedf(errors.CodeUnauthenticated, "unauthenticated")
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
@@ -77,7 +77,7 @@ func (c *Claims) IsSelfAccess(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "only the user/admin can access their own resource")
|
||||
return errors.NewForbiddenf(errors.CodeForbidden, "only the user/admin can access their own resource")
|
||||
}
|
||||
|
||||
func (c *Claims) IdentityID() string {
|
||||
|
||||
Reference in New Issue
Block a user