Skip to content

Commit 41e8af2

Browse files
committed
Add new otlp-centric error type
1 parent 6928951 commit 41e8af2

File tree

8 files changed

+549
-3
lines changed

8 files changed

+549
-3
lines changed

consumer/consumererror/error.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package consumererror // import "go.opentelemetry.io/collector/consumer/consumererror"
5+
6+
import (
7+
"net/http"
8+
9+
"google.golang.org/grpc/codes"
10+
"google.golang.org/grpc/status"
11+
12+
"go.opentelemetry.io/collector/consumer/consumererror/internal/statusconversion"
13+
)
14+
15+
// Error is intended to be used to encapsulate various information that can add
16+
// context to an error that occurred within a pipeline component. Error objects
17+
// are constructed through calling `New` with the relevant options to capture
18+
// data around the error that occurred.
19+
//
20+
// It may hold multiple errors from downstream components, and can be merged
21+
// with other errors as it travels upstream using `Combine`. The `Error` should
22+
// be obtained from a given `error` object using `errors.As`.
23+
//
24+
// Experimental: This API is at the early stage of development and may change
25+
// without backward compatibility
26+
type Error struct {
27+
error
28+
httpStatus int
29+
grpcStatus *status.Status
30+
retryable bool
31+
}
32+
33+
var _ error = (*Error)(nil)
34+
35+
// ErrorOption allows annotating an Error with metadata.
36+
type ErrorOption interface {
37+
applyOption(*Error)
38+
}
39+
40+
type errorOptionFunc func(*Error)
41+
42+
func (f errorOptionFunc) applyOption(e *Error) {
43+
f(e)
44+
}
45+
46+
// New wraps an error that happened while consuming telemetry and adds metadata
47+
// onto it to be passed back up the pipeline.
48+
// At least one option should be provided.
49+
//
50+
// Experimental: This API is at the early stage of development and may change
51+
// without backward compatibility
52+
func New(origErr error, options ...ErrorOption) error {
53+
err := &Error{error: origErr}
54+
55+
for _, option := range options {
56+
option.applyOption(err)
57+
}
58+
59+
return err
60+
}
61+
62+
// WithOTLPHTTPStatus records an HTTP status code that was received from a server
63+
// during data submission.
64+
// It is not necessary to use WithRetryable with creating an error with WithOTLPHTTPStatus
65+
// as the retryable property can be inferred from the HTTP status code using OTLP specification.
66+
//
67+
// Experimental: This API is at the early stage of development and may change
68+
// without backward compatibility
69+
func WithOTLPHTTPStatus(status int) ErrorOption {
70+
return errorOptionFunc(func(err *Error) {
71+
err.httpStatus = status
72+
})
73+
}
74+
75+
// WithOTLPGRPCStatus records a gRPC status code that was received from a server
76+
// during data submission.
77+
// It is not necessary to use WithRetryable with creating an error with WithOTLPGRPCStatus
78+
// as the retryable property can be inferred from the grpc status using OTLP specification.
79+
//
80+
// Experimental: This API is at the early stage of development and may change
81+
// without backward compatibility
82+
func WithOTLPGRPCStatus(status *status.Status) ErrorOption {
83+
return errorOptionFunc(func(err *Error) {
84+
err.grpcStatus = status
85+
})
86+
}
87+
88+
// WithRetryable records that this error is retryable according to OTLP specification.
89+
// WithRetryable is not necessary when creating an error with WithOTLPHTTPStatus or
90+
// WithOTLPGRPCStatus, as the retryable property can be inferred from OTLP specification.
91+
//
92+
// Experimental: This API is at the early stage of development and may change
93+
// without backward compatibility
94+
func WithRetryable() ErrorOption {
95+
return errorOptionFunc(func(err *Error) {
96+
err.retryable = true
97+
})
98+
}
99+
100+
// Error implements the error interface.
101+
func (e *Error) Error() string {
102+
return e.error.Error()
103+
}
104+
105+
// Unwrap returns the wrapped error for use by `errors.Is` and `errors.As`.
106+
func (e *Error) Unwrap() error {
107+
return e.error
108+
}
109+
110+
// OTLPHTTPStatus returns an HTTP status code either directly set by the source,
111+
// derived from a gRPC status code set by the source, or derived from Retryable.
112+
// When deriving the value, the OTLP specification is used to map to HTTP.
113+
// See https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md for more details.
114+
//
115+
// If a http status code cannot be derived from these three sources then 500 is returned.
116+
//
117+
// Experimental: This API is at the early stage of development and may change
118+
// without backward compatibility
119+
func (e *Error) OTLPHTTPStatus() int {
120+
if e.httpStatus != 0 {
121+
return e.httpStatus
122+
}
123+
if e.grpcStatus != nil {
124+
return statusconversion.GetHTTPStatusCodeFromStatus(e.grpcStatus)
125+
}
126+
if e.retryable {
127+
return http.StatusServiceUnavailable
128+
}
129+
return http.StatusInternalServerError
130+
}
131+
132+
// OTLPGRPCStatus returns an gRPC status code either directly set by the source,
133+
// derived from an HTTP status code set by the source, or derived from Retryable.
134+
// When deriving the value, the OTLP specification is used to map to GRPC.
135+
// See https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md for more details.
136+
//
137+
// If a grpc code cannot be derived from these three sources then INTERNAL is returned.
138+
//
139+
// Experimental: This API is at the early stage of development and may change
140+
// without backward compatibility
141+
func (e *Error) OTLPGRPCStatus() *status.Status {
142+
if e.grpcStatus != nil {
143+
return e.grpcStatus
144+
}
145+
if e.httpStatus != 0 {
146+
return statusconversion.NewStatusFromMsgAndHTTPCode(e.Error(), e.httpStatus)
147+
}
148+
if e.retryable {
149+
return status.New(codes.Unavailable, e.Error())
150+
}
151+
return status.New(codes.Internal, e.Error())
152+
}
153+
154+
// Retryable returns true if the error was created with the WithRetryable set to true,
155+
// if the http status code is retryable according to OTLP,
156+
// or if the grpc status is retryable according to OTLP.
157+
// Otherwise, returns false.
158+
//
159+
// See https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md for retryable
160+
// http and grpc codes.
161+
//
162+
// Experimental: This API is at the early stage of development and may change
163+
// without backward compatibility
164+
func (e *Error) Retryable() bool {
165+
if e.retryable {
166+
return true
167+
}
168+
switch e.httpStatus {
169+
case http.StatusTooManyRequests, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
170+
return true
171+
}
172+
if e.grpcStatus != nil {
173+
switch e.grpcStatus.Code() {
174+
case codes.Canceled, codes.DeadlineExceeded, codes.Aborted, codes.OutOfRange, codes.Unavailable, codes.DataLoss:
175+
return true
176+
}
177+
}
178+
return false
179+
}

consumer/consumererror/error_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package consumererror
5+
6+
import (
7+
"errors"
8+
"net/http"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"google.golang.org/grpc/codes"
14+
"google.golang.org/grpc/status"
15+
)
16+
17+
var errTest = errors.New("consumererror testing error")
18+
19+
func Test_New(t *testing.T) {
20+
httpStatus := 500
21+
grpcStatus := status.New(codes.Aborted, "aborted")
22+
wantErr := &Error{
23+
error: errTest,
24+
httpStatus: httpStatus,
25+
grpcStatus: grpcStatus,
26+
}
27+
28+
newErr := New(errTest,
29+
WithOTLPHTTPStatus(httpStatus),
30+
WithOTLPGRPCStatus(grpcStatus),
31+
)
32+
33+
require.Equal(t, wantErr, newErr)
34+
}
35+
36+
func Test_Error(t *testing.T) {
37+
newErr := New(errTest)
38+
39+
require.Equal(t, errTest.Error(), newErr.Error())
40+
}
41+
42+
func TestUnwrap(t *testing.T) {
43+
err := &Error{
44+
error: errTest,
45+
}
46+
47+
unwrapped := err.Unwrap()
48+
49+
require.Equal(t, errTest, unwrapped)
50+
}
51+
52+
func TestAs(t *testing.T) {
53+
err := &Error{
54+
error: errTest,
55+
}
56+
57+
secondError := errors.Join(errors.New("test"), err)
58+
59+
var e *Error
60+
require.True(t, errors.As(secondError, &e))
61+
assert.Equal(t, errTest.Error(), e.Error())
62+
}
63+
64+
func TestError_Error(t *testing.T) {
65+
err := &Error{
66+
error: errTest,
67+
}
68+
69+
require.Equal(t, errTest.Error(), err.Error())
70+
}
71+
72+
func TestError_Unwrap(t *testing.T) {
73+
err := &Error{
74+
error: errTest,
75+
}
76+
77+
require.Equal(t, errTest, err.Unwrap())
78+
}
79+
80+
func TestError_OTLPHTTPStatus(t *testing.T) {
81+
serverErr := http.StatusTooManyRequests
82+
testCases := []struct {
83+
name string
84+
httpStatus int
85+
grpcStatus *status.Status
86+
want int
87+
hasCode bool
88+
}{
89+
{
90+
name: "Passes through HTTP status",
91+
httpStatus: serverErr,
92+
want: serverErr,
93+
hasCode: true,
94+
},
95+
{
96+
name: "Converts gRPC status",
97+
grpcStatus: status.New(codes.ResourceExhausted, errTest.Error()),
98+
want: serverErr,
99+
hasCode: true,
100+
},
101+
{
102+
name: "Passes through HTTP status when gRPC status also present",
103+
httpStatus: serverErr,
104+
grpcStatus: status.New(codes.OK, errTest.Error()),
105+
want: serverErr,
106+
hasCode: true,
107+
},
108+
{
109+
name: "No statuses set",
110+
want: http.StatusInternalServerError,
111+
},
112+
}
113+
114+
for _, tt := range testCases {
115+
t.Run(tt.name, func(t *testing.T) {
116+
err := Error{
117+
error: errTest,
118+
httpStatus: tt.httpStatus,
119+
grpcStatus: tt.grpcStatus,
120+
}
121+
122+
s := err.OTLPHTTPStatus()
123+
124+
require.Equal(t, tt.want, s)
125+
})
126+
}
127+
}
128+
129+
func TestError_OTLPGRPCStatus(t *testing.T) {
130+
httpStatus := http.StatusTooManyRequests
131+
otherOTLPHTTPStatus := http.StatusOK
132+
serverErr := status.New(codes.ResourceExhausted, errTest.Error())
133+
testCases := []struct {
134+
name string
135+
httpStatus int
136+
grpcStatus *status.Status
137+
want *status.Status
138+
hasCode bool
139+
}{
140+
{
141+
name: "Converts HTTP status",
142+
httpStatus: httpStatus,
143+
want: serverErr,
144+
hasCode: true,
145+
},
146+
{
147+
name: "Passes through gRPC status",
148+
grpcStatus: serverErr,
149+
want: serverErr,
150+
hasCode: true,
151+
},
152+
{
153+
name: "Passes through gRPC status when gRPC status also present",
154+
httpStatus: otherOTLPHTTPStatus,
155+
grpcStatus: serverErr,
156+
want: serverErr,
157+
hasCode: true,
158+
},
159+
{
160+
name: "No statuses set",
161+
want: status.New(codes.Internal, errTest.Error()),
162+
},
163+
}
164+
165+
for _, tt := range testCases {
166+
t.Run(tt.name, func(t *testing.T) {
167+
err := Error{
168+
error: errTest,
169+
httpStatus: tt.httpStatus,
170+
grpcStatus: tt.grpcStatus,
171+
}
172+
173+
s := err.OTLPGRPCStatus()
174+
175+
require.Equal(t, tt.want, s)
176+
})
177+
}
178+
}

0 commit comments

Comments
 (0)