diff --git a/.github/workflows/e2e/k8s/sample-job.yml b/.github/workflows/e2e/k8s/sample-job.yml index 2ac20a83f..5abcc2c36 100644 --- a/.github/workflows/e2e/k8s/sample-job.yml +++ b/.github/workflows/e2e/k8s/sample-job.yml @@ -38,6 +38,8 @@ spec: value: "tracecontext,baggage" - name: OTEL_GO_AUTO_INCLUDE_DB_STATEMENT value: "true" + - name: OTEL_GO_AUTO_INCLUDE_DB_OPERATION + value: "true" - name: OTEL_BSP_SCHEDULE_DELAY value: "60000" resources: {} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dbdc12b3..417fce4fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http - Support `google.golang.org/grpc` `1.68.0`. ([#1251](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1251)) - Support `golang.org/x/net` `0.31.0`. ([#1254](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1254)) - Support `go.opentelemetry.io/otel@v1.32.0`. ([#1302](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1302)) +- Support `SELECT`, `INSERT`, `UPDATE`, and `DELETE` for database span names and `db.operation.name` attribute. ([#1253](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1253)) ### Fixed diff --git a/docs/configuration.md b/docs/configuration.md index d449efb12..4b1000e18 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,6 +27,7 @@ Alternatively, you can add support for additional or different configurations by | Environment variable | Description | Default value | |-------------------------------------|--------------------------------------------------------|---------------| | `OTEL_GO_AUTO_INCLUDE_DB_STATEMENT` | Sets whether to include SQL queries in the trace data. | | +| `OTEL_GO_AUTO_INCLUDE_DB_OPERATION` | Sets whether to include SQL operation in the trace data. | | ## Traces exporter diff --git a/go.mod b/go.mod index 0a7dd4b73..793c9e65a 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/hashicorp/go-version v1.7.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 + github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 go.opentelemetry.io/collector/pdata v1.19.0 go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 go.opentelemetry.io/otel v1.32.0 diff --git a/go.sum b/go.sum index 13d318c07..f44ff8da9 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 h1:zzrxE1FKn5ryBNl9eKOeqQ58Y/Qpo3Q9QNxKHX5uzzQ= +github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2/go.mod h1:hzfGeIUDq/j97IG+FhNqkowIyEcD88LrW6fyU3K3WqY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/collector/pdata v1.19.0 h1:jmnU5R8TOCbwRr4B8sjdRxM7L5WnEKlQWX1dtLYxIbE= diff --git a/internal/pkg/instrumentation/bpf/database/sql/probe.go b/internal/pkg/instrumentation/bpf/database/sql/probe.go index 8bf0c06c9..2e35cfed3 100644 --- a/internal/pkg/instrumentation/bpf/database/sql/probe.go +++ b/internal/pkg/instrumentation/bpf/database/sql/probe.go @@ -8,6 +8,8 @@ import ( "os" "strconv" + sql "github.com/xwb1989/sqlparser" + "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" @@ -27,6 +29,9 @@ const ( // IncludeDBStatementEnvVar is the environment variable to opt-in for sql query inclusion in the trace. IncludeDBStatementEnvVar = "OTEL_GO_AUTO_INCLUDE_DB_STATEMENT" + + // IncludeDBOperationEnvVar is the environment variable to opt-in for sql query operation in the trace. + IncludeDBOperationEnvVar = "OTEL_GO_AUTO_INCLUDE_DB_OPERATION" ) // New returns a new [probe.Probe]. @@ -97,6 +102,32 @@ func processFn(e *event) ptrace.SpanSlice { span.Attributes().PutStr(string(semconv.DBQueryTextKey), query) } + includeOperationVal := os.Getenv(IncludeDBOperationEnvVar) + if includeOperationVal != "" { + include, err := strconv.ParseBool(includeOperationVal) + if err == nil && include { + q, err := sql.Parse(query) + if err == nil { + operation := "" + switch q.(type) { + case *sql.Select: + operation = "SELECT" + case *sql.Update: + operation = "UPDATE" + case *sql.Insert: + operation = "INSERT" + case *sql.Delete: + operation = "DELETE" + } + + if operation != "" { + span.Attributes().PutStr(string(semconv.DBOperationNameKey), operation) + span.SetName(operation) + } + } + } + } + return spans } diff --git a/internal/pkg/instrumentation/bpf/database/sql/probe_test.go b/internal/pkg/instrumentation/bpf/database/sql/probe_test.go index 44e3ca99e..1bea454e9 100644 --- a/internal/pkg/instrumentation/bpf/database/sql/probe_test.go +++ b/internal/pkg/instrumentation/bpf/database/sql/probe_test.go @@ -4,6 +4,7 @@ package sql import ( + "os" "testing" "time" @@ -18,7 +19,60 @@ import ( "go.opentelemetry.io/auto/internal/pkg/instrumentation/utils" ) +func BenchmarkProcessFn(b *testing.B) { + tests := []struct { + name string + query string + }{ + { + name: "no query (baseline)", + query: "", + }, + { + name: "simple query", + query: "SELECT * FROM customers", + }, + { + name: "medium query", + query: "SELECT * FROM customers WHERE first_name='Mike' AND last_name IN ('Santa', 'Banana')", + }, + { + name: "hard query", + query: "WITH (SELECT last_name FROM customers WHERE first_name='Mike' AND country='North Pole') AS test_table SELECT * FROM test_table WHERE first_name='Mike' AND last_name IN ('Santa', 'Banana')", + }, + } + + start := time.Unix(0, time.Now().UnixNano()) // No wall clock. + end := start.Add(1 * time.Second) + + startOffset := utils.TimeToBootOffset(start) + endOffset := utils.TimeToBootOffset(end) + + traceID := trace.TraceID{1} + spanID := trace.SpanID{1} + + for _, t := range tests { + b.Run(t.name, func(b *testing.B) { + var byteQuery [256]byte + copy(byteQuery[:], []byte(t.query)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = processFn(&event{ + BaseSpanProperties: context.BaseSpanProperties{ + StartTime: startOffset, + EndTime: endOffset, + SpanContext: context.EBPFSpanContext{TraceID: traceID, SpanID: spanID}, + }, + Query: byteQuery, + }) + } + }) + } +} + func TestProbeConvertEvent(t *testing.T) { + err := os.Setenv(IncludeDBOperationEnvVar, "true") + assert.NoError(t, err) start := time.Unix(0, time.Now().UnixNano()) // No wall clock. end := start.Add(1 * time.Second) @@ -41,14 +95,14 @@ func TestProbeConvertEvent(t *testing.T) { want := func() ptrace.SpanSlice { spans := ptrace.NewSpanSlice() span := spans.AppendEmpty() - span.SetName("DB") + span.SetName("SELECT") span.SetKind(ptrace.SpanKindClient) span.SetStartTimestamp(utils.BootOffsetToTimestamp(startOffset)) span.SetEndTimestamp(utils.BootOffsetToTimestamp(endOffset)) span.SetTraceID(pcommon.TraceID(traceID)) span.SetSpanID(pcommon.SpanID(spanID)) span.SetFlags(uint32(trace.FlagsSampled)) - utils.Attributes(span.Attributes(), semconv.DBQueryText("SELECT * FROM foo")) + utils.Attributes(span.Attributes(), semconv.DBQueryText("SELECT * FROM foo"), semconv.DBOperationName("SELECT")) return spans }() assert.Equal(t, want, got) diff --git a/internal/test/e2e/databasesql/main.go b/internal/test/e2e/databasesql/main.go index 87931ddd0..2acf4bec4 100644 --- a/internal/test/e2e/databasesql/main.go +++ b/internal/test/e2e/databasesql/main.go @@ -100,6 +100,118 @@ func (s *Server) queryDb(w http.ResponseWriter, req *http.Request) { } } +func (s *Server) insert(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + conn, err := s.db.Conn(ctx) + if err != nil { + panic(err) + } + + rows, err := conn.QueryContext(req.Context(), "INSERT INTO contacts (first_name) VALUES ('Mike')") + if err != nil { + panic(err) + } + + logger.Info("insert called") + for rows.Next() { + var id int + var firstName string + var lastName string + var email string + var phone string + err := rows.Scan(&id, &firstName, &lastName, &email, &phone) + if err != nil { + panic(err) + } + fmt.Fprintf(w, "ID: %d, firstName: %s, lastName: %s, email: %s, phone: %s\n", id, firstName, lastName, email, phone) + } +} + +func (s *Server) update(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + conn, err := s.db.Conn(ctx) + if err != nil { + panic(err) + } + + rows, err := conn.QueryContext(req.Context(), "UPDATE contacts SET last_name = 'Santa' WHERE first_name = 'Mike'") + if err != nil { + panic(err) + } + + logger.Info("update called") + for rows.Next() { + var id int + var firstName string + var lastName string + var email string + var phone string + err := rows.Scan(&id, &firstName, &lastName, &email, &phone) + if err != nil { + panic(err) + } + fmt.Fprintf(w, "ID: %d, firstName: %s, lastName: %s, email: %s, phone: %s\n", id, firstName, lastName, email, phone) + } +} + +func (s *Server) delete(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + conn, err := s.db.Conn(ctx) + if err != nil { + panic(err) + } + + rows, err := conn.QueryContext(req.Context(), "DELETE FROM contacts WHERE first_name = 'Mike'") + if err != nil { + panic(err) + } + + logger.Info("delete called") + for rows.Next() { + var id int + var firstName string + var lastName string + var email string + var phone string + err := rows.Scan(&id, &firstName, &lastName, &email, &phone) + if err != nil { + panic(err) + } + fmt.Fprintf(w, "ID: %d, firstName: %s, lastName: %s, email: %s, phone: %s\n", id, firstName, lastName, email, phone) + } +} + +func (s *Server) drop(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + conn, err := s.db.Conn(ctx) + if err != nil { + panic(err) + } + + rows, err := conn.QueryContext(req.Context(), "DROP TABLE contacts") + if err != nil { + panic(err) + } + + logger.Info("drop called") + for rows.Next() { + var id int + var firstName string + var lastName string + var email string + var phone string + err := rows.Scan(&id, &firstName, &lastName, &email, &phone) + if err != nil { + panic(err) + } + fmt.Fprintf(w, "ID: %d, firstName: %s, lastName: %s, email: %s, phone: %s\n", id, firstName, lastName, email, phone) + } +} + var logger *zap.Logger func main() { @@ -115,6 +227,10 @@ func main() { s := NewServer() http.HandleFunc("/query_db", s.queryDb) + http.HandleFunc("/insert", s.insert) + http.HandleFunc("/update", s.update) + http.HandleFunc("/delete", s.delete) + http.HandleFunc("/drop", s.drop) go func() { _ = http.ListenAndServe(":8080", nil) }() @@ -134,6 +250,54 @@ func main() { logger.Info("Body:\n", zap.String("body", string(body[:]))) _ = resp.Body.Close() + resp, err = http.Get("http://localhost:8080/insert") + if err != nil { + logger.Error("Error performing GET", zap.Error(err)) + } + body, err = io.ReadAll(resp.Body) + if err != nil { + logger.Error("Error reading http body", zap.Error(err)) + } + + logger.Info("Body:\n", zap.String("body", string(body[:]))) + _ = resp.Body.Close() + + resp, err = http.Get("http://localhost:8080/update") + if err != nil { + logger.Error("Error performing GET", zap.Error(err)) + } + body, err = io.ReadAll(resp.Body) + if err != nil { + logger.Error("Error reading http body", zap.Error(err)) + } + + logger.Info("Body:\n", zap.String("body", string(body[:]))) + _ = resp.Body.Close() + + resp, err = http.Get("http://localhost:8080/delete") + if err != nil { + logger.Error("Error performing GET", zap.Error(err)) + } + body, err = io.ReadAll(resp.Body) + if err != nil { + logger.Error("Error reading http body", zap.Error(err)) + } + + logger.Info("Body:\n", zap.String("body", string(body[:]))) + _ = resp.Body.Close() + + resp, err = http.Get("http://localhost:8080/drop") + if err != nil { + logger.Error("Error performing GET", zap.Error(err)) + } + body, err = io.ReadAll(resp.Body) + if err != nil { + logger.Error("Error reading http body", zap.Error(err)) + } + + logger.Info("Body:\n", zap.String("body", string(body[:]))) + _ = resp.Body.Close() + // give time for auto-instrumentation to report signal time.Sleep(5 * time.Second) } diff --git a/internal/test/e2e/databasesql/traces.json b/internal/test/e2e/databasesql/traces.json index 8abff7384..e5c2ca283 100644 --- a/internal/test/e2e/databasesql/traces.json +++ b/internal/test/e2e/databasesql/traces.json @@ -63,6 +63,98 @@ "value": { "stringValue": "SELECT * FROM contacts" } + }, + { + "key": "db.operation.name", + "value": { + "stringValue": "SELECT" + } + } + ], + "flags": 256, + "kind": 3, + "name": "SELECT", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "db.query.text", + "value": { + "stringValue": "INSERT INTO contacts (first_name) VALUES ('Mike')" + } + }, + { + "key": "db.operation.name", + "value": { + "stringValue": "INSERT" + } + } + ], + "flags": 256, + "kind": 3, + "name": "INSERT", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "db.query.text", + "value": { + "stringValue": "UPDATE contacts SET last_name = 'Santa' WHERE first_name = 'Mike'" + } + }, + { + "key": "db.operation.name", + "value": { + "stringValue": "UPDATE" + } + } + ], + "flags": 256, + "kind": 3, + "name": "UPDATE", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "db.query.text", + "value": { + "stringValue": "DELETE FROM contacts WHERE first_name = 'Mike'" + } + }, + { + "key": "db.operation.name", + "value": { + "stringValue": "DELETE" + } + } + ], + "flags": 256, + "kind": 3, + "name": "DELETE", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "db.query.text", + "value": { + "stringValue": "DROP TABLE contacts" + } } ], "flags": 256, @@ -155,22 +247,494 @@ "stringValue": "GET" } }, + { + "key": "url.path", + "value": { + "stringValue": "/insert" + } + }, { "key": "http.response.status_code", "value": { - "intValue": "200" + "intValue": "0" + } + }, + { + "key": "network.peer.address", + "value": { + "stringValue": "::1" + } + }, + { + "key": "network.peer.port", + "value": { + "intValue": "xxxxx" + } + }, + { + "key": "server.address", + "value": { + "stringValue": "localhost" + } + }, + { + "key": "server.port", + "value": { + "intValue": "8080" + } + }, + { + "key": "network.protocol.version", + "value": { + "stringValue": "1.1" + } + }, + { + "key": "http.route", + "value": { + "stringValue": "/insert" + } + } + ], + "flags": 256, + "kind": 2, + "name": "GET /insert", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "http.request.method", + "value": { + "stringValue": "GET" } }, { "key": "url.path", "value": { - "stringValue": "/query_db" + "stringValue": "/update" } }, { - "key": "url.full", + "key": "http.response.status_code", "value": { - "stringValue": "http://localhost:8080/query_db" + "intValue": "0" + } + }, + { + "key": "network.peer.address", + "value": { + "stringValue": "::1" + } + }, + { + "key": "network.peer.port", + "value": { + "intValue": "xxxxx" + } + }, + { + "key": "server.address", + "value": { + "stringValue": "localhost" + } + }, + { + "key": "server.port", + "value": { + "intValue": "8080" + } + }, + { + "key": "network.protocol.version", + "value": { + "stringValue": "1.1" + } + }, + { + "key": "http.route", + "value": { + "stringValue": "/update" + } + } + ], + "flags": 256, + "kind": 2, + "name": "GET /update", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "http.request.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "url.path", + "value": { + "stringValue": "/delete" + } + }, + { + "key": "http.response.status_code", + "value": { + "intValue": "0" + } + }, + { + "key": "network.peer.address", + "value": { + "stringValue": "::1" + } + }, + { + "key": "network.peer.port", + "value": { + "intValue": "xxxxx" + } + }, + { + "key": "server.address", + "value": { + "stringValue": "localhost" + } + }, + { + "key": "server.port", + "value": { + "intValue": "8080" + } + }, + { + "key": "network.protocol.version", + "value": { + "stringValue": "1.1" + } + }, + { + "key": "http.route", + "value": { + "stringValue": "/delete" + } + } + ], + "flags": 256, + "kind": 2, + "name": "GET /delete", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "http.request.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "url.path", + "value": { + "stringValue": "/drop" + } + }, + { + "key": "http.response.status_code", + "value": { + "intValue": "0" + } + }, + { + "key": "network.peer.address", + "value": { + "stringValue": "::1" + } + }, + { + "key": "network.peer.port", + "value": { + "intValue": "xxxxx" + } + }, + { + "key": "server.address", + "value": { + "stringValue": "localhost" + } + }, + { + "key": "server.port", + "value": { + "intValue": "8080" + } + }, + { + "key": "network.protocol.version", + "value": { + "stringValue": "1.1" + } + }, + { + "key": "http.route", + "value": { + "stringValue": "/drop" + } + } + ], + "flags": 256, + "kind": 2, + "name": "GET /drop", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "http.request.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "http.response.status_code", + "value": { + "intValue": "200" + } + }, + { + "key": "url.path", + "value": { + "stringValue": "/query_db" + } + }, + { + "key": "url.full", + "value": { + "stringValue": "http://localhost:8080/query_db" + } + }, + { + "key": "server.address", + "value": { + "stringValue": "localhost" + } + }, + { + "key": "server.port", + "value": { + "intValue": "8080" + } + }, + { + "key": "network.protocol.version", + "value": { + "stringValue": "1.1" + } + } + ], + "flags": 256, + "kind": 3, + "name": "GET", + "parentSpanId": "", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "http.request.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "http.response.status_code", + "value": { + "intValue": "200" + } + }, + { + "key": "url.path", + "value": { + "stringValue": "/insert" + } + }, + { + "key": "url.full", + "value": { + "stringValue": "http://localhost:8080/insert" + } + }, + { + "key": "server.address", + "value": { + "stringValue": "localhost" + } + }, + { + "key": "server.port", + "value": { + "intValue": "8080" + } + }, + { + "key": "network.protocol.version", + "value": { + "stringValue": "1.1" + } + } + ], + "flags": 256, + "kind": 3, + "name": "GET", + "parentSpanId": "", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "http.request.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "http.response.status_code", + "value": { + "intValue": "200" + } + }, + { + "key": "url.path", + "value": { + "stringValue": "/update" + } + }, + { + "key": "url.full", + "value": { + "stringValue": "http://localhost:8080/update" + } + }, + { + "key": "server.address", + "value": { + "stringValue": "localhost" + } + }, + { + "key": "server.port", + "value": { + "intValue": "8080" + } + }, + { + "key": "network.protocol.version", + "value": { + "stringValue": "1.1" + } + } + ], + "flags": 256, + "kind": 3, + "name": "GET", + "parentSpanId": "", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "http.request.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "http.response.status_code", + "value": { + "intValue": "200" + } + }, + { + "key": "url.path", + "value": { + "stringValue": "/delete" + } + }, + { + "key": "url.full", + "value": { + "stringValue": "http://localhost:8080/delete" + } + }, + { + "key": "server.address", + "value": { + "stringValue": "localhost" + } + }, + { + "key": "server.port", + "value": { + "intValue": "8080" + } + }, + { + "key": "network.protocol.version", + "value": { + "stringValue": "1.1" + } + } + ], + "flags": 256, + "kind": 3, + "name": "GET", + "parentSpanId": "", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + }, + { + "attributes": [ + { + "key": "http.request.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "http.response.status_code", + "value": { + "intValue": "200" + } + }, + { + "key": "url.path", + "value": { + "stringValue": "/drop" + } + }, + { + "key": "url.full", + "value": { + "stringValue": "http://localhost:8080/drop" } }, { diff --git a/internal/test/e2e/databasesql/verify.bats b/internal/test/e2e/databasesql/verify.bats index 4beb8625f..185d6c629 100644 --- a/internal/test/e2e/databasesql/verify.bats +++ b/internal/test/e2e/databasesql/verify.bats @@ -5,22 +5,67 @@ load ../../test_helpers/utilities.sh SCOPE="go.opentelemetry.io/auto/database/sql" @test "${SCOPE} :: includes db.query.text attribute" { - result=$(span_attributes_for ${SCOPE} | jq "select(.key == \"db.query.text\").value.stringValue") + result=$(span_attributes_for ${SCOPE} | jq "select(.key == \"db.query.text\").value.stringValue" | jq -Rn '[inputs]' | jq -r .[0]) assert_equal "$result" '"SELECT * FROM contacts"' + result=$(span_attributes_for ${SCOPE} | jq "select(.key == \"db.query.text\").value.stringValue" | jq -Rn '[inputs]' | jq -r .[1]) + assert_equal "$result" "\"INSERT INTO contacts (first_name) VALUES ('Mike')\"" + result=$(span_attributes_for ${SCOPE} | jq "select(.key == \"db.query.text\").value.stringValue" | jq -Rn '[inputs]' | jq -r .[2]) + assert_equal "$result" "\"UPDATE contacts SET last_name = 'Santa' WHERE first_name = 'Mike'\"" + result=$(span_attributes_for ${SCOPE} | jq "select(.key == \"db.query.text\").value.stringValue" | jq -Rn '[inputs]' | jq -r .[3]) + assert_equal "$result" "\"DELETE FROM contacts WHERE first_name = 'Mike'\"" + result=$(span_attributes_for ${SCOPE} | jq "select(.key == \"db.query.text\").value.stringValue" | jq -Rn '[inputs]' | jq -r .[4]) + assert_equal "$result" "\"DROP TABLE contacts\"" +} + +@test "${SCOPE} :: span name is set correctly" { + result=$(span_names_for ${SCOPE} | jq -Rn '[inputs]' | jq -r .[0]) + assert_equal "$result" '"SELECT"' + result=$(span_names_for ${SCOPE} | jq -Rn '[inputs]' | jq -r .[1]) + assert_equal "$result" '"INSERT"' + result=$(span_names_for ${SCOPE} | jq -Rn '[inputs]' | jq -r .[2]) + assert_equal "$result" '"UPDATE"' + result=$(span_names_for ${SCOPE} | jq -Rn '[inputs]' | jq -r .[3]) + assert_equal "$result" '"DELETE"' + result=$(span_names_for ${SCOPE} | jq -Rn '[inputs]' | jq -r .[4]) + assert_equal "$result" '"DB"' } @test "${SCOPE} :: trace ID present and valid in all spans" { - trace_id=$(spans_from_scope_named ${SCOPE} | jq ".traceId") + trace_id=$(spans_from_scope_named ${SCOPE} | jq ".traceId" | jq -Rn '[inputs]' | jq -r .[0]) + assert_regex "$trace_id" ${MATCH_A_TRACE_ID} + trace_id=$(spans_from_scope_named ${SCOPE} | jq ".traceId" | jq -Rn '[inputs]' | jq -r .[1]) + assert_regex "$trace_id" ${MATCH_A_TRACE_ID} + trace_id=$(spans_from_scope_named ${SCOPE} | jq ".traceId" | jq -Rn '[inputs]' | jq -r .[2]) + assert_regex "$trace_id" ${MATCH_A_TRACE_ID} + trace_id=$(spans_from_scope_named ${SCOPE} | jq ".traceId" | jq -Rn '[inputs]' | jq -r .[3]) + assert_regex "$trace_id" ${MATCH_A_TRACE_ID} + trace_id=$(spans_from_scope_named ${SCOPE} | jq ".traceId" | jq -Rn '[inputs]' | jq -r .[4]) assert_regex "$trace_id" ${MATCH_A_TRACE_ID} } @test "${SCOPE} :: span ID present and valid in all spans" { - span_id=$(spans_from_scope_named ${SCOPE} | jq ".spanId") + span_id=$(spans_from_scope_named ${SCOPE} | jq ".spanId" | jq -Rn '[inputs]' | jq -r .[0]) + assert_regex "$span_id" ${MATCH_A_SPAN_ID} + span_id=$(spans_from_scope_named ${SCOPE} | jq ".spanId" | jq -Rn '[inputs]' | jq -r .[1]) + assert_regex "$span_id" ${MATCH_A_SPAN_ID} + span_id=$(spans_from_scope_named ${SCOPE} | jq ".spanId" | jq -Rn '[inputs]' | jq -r .[2]) + assert_regex "$span_id" ${MATCH_A_SPAN_ID} + span_id=$(spans_from_scope_named ${SCOPE} | jq ".spanId" | jq -Rn '[inputs]' | jq -r .[3]) + assert_regex "$span_id" ${MATCH_A_SPAN_ID} + span_id=$(spans_from_scope_named ${SCOPE} | jq ".spanId" | jq -Rn '[inputs]' | jq -r .[4]) assert_regex "$span_id" ${MATCH_A_SPAN_ID} } @test "${SCOPE} :: parent span ID present and valid in all spans" { - parent_span_id=$(spans_from_scope_named ${SCOPE} | jq ".parentSpanId") + parent_span_id=$(spans_from_scope_named ${SCOPE} | jq ".parentSpanId" | jq -Rn '[inputs]' | jq -r .[0]) + assert_regex "$parent_span_id" ${MATCH_A_SPAN_ID} + parent_span_id=$(spans_from_scope_named ${SCOPE} | jq ".parentSpanId" | jq -Rn '[inputs]' | jq -r .[1]) + assert_regex "$parent_span_id" ${MATCH_A_SPAN_ID} + parent_span_id=$(spans_from_scope_named ${SCOPE} | jq ".parentSpanId" | jq -Rn '[inputs]' | jq -r .[2]) + assert_regex "$parent_span_id" ${MATCH_A_SPAN_ID} + parent_span_id=$(spans_from_scope_named ${SCOPE} | jq ".parentSpanId" | jq -Rn '[inputs]' | jq -r .[3]) + assert_regex "$parent_span_id" ${MATCH_A_SPAN_ID} + parent_span_id=$(spans_from_scope_named ${SCOPE} | jq ".parentSpanId" | jq -Rn '[inputs]' | jq -r .[4]) assert_regex "$parent_span_id" ${MATCH_A_SPAN_ID} }