diff --git a/cmd/agent/app/reporter/reporter.go b/cmd/agent/app/reporter/reporter.go index 550a4a2dd9ba..5b300ad50da7 100644 --- a/cmd/agent/app/reporter/reporter.go +++ b/cmd/agent/app/reporter/reporter.go @@ -17,8 +17,8 @@ package reporter import ( "context" - "errors" + "github.com/jaegertracing/jaeger/pkg/multierror" "github.com/jaegertracing/jaeger/thrift-gen/jaeger" "github.com/jaegertracing/jaeger/thrift-gen/zipkincore" ) @@ -43,22 +43,22 @@ func NewMultiReporter(reps ...Reporter) MultiReporter { // EmitZipkinBatch calls each EmitZipkinBatch, returning the first error. func (mr MultiReporter) EmitZipkinBatch(ctx context.Context, spans []*zipkincore.Span) error { - var errs []error + var errors []error for _, rep := range mr { if err := rep.EmitZipkinBatch(ctx, spans); err != nil { - errs = append(errs, err) + errors = append(errors, err) } } - return errors.Join(errs...) + return multierror.Wrap(errors) } // EmitBatch calls each EmitBatch, returning the first error. func (mr MultiReporter) EmitBatch(ctx context.Context, batch *jaeger.Batch) error { - var errs []error + var errors []error for _, rep := range mr { if err := rep.EmitBatch(ctx, batch); err != nil { - errs = append(errs, err) + errors = append(errors, err) } } - return errors.Join(errs...) + return multierror.Wrap(errors) } diff --git a/cmd/agent/app/reporter/reporter_test.go b/cmd/agent/app/reporter/reporter_test.go index 7f5b86433f54..5f46c7166315 100644 --- a/cmd/agent/app/reporter/reporter_test.go +++ b/cmd/agent/app/reporter/reporter_test.go @@ -60,8 +60,8 @@ func TestMultiReporterErrors(t *testing.T) { {}, }, }) - assert.EqualError(t, e1, fmt.Sprintf("%s\n%s", errMsg, errMsg)) - assert.EqualError(t, e2, fmt.Sprintf("%s\n%s", errMsg, errMsg)) + assert.EqualError(t, e1, fmt.Sprintf("[%s, %s]", errMsg, errMsg)) + assert.EqualError(t, e2, fmt.Sprintf("[%s, %s]", errMsg, errMsg)) } type mockReporter struct { diff --git a/cmd/query/app/handler_archive_test.go b/cmd/query/app/handler_archive_test.go index 72c51034983c..a244f7370506 100644 --- a/cmd/query/app/handler_archive_test.go +++ b/cmd/query/app/handler_archive_test.go @@ -123,6 +123,6 @@ func TestArchiveTrace_WriteErrors(t *testing.T) { Return(mockTrace, nil).Once() var response structuredResponse err := postJSON(ts.server.URL+"/api/archive/"+mockTraceID.String(), []string{}, &response) - assert.EqualError(t, err, `500 error from server: {"data":null,"total":0,"limit":0,"offset":0,"errors":[{"code":500,"msg":"cannot save\ncannot save"}]}`+"\n") + assert.EqualError(t, err, `500 error from server: {"data":null,"total":0,"limit":0,"offset":0,"errors":[{"code":500,"msg":"[cannot save, cannot save]"}]}`+"\n") }, querysvc.QueryServiceOptions{ArchiveSpanWriter: mockWriter}) } diff --git a/cmd/query/app/http_handler.go b/cmd/query/app/http_handler.go index 3e09895d37bb..49b7805a628c 100644 --- a/cmd/query/app/http_handler.go +++ b/cmd/query/app/http_handler.go @@ -35,6 +35,7 @@ import ( "github.com/jaegertracing/jaeger/model" uiconv "github.com/jaegertracing/jaeger/model/converter/json" ui "github.com/jaegertracing/jaeger/model/json" + "github.com/jaegertracing/jaeger/pkg/multierror" "github.com/jaegertracing/jaeger/pkg/tenancy" "github.com/jaegertracing/jaeger/plugin/metrics/disabled" "github.com/jaegertracing/jaeger/proto-gen/api_v2/metrics" @@ -354,17 +355,17 @@ func (aH *APIHandler) metrics(w http.ResponseWriter, r *http.Request, getMetrics } func (aH *APIHandler) convertModelToUI(trace *model.Trace, adjust bool) (*ui.Trace, *structuredError) { - var errs []error + var errors []error if adjust { var err error trace, err = aH.queryService.Adjust(trace) if err != nil { - errs = append(errs, err) + errors = append(errors, err) } } uiTrace := uiconv.FromDomain(trace) var uiError *structuredError - if err := errors.Join(errs...); err != nil { + if err := multierror.Wrap(errors); err != nil { uiError = &structuredError{ Msg: err.Error(), TraceID: uiTrace.TraceID, diff --git a/cmd/query/app/querysvc/query_service.go b/cmd/query/app/querysvc/query_service.go index 7dde2813cae3..a88575d79d43 100644 --- a/cmd/query/app/querysvc/query_service.go +++ b/cmd/query/app/querysvc/query_service.go @@ -23,6 +23,7 @@ import ( "github.com/jaegertracing/jaeger/model" "github.com/jaegertracing/jaeger/model/adjuster" + "github.com/jaegertracing/jaeger/pkg/multierror" "github.com/jaegertracing/jaeger/storage" "github.com/jaegertracing/jaeger/storage/dependencystore" "github.com/jaegertracing/jaeger/storage/spanstore" @@ -109,7 +110,7 @@ func (qs QueryService) ArchiveTrace(ctx context.Context, traceID model.TraceID) writeErrors = append(writeErrors, err) } } - return errors.Join(writeErrors...) + return multierror.Wrap(writeErrors) } // Adjust applies adjusters to the trace. diff --git a/cmd/query/app/querysvc/query_service_test.go b/cmd/query/app/querysvc/query_service_test.go index ee214c33ea28..79e66253be2a 100644 --- a/cmd/query/app/querysvc/query_service_test.go +++ b/cmd/query/app/querysvc/query_service_test.go @@ -239,9 +239,10 @@ func TestArchiveTraceWithArchiveWriterError(t *testing.T) { type contextKey string ctx := context.Background() - joinErr := tqs.queryService.ArchiveTrace(context.WithValue(ctx, contextKey("foo"), "bar"), mockTraceID) + multiErr := tqs.queryService.ArchiveTrace(context.WithValue(ctx, contextKey("foo"), "bar"), mockTraceID) + assert.Len(t, multiErr, 2) // There are two spans in the mockTrace, ArchiveTrace should return a wrapped error. - assert.EqualError(t, joinErr, "cannot save\ncannot save") + assert.EqualError(t, multiErr, "[cannot save, cannot save]") } // Test QueryService.ArchiveTrace() with correctly configured ArchiveSpanWriter. diff --git a/model/adjuster/adjuster.go b/model/adjuster/adjuster.go index 5cce6c4a348d..2ac9e619caff 100644 --- a/model/adjuster/adjuster.go +++ b/model/adjuster/adjuster.go @@ -16,9 +16,8 @@ package adjuster import ( - "errors" - "github.com/jaegertracing/jaeger/model" + "github.com/jaegertracing/jaeger/pkg/multierror" ) // Adjuster applies certain modifications to a Trace object. @@ -57,7 +56,7 @@ type sequence struct { } func (c sequence) Adjust(trace *model.Trace) (*model.Trace, error) { - var errs []error + var errors []error for _, adjuster := range c.adjusters { var err error trace, err = adjuster.Adjust(trace) @@ -65,8 +64,8 @@ func (c sequence) Adjust(trace *model.Trace) (*model.Trace, error) { if c.failFast { return trace, err } - errs = append(errs, err) + errors = append(errors, err) } } - return trace, errors.Join(errs...) + return trace, multierror.Wrap(errors) } diff --git a/model/adjuster/adjuster_test.go b/model/adjuster/adjuster_test.go index 2c0cefbfbd67..569729a0a931 100644 --- a/model/adjuster/adjuster_test.go +++ b/model/adjuster/adjuster_test.go @@ -45,7 +45,7 @@ func TestSequences(t *testing.T) { }{ { adjuster: adjuster.Sequence(adj, failingAdj, adj, failingAdj), - err: fmt.Sprintf("%s\n%s", adjErr, adjErr), + err: fmt.Sprintf("[%s, %s]", adjErr, adjErr), lastSpanID: 2, }, { diff --git a/model/converter/thrift/zipkin/to_domain.go b/model/converter/thrift/zipkin/to_domain.go index 0ee67e5a6ad1..73e68f845761 100644 --- a/model/converter/thrift/zipkin/to_domain.go +++ b/model/converter/thrift/zipkin/to_domain.go @@ -20,12 +20,12 @@ import ( "encoding/base64" "encoding/binary" "encoding/json" - "errors" "fmt" "github.com/opentracing/opentracing-go/ext" "github.com/jaegertracing/jaeger/model" + "github.com/jaegertracing/jaeger/pkg/multierror" "github.com/jaegertracing/jaeger/thrift-gen/zipkincore" ) @@ -84,13 +84,13 @@ func ToDomainSpan(zSpan *zipkincore.Span) ([]*model.Span, error) { type toDomain struct{} func (td toDomain) ToDomain(zSpans []*zipkincore.Span) (*model.Trace, error) { - var errs []error + var errors []error processes := newProcessHashtable() trace := &model.Trace{} for _, zSpan := range zSpans { jSpans, err := td.ToDomainSpans(zSpan) if err != nil { - errs = append(errs, err) + errors = append(errors, err) } for _, jSpan := range jSpans { // remove duplicate Process instances @@ -98,7 +98,7 @@ func (td toDomain) ToDomain(zSpans []*zipkincore.Span) (*model.Trace, error) { trace.Spans = append(trace.Spans, jSpan) } } - return trace, errors.Join(errs...) + return trace, multierror.Wrap(errors) } func (td toDomain) ToDomainSpans(zSpan *zipkincore.Span) ([]*model.Span, error) { diff --git a/pkg/multierror/multierror.go b/pkg/multierror/multierror.go new file mode 100644 index 000000000000..bb1994490a1b --- /dev/null +++ b/pkg/multierror/multierror.go @@ -0,0 +1,56 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multierror + +import ( + "fmt" + "strings" +) + +// Wrap takes a slice of errors and returns a single error that encapsulates +// those underlying errors. If the slice is nil or empty it returns nil. +// If the slice only contains a single element, that error is returned directly. +// When more than one error is wrapped, the Error() string is a concatenation +// of the Error() values of all underlying errors. +func Wrap(errs []error) error { + return multiError(errs).flatten() +} + +// multiError bundles several errors together into a single error. +type multiError []error + +// flatten returns either: nil, the only error, or the multiError instance itself +// if there are 0, 1, or more errors in the slice respectively. +func (errors multiError) flatten() error { + switch len(errors) { + case 0: + return nil + case 1: + return errors[0] + default: + return errors + } +} + +// Error returns a string like "[e1, e2, ...]" where each eN is the Error() of +// each error in the slice. +func (errors multiError) Error() string { + parts := make([]string, len(errors)) + for i, err := range errors { + parts[i] = err.Error() + } + return fmt.Sprintf("[%s]", strings.Join(parts, ", ")) +} diff --git a/pkg/multierror/multierror_test.go b/pkg/multierror/multierror_test.go new file mode 100644 index 000000000000..edd320515b86 --- /dev/null +++ b/pkg/multierror/multierror_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2019 The Jaeger Authors. +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multierror + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func ExampleWrap() { + someFunc := func() error { + return errors.New("doh") + } + + var errs []error + for i := 0; i < 2; i++ { + if err := someFunc(); err != nil { + errs = append(errs, err) + } + fmt.Println(Wrap(errs).Error()) + } + // Output: doh + // [doh, doh] +} + +func TestWrapEmptySlice(t *testing.T) { + var errors []error + e1 := Wrap(errors) + assert.Nil(t, e1) + e2 := Wrap([]error{}) + assert.Nil(t, e2) +} + +func TestWrapSingleError(t *testing.T) { + err := errors.New("doh") + e1 := Wrap([]error{err}) + assert.Error(t, e1) + assert.Equal(t, err, e1) + assert.Equal(t, "doh", e1.Error()) +} + +func TestWrapManyErrors(t *testing.T) { + err1 := errors.New("ay") + err2 := errors.New("caramba") + e1 := Wrap([]error{err1, err2}) + assert.Error(t, e1) + assert.Equal(t, "[ay, caramba]", e1.Error()) +} diff --git a/plugin/storage/factory.go b/plugin/storage/factory.go index b2c6b7a92264..84d2aa4e01cb 100644 --- a/plugin/storage/factory.go +++ b/plugin/storage/factory.go @@ -16,7 +16,6 @@ package storage import ( - "errors" "flag" "fmt" "io" @@ -25,6 +24,7 @@ import ( "go.uber.org/zap" "github.com/jaegertracing/jaeger/pkg/metrics" + "github.com/jaegertracing/jaeger/pkg/multierror" "github.com/jaegertracing/jaeger/plugin" "github.com/jaegertracing/jaeger/plugin/storage/badger" "github.com/jaegertracing/jaeger/plugin/storage/cassandra" @@ -327,7 +327,7 @@ func (f *Factory) Close() error { } } } - return errors.Join(errs...) + return multierror.Wrap(errs) } func (f *Factory) publishOpts() { diff --git a/storage/spanstore/composite.go b/storage/spanstore/composite.go index 68f9607448c0..5746d09b9251 100644 --- a/storage/spanstore/composite.go +++ b/storage/spanstore/composite.go @@ -17,9 +17,9 @@ package spanstore import ( "context" - "errors" "github.com/jaegertracing/jaeger/model" + "github.com/jaegertracing/jaeger/pkg/multierror" ) // CompositeWriter is a span Writer that tries to save spans into several underlying span Writers @@ -36,11 +36,11 @@ func NewCompositeWriter(spanWriters ...Writer) *CompositeWriter { // WriteSpan calls WriteSpan on each span writer. It will sum up failures, it is not transactional func (c *CompositeWriter) WriteSpan(ctx context.Context, span *model.Span) error { - var errs []error + var errors []error for _, writer := range c.spanWriters { if err := writer.WriteSpan(ctx, span); err != nil { - errs = append(errs, err) + errors = append(errors, err) } } - return errors.Join(errs...) + return multierror.Wrap(errors) } diff --git a/storage/spanstore/composite_test.go b/storage/spanstore/composite_test.go index e8cb24d6a089..ec7aa8cd661f 100644 --- a/storage/spanstore/composite_test.go +++ b/storage/spanstore/composite_test.go @@ -48,10 +48,10 @@ func TestCompositeWriteSpanStoreSuccess(t *testing.T) { func TestCompositeWriteSpanStoreSecondFailure(t *testing.T) { c := NewCompositeWriter(&errProneWriteSpanStore{}, &errProneWriteSpanStore{}) - assert.EqualError(t, c.WriteSpan(context.Background(), nil), fmt.Sprintf("%s\n%s", errIWillAlwaysFail, errIWillAlwaysFail)) + assert.EqualError(t, c.WriteSpan(context.Background(), nil), fmt.Sprintf("[%s, %s]", errIWillAlwaysFail, errIWillAlwaysFail)) } func TestCompositeWriteSpanStoreFirstFailure(t *testing.T) { c := NewCompositeWriter(&errProneWriteSpanStore{}, &noopWriteSpanStore{}) - assert.EqualError(t, c.WriteSpan(context.Background(), nil), errIWillAlwaysFail.Error()) + assert.Equal(t, errIWillAlwaysFail, c.WriteSpan(context.Background(), nil)) }