From 33699d242d3b0823345488d8cfd5188781dfda5e Mon Sep 17 00:00:00 2001 From: Jack Wink <57678801+mothershipper@users.noreply.github.com> Date: Thu, 1 Apr 2021 13:07:46 -0700 Subject: [PATCH] Adds semantic conventions for exceptions (#1492) Adds support for the opentelemetry exceptions semantic conventions. In short, this has RecordError produce an exception event with exception attributes instead of using the error event and error attributes. While golang does not have exceptions, the spec itself does not differentiate between errors and exceptions for recording purposes. RecordError was kept as the method name, both for backwards compatibility and to reduce confusion (the method signature takes in a golang error object). The spec appears to allow this, as it suggests the method is optional and signature may reflect whatever is most appropriate for the language implementing it. It may seem non-intuitive to log an exception event from a method called RecordError, but it's beneficial to have consistent behavior across all opentelemetry SDKs. Downstream projects like the opentelemetry-collector can build off of the published API and not special case behaviors from individual languages. --- CHANGELOG.md | 3 +++ bridge/opentracing/internal/mock.go | 7 +++--- oteltest/span.go | 15 ++++------- oteltest/span_test.go | 7 +++--- sdk/trace/span.go | 19 ++++++-------- sdk/trace/trace_test.go | 13 +++++----- semconv/exception.go | 39 +++++++++++++++++++++++++++++ trace/trace.go | 8 +++--- 8 files changed, 73 insertions(+), 38 deletions(-) create mode 100644 semconv/exception.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7778bd8895d..6a125ff195f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added +- Adds semantic conventions for exceptions. (#1492) - Added support for configuring OTLP/HTTP Endpoints, Headers, Compression and Timeout via the Environment Variables. (#1758) - `OTEL_EXPORTER_OTLP_ENDPOINT` - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` @@ -23,6 +24,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `OTEL_EXPORTER_OTLP_TIMEOUT` - `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` - `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` + ### Fixed - The `Span.IsRecording` implementation from `go.opentelemetry.io/otel/sdk/trace` always returns false when not being sampled. (#1750) @@ -31,6 +33,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed +- Span `RecordError` now records an `exception` event to comply with the semantic convention specification. (#1492) - Jaeger exporter was updated to use thrift v0.14.1. (#1712) - Migrate from using internally built and maintained version of the OTLP to the one hosted at `go.opentelemetry.io/proto/otlp`. (#1713) - Migrate from using `github.com/gogo/protobuf` to `google.golang.org/protobuf` to match `go.opentelemetry.io/proto/otlp`. (#1713) diff --git a/bridge/opentracing/internal/mock.go b/bridge/opentracing/internal/mock.go index 658ec615f3a..e57d9129b85 100644 --- a/bridge/opentracing/internal/mock.go +++ b/bridge/opentracing/internal/mock.go @@ -24,6 +24,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/internal/baggage" + "go.opentelemetry.io/otel/semconv" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/bridge/opentracing/migration" @@ -255,10 +256,10 @@ func (s *MockSpan) RecordError(err error, opts ...trace.EventOption) { s.SetStatus(codes.Error, "") opts = append(opts, trace.WithAttributes( - attribute.String("error.type", reflect.TypeOf(err).String()), - attribute.String("error.message", err.Error()), + semconv.ExceptionTypeKey.String(reflect.TypeOf(err).String()), + semconv.ExceptionMessageKey.String(err.Error()), )) - s.AddEvent("error", opts...) + s.AddEvent(semconv.ExceptionEventName, opts...) } func (s *MockSpan) Tracer() trace.Tracer { diff --git a/oteltest/span.go b/oteltest/span.go index 2b85516a31c..0f2436c2b5c 100644 --- a/oteltest/span.go +++ b/oteltest/span.go @@ -22,15 +22,10 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/semconv" "go.opentelemetry.io/otel/trace" ) -const ( - errorTypeKey = attribute.Key("error.type") - errorMessageKey = attribute.Key("error.message") - errorEventName = "error" -) - var _ trace.Span = (*Span)(nil) // Span is an OpenTelemetry Span used for testing. @@ -79,7 +74,7 @@ func (s *Span) End(opts ...trace.SpanOption) { } } -// RecordError records an error as a Span event. +// RecordError records an error as an exception Span event. func (s *Span) RecordError(err error, opts ...trace.EventOption) { if err == nil || s.ended { return @@ -92,11 +87,11 @@ func (s *Span) RecordError(err error, opts ...trace.EventOption) { } opts = append(opts, trace.WithAttributes( - errorTypeKey.String(errTypeString), - errorMessageKey.String(err.Error()), + semconv.ExceptionTypeKey.String(errTypeString), + semconv.ExceptionMessageKey.String(err.Error()), )) - s.AddEvent(errorEventName, opts...) + s.AddEvent(semconv.ExceptionEventName, opts...) } // AddEvent adds an event to s. diff --git a/oteltest/span_test.go b/oteltest/span_test.go index eb00f0e2922..11372df09c7 100644 --- a/oteltest/span_test.go +++ b/oteltest/span_test.go @@ -27,6 +27,7 @@ import ( ottest "go.opentelemetry.io/otel/internal/internaltest" "go.opentelemetry.io/otel/internal/matchers" "go.opentelemetry.io/otel/oteltest" + "go.opentelemetry.io/otel/semconv" "go.opentelemetry.io/otel/trace" ) @@ -160,10 +161,10 @@ func TestSpan(t *testing.T) { expectedEvents := []oteltest.Event{{ Timestamp: testTime, - Name: "error", + Name: semconv.ExceptionEventName, Attributes: map[attribute.Key]attribute.Value{ - attribute.Key("error.type"): attribute.StringValue(s.typ), - attribute.Key("error.message"): attribute.StringValue(s.msg), + semconv.ExceptionTypeKey: attribute.StringValue(s.typ), + semconv.ExceptionMessageKey: attribute.StringValue(s.msg), }, }} e.Expect(subject.Events()).ToEqual(expectedEvents) diff --git a/sdk/trace/span.go b/sdk/trace/span.go index 4cc86489d42..82e21423664 100644 --- a/sdk/trace/span.go +++ b/sdk/trace/span.go @@ -24,6 +24,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/semconv" "go.opentelemetry.io/otel/trace" export "go.opentelemetry.io/otel/sdk/export/trace" @@ -32,12 +33,6 @@ import ( "go.opentelemetry.io/otel/sdk/resource" ) -const ( - errorTypeKey = attribute.Key("error.type") - errorMessageKey = attribute.Key("error.message") - errorEventName = "error" -) - // ReadOnlySpan allows reading information from the data structure underlying a // trace.Span. It is used in places where reading information from a span is // necessary but changing the span isn't necessary or allowed. @@ -222,10 +217,10 @@ func (s *span) End(options ...trace.SpanOption) { // Record but don't stop the panic. defer panic(recovered) s.addEvent( - errorEventName, + semconv.ExceptionEventName, trace.WithAttributes( - errorTypeKey.String(typeStr(recovered)), - errorMessageKey.String(fmt.Sprint(recovered)), + semconv.ExceptionTypeKey.String(typeStr(recovered)), + semconv.ExceptionMessageKey.String(fmt.Sprint(recovered)), ), ) } @@ -264,10 +259,10 @@ func (s *span) RecordError(err error, opts ...trace.EventOption) { } opts = append(opts, trace.WithAttributes( - errorTypeKey.String(typeStr(err)), - errorMessageKey.String(err.Error()), + semconv.ExceptionTypeKey.String(typeStr(err)), + semconv.ExceptionMessageKey.String(err.Error()), )) - s.addEvent(errorEventName, opts...) + s.addEvent(semconv.ExceptionEventName, opts...) } func typeStr(i interface{}) string { diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index 41f64c18e77..06928a0d8a9 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -30,6 +30,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/oteltest" + "go.opentelemetry.io/otel/semconv" "go.opentelemetry.io/otel/trace" "github.com/google/go-cmp/cmp" @@ -1119,11 +1120,11 @@ func TestRecordError(t *testing.T) { SpanKind: trace.SpanKindInternal, MessageEvents: []trace.Event{ { - Name: errorEventName, + Name: semconv.ExceptionEventName, Time: errTime, Attributes: []attribute.KeyValue{ - errorTypeKey.String(s.typ), - errorMessageKey.String(s.msg), + semconv.ExceptionTypeKey.String(s.typ), + semconv.ExceptionMessageKey.String(s.msg), }, }, }, @@ -1314,10 +1315,10 @@ func TestSpanCapturesPanic(t *testing.T) { spans := te.Spans() require.Len(t, spans, 1) require.Len(t, spans[0].MessageEvents, 1) - assert.Equal(t, spans[0].MessageEvents[0].Name, errorEventName) + assert.Equal(t, spans[0].MessageEvents[0].Name, semconv.ExceptionEventName) assert.Equal(t, spans[0].MessageEvents[0].Attributes, []attribute.KeyValue{ - errorTypeKey.String("*errors.errorString"), - errorMessageKey.String("error message"), + semconv.ExceptionTypeKey.String("*errors.errorString"), + semconv.ExceptionMessageKey.String("error message"), }) } diff --git a/semconv/exception.go b/semconv/exception.go new file mode 100644 index 00000000000..97002611228 --- /dev/null +++ b/semconv/exception.go @@ -0,0 +1,39 @@ +// 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 semconv + +import "go.opentelemetry.io/otel/attribute" + +// Semantic conventions for exception attribute keys. +const ( + // The Go type containing the error or exception. + ExceptionTypeKey = attribute.Key("exception.type") + + // The exception message. + ExceptionMessageKey = attribute.Key("exception.message") + + // A stacktrace as a string. This most commonly will come from + // "runtime/debug".Stack. + ExceptionStacktraceKey = attribute.Key("exception.stacktrace") + + // If the exception event is recorded at a point where it is known + // that the exception is escaping the scope of the span this + // attribute is set to true. + ExceptionEscapedKey = attribute.Key("exception.escaped") +) + +const ( + // ExceptionEventName is the name of the Span event representing an exception. + ExceptionEventName = "exception" +) diff --git a/trace/trace.go b/trace/trace.go index ae818ad1ce2..893c0c8fe75 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -503,10 +503,10 @@ type Span interface { // true if the Span is active and events can be recorded. IsRecording() bool - // RecordError will record err as a span event for this span. An additional call to - // SetStatus is required if the Status of the Span should be set to Error, this method - // does not change the Span status. If this span is not being recorded or err is nil - // than this method does nothing. + // RecordError will record err as an exception span event for this span. An + // additional call toSetStatus is required if the Status of the Span should + // be set to Error, this method does not change the Span status. If this + // span is not being recorded or err is nil than this method does nothing. RecordError(err error, options ...EventOption) // SpanContext returns the SpanContext of the Span. The returned