Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add support for per-series start time tracking for cumulative metrics in `go.opentelemetry.io/otel/sdk/metric`.
Set `OTEL_GO_X_PER_SERIES_START_TIMESTAMPS=true` to enable. (#8060)
- Add `WithCardinalityLimitSelector` for metric reader for configuring cardinality limits specific to the instrument kind. (#7855)
- Add `WithStackTrace` option for TracerProvider to add stackTraces to all spans. (#8094)

### Changed

Expand Down
16 changes: 16 additions & 0 deletions sdk/trace/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ type tracerProviderConfig struct {

// resource contains attributes representing an entity that produces telemetry.
resource *resource.Resource

// stackTrace configs whether to capture stack trace for all recorded errors and panics.
stackTrace bool
}

// MarshalLog is the marshaling function used by the logging system to represent this Provider.
Expand All @@ -52,12 +55,14 @@ func (cfg tracerProviderConfig) MarshalLog() any {
IDGeneratorType string
SpanLimits SpanLimits
Resource *resource.Resource
StackTrace bool
}{
SpanProcessors: cfg.processors,
SamplerType: fmt.Sprintf("%T", cfg.sampler),
IDGeneratorType: fmt.Sprintf("%T", cfg.idGenerator),
SpanLimits: cfg.spanLimits,
Resource: cfg.resource,
StackTrace: cfg.stackTrace,
}
}

Expand All @@ -78,6 +83,7 @@ type TracerProvider struct {
idGenerator IDGenerator
spanLimits SpanLimits
resource *resource.Resource
stackTrace bool
}

var _ trace.TracerProvider = &TracerProvider{}
Expand Down Expand Up @@ -110,6 +116,7 @@ func NewTracerProvider(opts ...TracerProviderOption) *TracerProvider {
idGenerator: o.idGenerator,
spanLimits: o.spanLimits,
resource: o.resource,
stackTrace: o.stackTrace,
}
global.Info("TracerProvider created", "config", o)

Expand Down Expand Up @@ -384,6 +391,15 @@ func WithIDGenerator(g IDGenerator) TracerProviderOption {
})
}

// WithStackTrace configures the TracerProvider to capture a stack trace

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like WithAlwaysStackTrace() or something could make it clearer what this does.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Will do.

// for all recorded errors and panics.
func WithStackTrace(b bool) TracerProviderOption {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud: Do we want to be able to support anything else related to this? NeverStackTrace?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the benefit of NeverStackTrace.

This would not allow the user to set the stackTrace on some important code path 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some possible use-cases:

  • Your backend doesn't support stack traces
  • An instrumentation library you don't control is collecting too many stack traces and you want to disable it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added NeverStackTrace

return traceProviderOptionFunc(func(cfg tracerProviderConfig) tracerProviderConfig {
cfg.stackTrace = b
return cfg
})
}

// WithSampler returns a TracerProviderOption that will configure the Sampler
// s as a TracerProvider's Sampler. The configured Sampler is used by the
// Tracers the TracerProvider creates to make their sampling decisions for the
Expand Down
4 changes: 2 additions & 2 deletions sdk/trace/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ func (s *recordingSpan) End(options ...trace.SpanEndOption) {
),
}

if config.StackTrace() {
if config.StackTrace() || s.tracer.provider.stackTrace {
opts = append(opts, trace.WithAttributes(
semconv.ExceptionStacktrace(recordStackTrace()),
))
Expand Down Expand Up @@ -558,7 +558,7 @@ func (s *recordingSpan) RecordError(err error, opts ...trace.EventOption) {
))

c := trace.NewEventConfig(opts...)
if c.StackTrace() {
if c.StackTrace() || s.tracer.provider.stackTrace {
opts = append(opts, trace.WithAttributes(
semconv.ExceptionStacktrace(recordStackTrace()),
))
Expand Down
100 changes: 100 additions & 0 deletions sdk/trace/trace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,71 @@ func TestRecordErrorWithStackTrace(t *testing.T) {
)
}

func TestProviderRecordErrorWithStackTrace(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this and the other test are mostly the same as the tests above them. If it makes sense, make these table-driven instead of separate tests.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried. Let's see.

err := newTestError("test error")
typ := "go.opentelemetry.io/otel/sdk/trace.testError"
msg := "test error"

te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()), WithStackTrace(true))
span := startSpan(tp, "RecordError")

errTime := time.Now()
span.RecordError(err, trace.WithTimestamp(errTime))

got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}

want := &snapshot{
spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
parent: sc.WithRemote(true),
name: "span0",
status: Status{Code: codes.Unset},
spanKind: trace.SpanKindInternal,
events: []Event{
{
Name: semconv.ExceptionEventName,
Time: errTime,
Attributes: []attribute.KeyValue{
semconv.ExceptionType(typ),
semconv.ExceptionMessage(msg),
},
},
},
instrumentationScope: instrumentation.Scope{Name: "RecordError"},
}

assert.Equal(t, want.spanContext, got.spanContext)
assert.Equal(t, want.parent, got.parent)
assert.Equal(t, want.name, got.name)
assert.Equal(t, want.status, got.status)
assert.Equal(t, want.spanKind, got.spanKind)
assert.Equal(t, got.events[0].Attributes[0].Value.AsString(), want.events[0].Attributes[0].Value.AsString())
assert.Equal(t, got.events[0].Attributes[1].Value.AsString(), want.events[0].Attributes[1].Value.AsString())
gotStackTraceFunctionName := strings.Split(got.events[0].Attributes[2].Value.AsString(), "\n")

assert.Truef(
t,
strings.HasPrefix(gotStackTraceFunctionName[1], "go.opentelemetry.io/otel/sdk/trace.recordStackTrace"),
"%q not prefixed with go.opentelemetry.io/otel/sdk/trace.recordStackTrace",
gotStackTraceFunctionName[1],
)
assert.Truef(
t,
strings.HasPrefix(
gotStackTraceFunctionName[3],
"go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).RecordError",
),
"%q not prefixed with go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).RecordError",
gotStackTraceFunctionName[3],
)
}

func TestRecordErrorNil(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
Expand Down Expand Up @@ -1603,6 +1668,41 @@ func TestSpanCapturesPanicWithStackTrace(t *testing.T) {
)
}

func TestProviderSpanCapturesPanicWithStackTrace(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()), WithStackTrace(true))
_, span := tp.Tracer("CatchPanic").Start(
t.Context(),
"span",
)

f := func() {
defer span.End()
panic(errors.New("error message"))
}
require.PanicsWithError(t, "error message", f)
spans := te.Spans()
require.Len(t, spans, 1)
require.Len(t, spans[0].Events(), 1)
assert.Equal(t, semconv.ExceptionEventName, spans[0].Events()[0].Name)
assert.Equal(t, "*errors.errorString", spans[0].Events()[0].Attributes[0].Value.AsString())
assert.Equal(t, "error message", spans[0].Events()[0].Attributes[1].Value.AsString())

gotStackTraceFunctionName := strings.Split(spans[0].Events()[0].Attributes[2].Value.AsString(), "\n")
assert.Truef(
t,
strings.HasPrefix(gotStackTraceFunctionName[1], "go.opentelemetry.io/otel/sdk/trace.recordStackTrace"),
"%q not prefixed with go.opentelemetry.io/otel/sdk/trace.recordStackTrace",
gotStackTraceFunctionName[1],
)
assert.Truef(
t,
strings.HasPrefix(gotStackTraceFunctionName[3], "go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End"),
"%q not prefixed with go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End",
gotStackTraceFunctionName[3],
)
}

func TestReadOnlySpan(t *testing.T) {
kv := attribute.String("foo", "bar")

Expand Down
Loading