diff --git a/CHANGELOG.md b/CHANGELOG.md index 287232c06..1de7db057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http - Support Go standard libraries for 1.22.9 and 1.23.3. ([#1250](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1250)) - Support `google.golang.org/grpc` `1.68.0`. ([#1251](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1251)) +- 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/go.mod b/go.mod index e61ad601d..569cbb83d 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 go.opentelemetry.io/contrib/bridges/prometheus v0.56.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.7.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.7.0 // indirect diff --git a/go.sum b/go.sum index 856979e2a..b73493689 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,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.18.0 h1:/yg2rO2dxqDM2p6GutsMCxXN6sKlXwyIz/ZYyUPONBg= diff --git a/internal/pkg/instrumentation/bpf/database/sql/probe.go b/internal/pkg/instrumentation/bpf/database/sql/probe.go index 8bf0c06c9..3a6bad29f 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" @@ -95,6 +97,26 @@ func processFn(e *event) ptrace.SpanSlice { query := unix.ByteSliceToString(e.Query[:]) if query != "" { span.Attributes().PutStr(string(semconv.DBQueryTextKey), query) + + 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..3d2be2789 100644 --- a/internal/pkg/instrumentation/bpf/database/sql/probe_test.go +++ b/internal/pkg/instrumentation/bpf/database/sql/probe_test.go @@ -41,14 +41,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..21c90e620 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("queryDb 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("queryDb 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("queryDb 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("queryDb 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 e718c8f2c..b9f98a92d 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} }