Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
149 lines
4.5 KiB
Go
149 lines
4.5 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package otelx
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
|
|
pkgerrors "github.com/pkg/errors"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
// WithSpan wraps execution of f in a span identified by name.
|
|
//
|
|
// If f returns an error or panics, the span status will be set to the error
|
|
// state. The error (or panic) will be propagated unmodified.
|
|
//
|
|
// f will be wrapped in a child span by default. To make a new root span
|
|
// instead, pass the trace.WithNewRoot() option.
|
|
func WithSpan(ctx context.Context, name string, f func(context.Context) error, opts ...trace.SpanStartOption) (err error) {
|
|
ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, name, opts...)
|
|
defer func() {
|
|
defer span.End()
|
|
if r := recover(); r != nil {
|
|
setErrorStatusPanic(span, r)
|
|
panic(r)
|
|
} else if err != nil {
|
|
span.SetStatus(codes.Error, err.Error())
|
|
setErrorTags(span, err)
|
|
}
|
|
}()
|
|
return f(ctx)
|
|
}
|
|
|
|
// End finishes span, and automatically sets the error state if *err is not nil
|
|
// or during panicking.
|
|
//
|
|
// Usage:
|
|
//
|
|
// func Divide(ctx context.Context, numerator, denominator int) (ratio int, err error) {
|
|
// ctx, span := tracer.Start(ctx, "Divide")
|
|
// defer otelx.End(span, &err)
|
|
// if denominator == 0 {
|
|
// return 0, errors.New("cannot divide by zero")
|
|
// }
|
|
// return numerator / denominator, nil
|
|
// }
|
|
//
|
|
// During a panic, we don't fully conform to OpenTelemetry's semantic
|
|
// conventions because that would require us to emit a span event to attach the
|
|
// stacktrace and error type, and we don't want to do that. Instead, we set the
|
|
// tags on the span directly.
|
|
// https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/
|
|
//
|
|
// For improved compatibility with Datadog, we also set some additional tags as
|
|
// documented here:
|
|
// https://docs.datadoghq.com/standard-attributes/?product=apm&search=error
|
|
func End(span trace.Span, err *error) {
|
|
defer span.End()
|
|
if r := recover(); r != nil {
|
|
setErrorStatusPanic(span, r)
|
|
panic(r)
|
|
}
|
|
if err == nil || *err == nil {
|
|
return
|
|
}
|
|
span.SetStatus(codes.Error, (*err).Error())
|
|
setErrorTags(span, *err)
|
|
}
|
|
|
|
func setErrorStatusPanic(span trace.Span, recovered any) {
|
|
span.SetAttributes(
|
|
// OpenTelemetry says to add these attributes to an event, not the span
|
|
// itself. We don't want to do that, so we're adding them to the span
|
|
// directly.
|
|
semconv.ExceptionEscaped(true),
|
|
// OpenTelemetry describes "exception.stacktrace" We don't love that,
|
|
// though, so we're using "error.stack" instead, like DataDog).
|
|
attribute.String("error.stack", stacktrace()),
|
|
)
|
|
if t := reflect.TypeOf(recovered); t != nil {
|
|
span.SetAttributes(semconv.ExceptionType(t.String()))
|
|
}
|
|
switch e := recovered.(type) {
|
|
case error:
|
|
span.SetStatus(codes.Error, "panic: "+e.Error())
|
|
setErrorTags(span, e)
|
|
case string, fmt.Stringer:
|
|
span.SetStatus(codes.Error, fmt.Sprintf("panic: %v", e))
|
|
default:
|
|
span.SetStatus(codes.Error, "panic")
|
|
case nil:
|
|
// nothing
|
|
}
|
|
}
|
|
|
|
func setErrorTags(span trace.Span, err error) {
|
|
span.SetAttributes(
|
|
attribute.String("error", err.Error()),
|
|
attribute.String("error.message", err.Error()), // DataDog compat
|
|
attribute.String("error.type", fmt.Sprintf("%T", errors.Unwrap(err))), // the innermost error type is the most useful here
|
|
)
|
|
if e := interface{ StackTrace() pkgerrors.StackTrace }(nil); errors.As(err, &e) {
|
|
span.SetAttributes(attribute.String("error.stack", fmt.Sprintf("%+v", e.StackTrace())))
|
|
}
|
|
if e := interface{ Reason() string }(nil); errors.As(err, &e) {
|
|
span.SetAttributes(attribute.String("error.reason", e.Reason()))
|
|
}
|
|
if e := interface{ Debug() string }(nil); errors.As(err, &e) {
|
|
span.SetAttributes(attribute.String("error.debug", e.Debug()))
|
|
}
|
|
if e := interface{ ID() string }(nil); errors.As(err, &e) {
|
|
span.SetAttributes(attribute.String("error.id", e.ID()))
|
|
}
|
|
if e := interface{ Details() map[string]interface{} }(nil); errors.As(err, &e) {
|
|
for k, v := range e.Details() {
|
|
span.SetAttributes(attribute.String("error.details."+k, fmt.Sprintf("%v", v)))
|
|
}
|
|
}
|
|
}
|
|
|
|
func stacktrace() string {
|
|
pc := make([]uintptr, 5)
|
|
n := runtime.Callers(4, pc)
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
pc = pc[:n]
|
|
frames := runtime.CallersFrames(pc)
|
|
|
|
var builder strings.Builder
|
|
for {
|
|
frame, more := frames.Next()
|
|
fmt.Fprintf(&builder, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)
|
|
if !more {
|
|
break
|
|
}
|
|
}
|
|
return builder.String()
|
|
}
|