diff --git a/otel/go.mod b/otel/go.mod index 073af2f3d..08245a9b5 100644 --- a/otel/go.mod +++ b/otel/go.mod @@ -7,14 +7,19 @@ replace github.com/getsentry/sentry-go => ../ require ( github.com/getsentry/sentry-go v0.32.0 github.com/google/go-cmp v0.5.9 + github.com/stretchr/testify v1.8.2 go.opentelemetry.io/otel v1.11.0 go.opentelemetry.io/otel/sdk v1.11.0 go.opentelemetry.io/otel/trace v1.11.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/otel/go.sum b/otel/go.sum index 2d524bc16..adb2f231e 100644 --- a/otel/go.sum +++ b/otel/go.sum @@ -1,3 +1,5 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -9,12 +11,23 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk= @@ -29,5 +42,9 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/otel/internal/utils/mapotelstatus_test.go b/otel/internal/utils/mapotelstatus_test.go index 7629403e8..31eae9ed7 100644 --- a/otel/internal/utils/mapotelstatus_test.go +++ b/otel/internal/utils/mapotelstatus_test.go @@ -8,13 +8,16 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.12.0" + oteltrace "go.opentelemetry.io/otel/trace" "strconv" "testing" ) type mockReadOnlySpan struct { trace.ReadOnlySpan + name string status trace.Status + spanKind oteltrace.SpanKind attributes []attribute.KeyValue } @@ -28,6 +31,14 @@ func (m *mockReadOnlySpan) Status() trace.Status { return m.status } +func (m *mockReadOnlySpan) SpanKind() oteltrace.SpanKind { + return m.spanKind +} + +func (m *mockReadOnlySpan) Name() string { + return m.name +} + func TestMapOtelStatus(t *testing.T) { t.Run("Given no meaningful attributes to derive the status", func(t *testing.T) { tests := []struct { diff --git a/otel/internal/utils/spanattributes.go b/otel/internal/utils/spanattributes.go index 65e3c25ae..e7f6536a5 100644 --- a/otel/internal/utils/spanattributes.go +++ b/otel/internal/utils/spanattributes.go @@ -102,15 +102,17 @@ func descriptionForHttpMethod(s otelSdkTrace.ReadOnlySpan) SpanAttributes { } var httpPath string - if httpTarget != "" { + + // Prefer httpRoute if available + if httpRoute != "" { + httpPath = httpRoute + } else if httpTarget != "" { if parsedUrl, err := url.Parse(httpTarget); err == nil { // Do not include the query and fragment parts httpPath = parsedUrl.Path } else { httpPath = httpTarget } - } else if httpRoute != "" { - httpPath = httpRoute } else if httpUrl != "" { // This is normally the HTTP-client case if parsedUrl, err := url.Parse(httpUrl); err == nil { diff --git a/otel/internal/utils/spanattributes_test.go b/otel/internal/utils/spanattributes_test.go new file mode 100644 index 000000000..43c33233d --- /dev/null +++ b/otel/internal/utils/spanattributes_test.go @@ -0,0 +1,174 @@ +package utils_test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" + oteltrace "go.opentelemetry.io/otel/trace" + + "github.com/getsentry/sentry-go" + "github.com/getsentry/sentry-go/otel/internal/utils" +) + +func TestParseSpanAttributes(t *testing.T) { + t.Run("Handles HTTP spans", func(t *testing.T) { + t.Run("Prefers httpRoute over httpTarget", func(t *testing.T) { + span := &mockReadOnlySpan{ + name: "", + spanKind: oteltrace.SpanKindServer, + attributes: []attribute.KeyValue{ + semconv.HTTPMethodKey.String(http.MethodOptions), + semconv.HTTPTargetKey.String("/projects/123/settings?q=proj#123"), + semconv.HTTPRouteKey.String("/projects/:projectID/settings"), + semconv.HTTPURLKey.String("https://sentry.io/projects/:projectID/settings?q=proj#123"), + }, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, "OPTIONS /projects/:projectID/settings", parsed.Description) + assert.Equal(t, "http.server", parsed.Op) + assert.Equal(t, sentry.SourceRoute, parsed.Source) + }) + + t.Run("Falls back to httpTarget when httpRoute is missing", func(t *testing.T) { + span := &mockReadOnlySpan{ + name: "", + spanKind: oteltrace.SpanKindClient, + attributes: []attribute.KeyValue{ + semconv.HTTPMethodKey.String(http.MethodGet), + semconv.HTTPTargetKey.String("/users?page=2"), + semconv.HTTPURLKey.String("https://sentry.io/users?page=2"), + }, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, "GET /users", parsed.Description) + assert.Equal(t, "http.client", parsed.Op) + assert.Equal(t, sentry.SourceURL, parsed.Source) + }) + + t.Run("Falls back to httpUrl if route and target are missing", func(t *testing.T) { + span := &mockReadOnlySpan{ + name: "", + spanKind: oteltrace.SpanKindClient, + attributes: []attribute.KeyValue{ + semconv.HTTPMethodKey.String(http.MethodGet), + semconv.HTTPURLKey.String("https://sentry.io/api/v1/issues?limit=10"), + }, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, "GET https://sentry.io/api/v1/issues", parsed.Description) + assert.Equal(t, "http.client", parsed.Op) + assert.Equal(t, sentry.SourceURL, parsed.Source) + }) + + t.Run("Uses fallback when no URL info exists", func(t *testing.T) { + span := &mockReadOnlySpan{ + name: "Some description", + spanKind: oteltrace.SpanKindClient, + attributes: []attribute.KeyValue{ + semconv.HTTPMethodKey.String(http.MethodGet), + }, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, "Some description", parsed.Description) + assert.Equal(t, "http.client", parsed.Op) + assert.Equal(t, sentry.SourceCustom, parsed.Source) + }) + }) + + t.Run("Falls back to raw httpTarget when URL parsing fails", func(t *testing.T) { + invalidTarget := "://bad:url::not_valid" + span := &mockReadOnlySpan{ + name: "", + spanKind: oteltrace.SpanKindServer, + attributes: []attribute.KeyValue{ + semconv.HTTPMethodKey.String(http.MethodGet), + semconv.HTTPTargetKey.String(invalidTarget), + }, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, fmt.Sprintf("GET %s", invalidTarget), parsed.Description) + assert.Equal(t, "http.server", parsed.Op) + assert.Equal(t, sentry.SourceURL, parsed.Source) + }) + + t.Run("Handles DB spans", func(t *testing.T) { + t.Run("Includes DB statement in description", func(t *testing.T) { + stmt := "SELECT * FROM users" + span := &mockReadOnlySpan{ + name: "", + attributes: []attribute.KeyValue{ + semconv.DBSystemKey.String("postgresql"), + semconv.DBStatementKey.String(stmt), + }, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, stmt, parsed.Description) + assert.Equal(t, "db", parsed.Op) + assert.Equal(t, sentry.SourceTask, parsed.Source) + }) + }) + + t.Run("Handles RPC spans", func(t *testing.T) { + span := &mockReadOnlySpan{ + name: "rpc call", + attributes: []attribute.KeyValue{ + semconv.RPCSystemKey.String("grpc"), + }, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, "rpc", parsed.Op) + assert.Equal(t, "rpc call", parsed.Description) + assert.Equal(t, sentry.SourceRoute, parsed.Source) + }) + + t.Run("Handles Messaging spans", func(t *testing.T) { + span := &mockReadOnlySpan{ + name: "publish event", + attributes: []attribute.KeyValue{ + semconv.MessagingSystemKey.String("kafka"), + }, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, "messaging", parsed.Op) + assert.Equal(t, "publish event", parsed.Description) + assert.Equal(t, sentry.SourceRoute, parsed.Source) + }) + + t.Run("Handles FaaS spans", func(t *testing.T) { + span := &mockReadOnlySpan{ + name: "lambda triggered", + attributes: []attribute.KeyValue{ + semconv.FaaSTriggerKey.String("http"), + }, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, "http", parsed.Op) + assert.Equal(t, "lambda triggered", parsed.Description) + assert.Equal(t, sentry.SourceRoute, parsed.Source) + }) + + t.Run("Handles unknown span types", func(t *testing.T) { + span := &mockReadOnlySpan{ + name: "some span", + attributes: []attribute.KeyValue{}, + } + + parsed := utils.ParseSpanAttributes(span) + assert.Equal(t, "", parsed.Op) + assert.Equal(t, "some span", parsed.Description) + assert.Equal(t, sentry.SourceCustom, parsed.Source) + }) +}