Skip to content

Commit ec13377

Browse files
jmacdhanyuancheungMrAlias
authored
OTLP traces export errors use a consistent error message prefix (open-telemetry#3516)
* OTLP traces export errors use a consistent error message prefix * use a wrapped error * update changelog * merge changelog * Update CHANGELOG.md --------- Co-authored-by: Chester Cheung <[email protected]> Co-authored-by: Tyler Yahn <[email protected]>
1 parent af3db6e commit ec13377

File tree

8 files changed

+187
-15
lines changed

8 files changed

+187
-15
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
8989
- `traceIDRatioSampler` (given by `TraceIDRatioBased(float64)`) now uses the rightmost bits for sampling decisions,
9090
fixing random sampling when using ID generators like `xray.IDGenerator`
9191
and increasing parity with other language implementations. (#3557)
92+
- OTLP trace exporter errors are wrapped in a type that prefixes the signal name in front of error strings (e.g., "traces export: context canceled" instead of just "context canceled").
93+
Existing users of the exporters attempting to identify specific errors will need to use `errors.Unwrap()` to get the underlying error. (#3516)
9294
- The OTLP exporter for traces and metrics will print the final retryable error message when attempts to retry time out. (#3514)
9395
- The instrument kind names in `go.opentelemetry.io/otel/sdk/metric` are updated to match the API. (#3562)
9496
- `InstrumentKindSyncCounter` is renamed to `InstrumentKindCounter`
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package internal // import "go.opentelemetry.io/otel/exporters/otlp/internal"
16+
17+
// ErrorKind is used to identify the kind of export error
18+
// being wrapped.
19+
type ErrorKind int
20+
21+
const (
22+
// TracesExport indicates the error comes from the OTLP trace exporter.
23+
TracesExport ErrorKind = iota
24+
)
25+
26+
// prefix returns a prefix for the Error() string.
27+
func (k ErrorKind) prefix() string {
28+
switch k {
29+
case TracesExport:
30+
return "traces export: "
31+
default:
32+
return "unknown: "
33+
}
34+
}
35+
36+
// wrappedExportError wraps an OTLP exporter error with the kind of
37+
// signal that produced it.
38+
type wrappedExportError struct {
39+
wrap error
40+
kind ErrorKind
41+
}
42+
43+
// WrapTracesError wraps an error from the OTLP exporter for traces.
44+
func WrapTracesError(err error) error {
45+
return wrappedExportError{
46+
wrap: err,
47+
kind: TracesExport,
48+
}
49+
}
50+
51+
var _ error = wrappedExportError{}
52+
53+
// Error attaches a prefix corresponding to the kind of exporter.
54+
func (t wrappedExportError) Error() string {
55+
return t.kind.prefix() + t.wrap.Error()
56+
}
57+
58+
// Unwrap returns the wrapped error.
59+
func (t wrappedExportError) Unwrap() error {
60+
return t.wrap
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package internal // import "go.opentelemetry.io/otel/exporters/otlp/internal"
16+
17+
import (
18+
"context"
19+
"errors"
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestWrappedError(t *testing.T) {
26+
e := WrapTracesError(context.Canceled)
27+
28+
require.Equal(t, context.Canceled, errors.Unwrap(e))
29+
require.Equal(t, TracesExport, e.(wrappedExportError).kind)
30+
require.Equal(t, "traces export: context canceled", e.Error())
31+
require.True(t, errors.Is(e, context.Canceled))
32+
}

exporters/otlp/otlptrace/exporter.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"sync"
2121

22+
"go.opentelemetry.io/otel/exporters/otlp/internal"
2223
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform"
2324
tracesdk "go.opentelemetry.io/otel/sdk/trace"
2425
)
@@ -45,7 +46,11 @@ func (e *Exporter) ExportSpans(ctx context.Context, ss []tracesdk.ReadOnlySpan)
4546
return nil
4647
}
4748

48-
return e.client.UploadTraces(ctx, protoSpans)
49+
err := e.client.UploadTraces(ctx, protoSpans)
50+
if err != nil {
51+
return internal.WrapTracesError(err)
52+
}
53+
return nil
4954
}
5055

5156
// Start establishes a connection to the receiving endpoint.
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package otlptrace_test
16+
17+
import (
18+
"context"
19+
"errors"
20+
"strings"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
25+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
26+
"go.opentelemetry.io/otel/sdk/trace/tracetest"
27+
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
28+
)
29+
30+
type client struct {
31+
uploadErr error
32+
}
33+
34+
var _ otlptrace.Client = &client{}
35+
36+
func (c *client) Start(ctx context.Context) error {
37+
return nil
38+
}
39+
40+
func (c *client) Stop(ctx context.Context) error {
41+
return nil
42+
}
43+
44+
func (c *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.ResourceSpans) error {
45+
return c.uploadErr
46+
}
47+
48+
func TestExporterClientError(t *testing.T) {
49+
ctx := context.Background()
50+
exp, err := otlptrace.New(ctx, &client{
51+
uploadErr: context.Canceled,
52+
})
53+
assert.NoError(t, err)
54+
55+
spans := tracetest.SpanStubs{{Name: "Span 0"}}.Snapshots()
56+
err = exp.ExportSpans(ctx, spans)
57+
58+
assert.Error(t, err)
59+
assert.True(t, errors.Is(err, context.Canceled))
60+
assert.True(t, strings.HasPrefix(err.Error(), "traces export: "), err)
61+
62+
assert.NoError(t, exp.Shutdown(ctx))
63+
}

exporters/otlp/otlptrace/otlptracegrpc/client_test.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package otlptracegrpc_test
1616

1717
import (
1818
"context"
19+
"errors"
1920
"fmt"
2021
"net"
2122
"strings"
@@ -239,7 +240,9 @@ func TestExportSpansTimeoutHonored(t *testing.T) {
239240
// Release the export so everything is cleaned up on shutdown.
240241
close(exportBlock)
241242

242-
require.Equal(t, codes.DeadlineExceeded, status.Convert(err).Code())
243+
unwrapped := errors.Unwrap(err)
244+
require.Equal(t, codes.DeadlineExceeded, status.Convert(unwrapped).Code())
245+
require.True(t, strings.HasPrefix(err.Error(), "traces export: "), err)
243246
}
244247

245248
func TestNewWithMultipleAttributeTypes(t *testing.T) {
@@ -399,18 +402,18 @@ func TestPartialSuccess(t *testing.T) {
399402
})
400403
t.Cleanup(func() { require.NoError(t, mc.stop()) })
401404

402-
errors := []error{}
405+
errs := []error{}
403406
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
404-
errors = append(errors, err)
407+
errs = append(errs, err)
405408
}))
406409
ctx := context.Background()
407410
exp := newGRPCExporter(t, ctx, mc.endpoint)
408411
t.Cleanup(func() { require.NoError(t, exp.Shutdown(ctx)) })
409412
require.NoError(t, exp.ExportSpans(ctx, roSpans))
410413

411-
require.Equal(t, 1, len(errors))
412-
require.Contains(t, errors[0].Error(), "partially successful")
413-
require.Contains(t, errors[0].Error(), "2 spans rejected")
414+
require.Equal(t, 1, len(errs))
415+
require.Contains(t, errs[0].Error(), "partially successful")
416+
require.Contains(t, errs[0].Error(), "2 spans rejected")
414417
}
415418

416419
func TestCustomUserAgent(t *testing.T) {

exporters/otlp/otlptrace/otlptracehttp/client.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
197197
}
198198
return newResponseError(resp.Header)
199199
default:
200-
return fmt.Errorf("failed to send %s to %s: %s", d.name, request.URL, resp.Status)
200+
return fmt.Errorf("failed to send to %s: %s", request.URL, resp.Status)
201201
}
202202
})
203203
}

exporters/otlp/otlptrace/otlptracehttp/client_test.go

+13-7
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ package otlptracehttp_test
1616

1717
import (
1818
"context"
19+
"errors"
1920
"fmt"
2021
"net/http"
2122
"os"
23+
"strings"
2224
"testing"
2325
"time"
2426

@@ -219,7 +221,9 @@ func TestTimeout(t *testing.T) {
219221
assert.NoError(t, exporter.Shutdown(ctx))
220222
}()
221223
err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan())
222-
assert.Equalf(t, true, os.IsTimeout(err), "expected timeout error, got: %v", err)
224+
unwrapped := errors.Unwrap(err)
225+
assert.Equalf(t, true, os.IsTimeout(unwrapped), "expected timeout error, got: %v", unwrapped)
226+
assert.True(t, strings.HasPrefix(err.Error(), "traces export: "), err)
223227
}
224228

225229
func TestNoRetry(t *testing.T) {
@@ -246,7 +250,9 @@ func TestNoRetry(t *testing.T) {
246250
}()
247251
err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan())
248252
assert.Error(t, err)
249-
assert.Equal(t, fmt.Sprintf("failed to send traces to http://%s/v1/traces: 400 Bad Request", mc.endpoint), err.Error())
253+
unwrapped := errors.Unwrap(err)
254+
assert.Equal(t, fmt.Sprintf("failed to send to http://%s/v1/traces: 400 Bad Request", mc.endpoint), unwrapped.Error())
255+
assert.True(t, strings.HasPrefix(err.Error(), "traces export: "))
250256
assert.Empty(t, mc.GetSpans())
251257
}
252258

@@ -384,14 +390,14 @@ func TestPartialSuccess(t *testing.T) {
384390
assert.NoError(t, exporter.Shutdown(context.Background()))
385391
}()
386392

387-
errors := []error{}
393+
errs := []error{}
388394
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
389-
errors = append(errors, err)
395+
errs = append(errs, err)
390396
}))
391397
err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan())
392398
assert.NoError(t, err)
393399

394-
require.Equal(t, 1, len(errors))
395-
require.Contains(t, errors[0].Error(), "partially successful")
396-
require.Contains(t, errors[0].Error(), "2 spans rejected")
400+
require.Equal(t, 1, len(errs))
401+
require.Contains(t, errs[0].Error(), "partially successful")
402+
require.Contains(t, errs[0].Error(), "2 spans rejected")
397403
}

0 commit comments

Comments
 (0)