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