diff --git a/api/global/handler.go b/api/global/handler.go new file mode 100644 index 00000000000..b40eb732046 --- /dev/null +++ b/api/global/handler.go @@ -0,0 +1,92 @@ +// Copyright The OpenTelemetry Authors +// +// 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 global + +import ( + "log" + "os" + "sync" + "sync/atomic" + + "go.opentelemetry.io/otel/api/oterror" +) + +var ( + // globalHandler provides an oterror.Handler that can be used throughout + // an OpenTelemetry instrumented project. When a user specified Handler + // is registered (`SetHandler`) all calls to `Handle` will be delegated + // to the registered Handler. + globalHandler = &handler{ + l: log.New(os.Stderr, "", log.LstdFlags), + } + + // delegateHanderOnce ensures that a user provided Handler is only ever + // registered once. + delegateHanderOnce sync.Once + + // Ensure the handler implements oterror.Handle at build time. + _ oterror.Handler = (*handler)(nil) +) + +// handler logs all errors to STDERR. +type handler struct { + delegate atomic.Value + + l *log.Logger +} + +// setDelegate sets the handler delegate if one is not already set. +func (h *handler) setDelegate(d oterror.Handler) { + if h.delegate.Load() != nil { + // Delegate already registered + return + } + h.delegate.Store(d) +} + +// Handle implements oterror.Handler. +func (h *handler) Handle(err error) { + if d := h.delegate.Load(); d != nil { + d.(oterror.Handler).Handle(err) + return + } + h.l.Print(err) +} + +// Handler returns the global Handler instance. If no Handler instance has +// be explicitly set yet, a default Handler is returned that logs to STDERR +// until an Handler is set (all functionality is delegated to the set +// Handler once it is set). +func Handler() oterror.Handler { + return globalHandler +} + +// SetHandler sets the global Handler to be h. +func SetHandler(h oterror.Handler) { + delegateHanderOnce.Do(func() { + current := Handler() + if current == h { + return + } + if internalHandler, ok := current.(*handler); ok { + internalHandler.setDelegate(h) + } + }) +} + +// Handle is a convience function for Handler().Handle(err) +func Handle(err error) { + globalHandler.Handle(err) +} diff --git a/api/global/handler_test.go b/api/global/handler_test.go new file mode 100644 index 00000000000..0566ee468b4 --- /dev/null +++ b/api/global/handler_test.go @@ -0,0 +1,137 @@ +// Copyright The OpenTelemetry Authors +// +// 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 global + +import ( + "bytes" + "errors" + "log" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type errLogger []string + +func (l *errLogger) Write(p []byte) (int, error) { + msg := bytes.TrimRight(p, "\n") + (*l) = append(*l, string(msg)) + return len(msg), nil +} + +func (l *errLogger) Reset() { + *l = errLogger([]string{}) +} + +func (l *errLogger) Got() []string { + return []string(*l) +} + +type HandlerTestSuite struct { + suite.Suite + + origHandler *handler + errLogger *errLogger +} + +func (s *HandlerTestSuite) SetupSuite() { + s.errLogger = new(errLogger) + s.origHandler = globalHandler + globalHandler = &handler{ + l: log.New(s.errLogger, "", 0), + } +} + +func (s *HandlerTestSuite) TearDownSuite() { + globalHandler = s.origHandler +} + +func (s *HandlerTestSuite) SetupTest() { + s.errLogger.Reset() +} + +func (s *HandlerTestSuite) TestGlobalHandler() { + errs := []string{"one", "two"} + Handler().Handle(errors.New(errs[0])) + Handle(errors.New(errs[1])) + s.Assert().Equal(errs, s.errLogger.Got()) +} + +func (s *HandlerTestSuite) TestNoDropsOnDelegate() { + var sent int + err := errors.New("") + stop := make(chan struct{}) + beat := make(chan struct{}) + done := make(chan struct{}) + + go func() { + for { + select { + case <-stop: + done <- struct{}{} + return + default: + sent++ + Handle(err) + } + + select { + case beat <- struct{}{}: + default: + } + } + }() + + // Wait for the spice to flow + select { + case <-time.Tick(2 * time.Millisecond): + s.T().Fatal("no errors were sent in 2ms") + case <-beat: + } + + // Change to another Handler. We are testing this is loss-less. + newErrLogger := new(errLogger) + secondary := &handler{ + l: log.New(newErrLogger, "", 0), + } + SetHandler(secondary) + + select { + case <-time.Tick(2 * time.Millisecond): + s.T().Fatal("no errors were sent within 2ms after SetHandler") + case <-beat: + } + + // Now beat is clear, wait for a fresh send. + select { + case <-time.Tick(2 * time.Millisecond): + s.T().Fatal("no fresh errors were sent within 2ms after SetHandler") + case <-beat: + } + + // Stop sending errors. + stop <- struct{}{} + // Ensure we do not lose any straglers. + <-done + + got := append(s.errLogger.Got(), newErrLogger.Got()...) + s.Assert().Greater(len(got), 1, "at least 2 errors should have been sent") + s.Assert().Len(got, sent) +} + +func TestHandlerTestSuite(t *testing.T) { + suite.Run(t, new(HandlerTestSuite)) +} diff --git a/api/global/global.go b/api/global/metric.go similarity index 54% rename from api/global/global.go rename to api/global/metric.go index c58976e277e..4de85180fea 100644 --- a/api/global/global.go +++ b/api/global/metric.go @@ -17,34 +17,8 @@ package global import ( "go.opentelemetry.io/otel/api/global/internal" "go.opentelemetry.io/otel/api/metric" - "go.opentelemetry.io/otel/api/propagation" - "go.opentelemetry.io/otel/api/trace" ) -// Tracer creates a named tracer that implements Tracer interface. -// If the name is an empty string then provider uses default name. -// -// This is short for TraceProvider().Tracer(name) -func Tracer(name string) trace.Tracer { - return TraceProvider().Tracer(name) -} - -// TraceProvider returns the registered global trace provider. -// If none is registered then an instance of trace.NoopProvider is returned. -// -// Use the trace provider to create a named tracer. E.g. -// tracer := global.TraceProvider().Tracer("example.com/foo") -// or -// tracer := global.Tracer("example.com/foo") -func TraceProvider() trace.Provider { - return internal.TraceProvider() -} - -// SetTraceProvider registers `tp` as the global trace provider. -func SetTraceProvider(tp trace.Provider) { - internal.SetTraceProvider(tp) -} - // Meter gets a named Meter interface. If the name is an // empty string, the provider uses a default name. // @@ -69,15 +43,3 @@ func MeterProvider() metric.Provider { func SetMeterProvider(mp metric.Provider) { internal.SetMeterProvider(mp) } - -// Propagators returns the registered global propagators instance. If -// none is registered then an instance of propagators.NoopPropagators -// is returned. -func Propagators() propagation.Propagators { - return internal.Propagators() -} - -// SetPropagators registers `p` as the global propagators instance. -func SetPropagators(p propagation.Propagators) { - internal.SetPropagators(p) -} diff --git a/api/global/metric_test.go b/api/global/metric_test.go new file mode 100644 index 00000000000..e5b77befa6c --- /dev/null +++ b/api/global/metric_test.go @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// +// 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 global_test + +import ( + "testing" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/metric" +) + +type testMeterProvider struct{} + +var _ metric.Provider = &testMeterProvider{} + +func (*testMeterProvider) Meter(_ string) metric.Meter { + return metric.Meter{} +} + +func TestMultipleGlobalMeterProvider(t *testing.T) { + p1 := testMeterProvider{} + p2 := metric.NoopProvider{} + global.SetMeterProvider(&p1) + global.SetMeterProvider(&p2) + + got := global.MeterProvider() + want := &p2 + if got != want { + t.Fatalf("Provider: got %p, want %p\n", got, want) + } +} diff --git a/api/global/propagation.go b/api/global/propagation.go new file mode 100644 index 00000000000..e3090293dae --- /dev/null +++ b/api/global/propagation.go @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// +// 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 global + +import ( + "go.opentelemetry.io/otel/api/global/internal" + "go.opentelemetry.io/otel/api/propagation" +) + +// Propagators returns the registered global propagators instance. If +// none is registered then an instance of propagators.NoopPropagators +// is returned. +func Propagators() propagation.Propagators { + return internal.Propagators() +} + +// SetPropagators registers `p` as the global propagators instance. +func SetPropagators(p propagation.Propagators) { + internal.SetPropagators(p) +} diff --git a/api/global/trace.go b/api/global/trace.go new file mode 100644 index 00000000000..5c23325b751 --- /dev/null +++ b/api/global/trace.go @@ -0,0 +1,44 @@ +// Copyright The OpenTelemetry Authors +// +// 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 global + +import ( + "go.opentelemetry.io/otel/api/global/internal" + "go.opentelemetry.io/otel/api/trace" +) + +// Tracer creates a named tracer that implements Tracer interface. +// If the name is an empty string then provider uses default name. +// +// This is short for TraceProvider().Tracer(name) +func Tracer(name string) trace.Tracer { + return TraceProvider().Tracer(name) +} + +// TraceProvider returns the registered global trace provider. +// If none is registered then an instance of trace.NoopProvider is returned. +// +// Use the trace provider to create a named tracer. E.g. +// tracer := global.TraceProvider().Tracer("example.com/foo") +// or +// tracer := global.Tracer("example.com/foo") +func TraceProvider() trace.Provider { + return internal.TraceProvider() +} + +// SetTraceProvider registers `tp` as the global trace provider. +func SetTraceProvider(tp trace.Provider) { + internal.SetTraceProvider(tp) +} diff --git a/api/global/global_test.go b/api/global/trace_test.go similarity index 65% rename from api/global/global_test.go rename to api/global/trace_test.go index 974077aa636..a6961b35367 100644 --- a/api/global/global_test.go +++ b/api/global/trace_test.go @@ -18,28 +18,17 @@ import ( "testing" "go.opentelemetry.io/otel/api/global" - "go.opentelemetry.io/otel/api/metric" "go.opentelemetry.io/otel/api/trace" ) -type ( - testTraceProvider struct{} - testMeterProvider struct{} -) +type testTraceProvider struct{} -var ( - _ trace.Provider = &testTraceProvider{} - _ metric.Provider = &testMeterProvider{} -) +var _ trace.Provider = &testTraceProvider{} func (*testTraceProvider) Tracer(_ string) trace.Tracer { return &trace.NoopTracer{} } -func (*testMeterProvider) Meter(_ string) metric.Meter { - return metric.Meter{} -} - func TestMultipleGlobalTracerProvider(t *testing.T) { p1 := testTraceProvider{} p2 := trace.NoopProvider{} @@ -52,16 +41,3 @@ func TestMultipleGlobalTracerProvider(t *testing.T) { t.Fatalf("Provider: got %p, want %p\n", got, want) } } - -func TestMultipleGlobalMeterProvider(t *testing.T) { - p1 := testMeterProvider{} - p2 := metric.NoopProvider{} - global.SetMeterProvider(&p1) - global.SetMeterProvider(&p2) - - got := global.MeterProvider() - want := &p2 - if got != want { - t.Fatalf("Provider: got %p, want %p\n", got, want) - } -} diff --git a/api/metric/api_test.go b/api/metric/api_test.go index ebe21b4b92b..ca702788286 100644 --- a/api/metric/api_test.go +++ b/api/metric/api_test.go @@ -22,6 +22,7 @@ import ( "go.opentelemetry.io/otel/api/kv" "go.opentelemetry.io/otel/api/metric" + "go.opentelemetry.io/otel/api/oterror" "go.opentelemetry.io/otel/api/unit" mockTest "go.opentelemetry.io/otel/internal/metric" @@ -391,7 +392,7 @@ func TestWrappedInstrumentError(t *testing.T) { valuerecorder, err := meter.NewInt64ValueRecorder("test.valuerecorder") - require.Equal(t, err, metric.ErrSDKReturnedNilImpl) + require.Equal(t, err, oterror.ErrSDKReturnedNilImpl) require.NotNil(t, valuerecorder.SyncImpl()) observer, err := meter.NewInt64ValueObserver("test.observer", func(_ context.Context, result metric.Int64ObserverResult) {}) diff --git a/api/metric/sync.go b/api/metric/sync.go index 02913768323..46d8fa02c72 100644 --- a/api/metric/sync.go +++ b/api/metric/sync.go @@ -16,9 +16,9 @@ package metric import ( "context" - "errors" "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/oterror" ) // Measurement is used for reporting a synchronous batch of metric @@ -45,10 +45,6 @@ type asyncInstrument struct { instrument AsyncImpl } -// ErrSDKReturnedNilImpl is used when one of the `MeterImpl` New -// methods returns nil. -var ErrSDKReturnedNilImpl = errors.New("SDK returned a nil implementation") - // SyncImpl returns the instrument that created this measurement. // This returns an implementation-level object for use by the SDK, // users should not refer to this. @@ -114,7 +110,7 @@ func (h syncBoundInstrument) Unbind() { func checkNewAsync(instrument AsyncImpl, err error) (asyncInstrument, error) { if instrument == nil { if err == nil { - err = ErrSDKReturnedNilImpl + err = oterror.ErrSDKReturnedNilImpl } instrument = NoopAsync{} } @@ -129,7 +125,7 @@ func checkNewAsync(instrument AsyncImpl, err error) (asyncInstrument, error) { func checkNewSync(instrument SyncImpl, err error) (syncInstrument, error) { if instrument == nil { if err == nil { - err = ErrSDKReturnedNilImpl + err = oterror.ErrSDKReturnedNilImpl } // Note: an alternate behavior would be to synthesize a new name // or group all duplicately-named instruments of a certain type diff --git a/api/oterror/doc.go b/api/oterror/doc.go new file mode 100644 index 00000000000..9d4b5f7756f --- /dev/null +++ b/api/oterror/doc.go @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// +// 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. + +/* +* The oterror package provides unified error interactions in OpenTelemetry. +* This includes providing standardized errors common to OpenTelemetry (APIs, +* SDKs, and exporters). Additionally it provides an API for unified error +* handling in OpenTelemetry. +* +* The unified error handling interface is used for any error that +* OpenTelemetry component are not able to remediate on their own, instead +* handling them in a uniform and user-defined way. + */ + +package oterror diff --git a/api/oterror/errors.go b/api/oterror/errors.go new file mode 100644 index 00000000000..06889425352 --- /dev/null +++ b/api/oterror/errors.go @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// +// 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 oterror + +import "errors" + +var ( + // ErrSDKReturnedNilImpl is returned when a new `MeterImpl` returns nil. + ErrSDKReturnedNilImpl = errors.New("SDK returned a nil implementation") +) diff --git a/api/oterror/handler.go b/api/oterror/handler.go new file mode 100644 index 00000000000..7f3e54e6462 --- /dev/null +++ b/api/oterror/handler.go @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// +// 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 oterror + +// Handler handles irremediable events. +type Handler interface { + // Handle handles any error deemed irremediable by an OpenTelemetry + // component. + Handle(error) +}