mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-07-02 00:17:40 +08:00
feat(gotenberg): add ClassifyError with bounded error.type enum
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Engine-agnostic, low-cardinality error.type values shared by the conversion
|
||||
// engines. They are safe to use both as the semconv error.type span attribute
|
||||
// and as bounded metric label values.
|
||||
const (
|
||||
ErrorTypeTimeout = "timeout"
|
||||
ErrorTypeContextCancelled = "context_cancelled"
|
||||
ErrorTypeQueueSizeExceeded = "queue_size_exceeded"
|
||||
ErrorTypeProcessRestarting = "process_restarting"
|
||||
ErrorTypeInvalidInput = "invalid_input"
|
||||
ErrorTypeUnknown = "unknown"
|
||||
)
|
||||
|
||||
// ClassifyError maps err to a bounded, engine-agnostic error.type value. It
|
||||
// recognizes the failure modes shared by every engine: deadline, cancellation,
|
||||
// queue saturation, and process restart. It returns an empty string for a nil
|
||||
// error and [ErrorTypeUnknown] for anything it does not recognize, leaving
|
||||
// engine-specific refinement (such as [ErrorTypeInvalidInput]) to the caller.
|
||||
func ClassifyError(err error) string {
|
||||
switch {
|
||||
case err == nil:
|
||||
return ""
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return ErrorTypeTimeout
|
||||
case errors.Is(err, context.Canceled):
|
||||
return ErrorTypeContextCancelled
|
||||
case errors.Is(err, ErrMaximumQueueSizeExceeded):
|
||||
return ErrorTypeQueueSizeExceeded
|
||||
case errors.Is(err, ErrProcessAlreadyRestarting):
|
||||
return ErrorTypeProcessRestarting
|
||||
default:
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// SpanErrorType records errorType as the semconv error.type attribute on span.
|
||||
// It is a no-op when errorType is empty.
|
||||
func SpanErrorType(span trace.Span, errorType string) {
|
||||
if errorType == "" {
|
||||
return
|
||||
}
|
||||
span.SetAttributes(semconv.ErrorTypeKey.String(errorType))
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
func TestClassifyError(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, ""},
|
||||
{"deadline", context.DeadlineExceeded, ErrorTypeTimeout},
|
||||
{"canceled", context.Canceled, ErrorTypeContextCancelled},
|
||||
{"queue size exceeded", ErrMaximumQueueSizeExceeded, ErrorTypeQueueSizeExceeded},
|
||||
{"process restarting", ErrProcessAlreadyRestarting, ErrorTypeProcessRestarting},
|
||||
{"wrapped deadline", fmt.Errorf("convert: %w", context.DeadlineExceeded), ErrorTypeTimeout},
|
||||
{"joined queue", errors.Join(errors.New("attempt"), ErrMaximumQueueSizeExceeded), ErrorTypeQueueSizeExceeded},
|
||||
{"arbitrary", errors.New("boom"), ErrorTypeUnknown},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := ClassifyError(tc.err); got != tc.want {
|
||||
t.Errorf("ClassifyError(%v) = %q, want %q", tc.err, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpanErrorType(t *testing.T) {
|
||||
recorder := newTestSpanRecorder(t)
|
||||
|
||||
_, span := otel.Tracer("test").Start(context.Background(), "engine.Op")
|
||||
SpanErrorType(span, "") // no-op, must not add an attribute
|
||||
SpanErrorType(span, ErrorTypeTimeout) // sets error.type
|
||||
span.End()
|
||||
|
||||
got := findSpan(recorder, "engine.Op")
|
||||
if got == nil {
|
||||
t.Fatal("expected the span to be recorded")
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, kv := range got.Attributes() {
|
||||
if string(kv.Key) == "error.type" {
|
||||
count++
|
||||
if kv.Value.AsString() != ErrorTypeTimeout {
|
||||
t.Errorf("expected error.type=%q, got %q", ErrorTypeTimeout, kv.Value.AsString())
|
||||
}
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Errorf("expected exactly one error.type attribute, got %d", count)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user