Skip to content

Support SELECT/INSERT/UPDATE/DELETE db operation + span names #1253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions .github/workflows/e2e/k8s/sample-job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ spec:
value: "tracecontext,baggage"
- name: OTEL_GO_AUTO_INCLUDE_DB_STATEMENT
value: "true"
- name: OTEL_GO_AUTO_PARSE_DB_STATEMENT
value: "true"
- name: OTEL_BSP_SCHEDULE_DELAY
value: "60000"
resources: {}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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/[email protected]`. ([#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

Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_PARSE_DB_STATEMENT` | Sets whether to parse the SQL statement for trace data, setting `db.operation.name`. Only valid if `OTEL_GO_AUTO_INCLUDE_DB_STATEMENT` is also set. | |

## Traces exporter

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.10.0
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
go.opentelemetry.io/collector/pdata v1.21.0
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0
go.opentelemetry.io/otel v1.32.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.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.21.0 h1:PG+UbiFMJ35X/WcAR7Rf/PWmWtRdW0aHlOidsR6c5MA=
Expand Down
70 changes: 70 additions & 0 deletions internal/pkg/instrumentation/bpf/database/sql/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
package sql

import (
"fmt"
"log/slog"
"os"
"strconv"

"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"
Expand All @@ -27,6 +30,9 @@ const (

// IncludeDBStatementEnvVar is the environment variable to opt-in for sql query inclusion in the trace.
IncludeDBStatementEnvVar = "OTEL_GO_AUTO_INCLUDE_DB_STATEMENT"

// ParseDBStatementEnvVar is the environment variable to opt-in for sql query operation in the trace.
ParseDBStatementEnvVar = "OTEL_GO_AUTO_PARSE_DB_STATEMENT"
)

// New returns a new [probe.Probe].
Expand Down Expand Up @@ -97,6 +103,31 @@ func processFn(e *event) ptrace.SpanSlice {
span.Attributes().PutStr(string(semconv.DBQueryTextKey), query)
}

includeOperationVal := os.Getenv(ParseDBStatementEnvVar)
if includeOperationVal != "" {
include, err := strconv.ParseBool(includeOperationVal)
if err == nil && include {
operation, target, err := Parse(query)
if err == nil {
name := ""
if operation != "" {
span.Attributes().PutStr(string(semconv.DBOperationNameKey), operation)
name = operation
}
if target != "" {
span.Attributes().PutStr(string(semconv.DBCollectionNameKey), target)
if name != "" {
// if operation is in the name and target is available, set name to {operation} {target}
name += " " + target
}
}
if name != "" {
span.SetName(name)
}
}
}
}

return spans
}

Expand All @@ -112,3 +143,42 @@ func shouldIncludeDBStatement() bool {

return false
}

// Parse takes a SQL query string and returns the parsed query statement type
// and table name, or an error if parsing failed.
func Parse(query string) (string, string, error) {
stmt, err := sqlparser.Parse(query)
if err != nil {
return "", "", fmt.Errorf("failed to parse query: %w", err)
}

switch stmt := stmt.(type) {
case *sqlparser.Select:
return "SELECT", getTableName(stmt.From), nil
case *sqlparser.Update:
return "UPDATE", getTableName(stmt.TableExprs), nil
case *sqlparser.Insert:
return "INSERT", stmt.Table.Name.String(), nil
case *sqlparser.Delete:
return "DELETE", getTableName(stmt.TableExprs), nil
default:
return "", "", fmt.Errorf("unsupported operation")
}
}

// getTableName extracts the table name from a SQL node.
func getTableName(node sqlparser.SQLNode) string {
switch tableExpr := node.(type) {
case sqlparser.TableName:
return tableExpr.Name.String()
case sqlparser.TableExprs:
for _, expr := range tableExpr {
if tableName, ok := expr.(*sqlparser.AliasedTableExpr); ok {
if name, ok := tableName.Expr.(sqlparser.TableName); ok {
return name.Name.String()
}
}
}
}
return ""
}
57 changes: 55 additions & 2 deletions internal/pkg/instrumentation/bpf/database/sql/probe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,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.ReportAllocs()
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) {
t.Setenv(ParseDBStatementEnvVar, "true")
start := time.Unix(0, time.Now().UnixNano()) // No wall clock.
end := start.Add(1 * time.Second)

Expand All @@ -41,14 +94,14 @@ func TestProbeConvertEvent(t *testing.T) {
want := func() ptrace.SpanSlice {
spans := ptrace.NewSpanSlice()
span := spans.AppendEmpty()
span.SetName("DB")
span.SetName("SELECT foo")
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"), semconv.DBCollectionName("foo"))
return spans
}()
assert.Equal(t, want, got)
Expand Down
78 changes: 60 additions & 18 deletions internal/test/e2e/databasesql/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import (
)

const (
sqlQuery = "SELECT * FROM contacts"
dbName = "test.db"
dbName = "test.db"

tableDefinition = `CREATE TABLE contacts (
contact_id INTEGER PRIMARY KEY,
Expand Down Expand Up @@ -72,20 +71,20 @@ func NewServer() *Server {
}
}

func (s *Server) queryDb(w http.ResponseWriter, req *http.Request) {
func (s *Server) query(w http.ResponseWriter, req *http.Request, query string) {
ctx := req.Context()

conn, err := s.db.Conn(ctx)
if err != nil {
panic(err)
}

rows, err := conn.QueryContext(req.Context(), sqlQuery)
rows, err := conn.QueryContext(req.Context(), query)
if err != nil {
panic(err)
logger.Error(err.Error())
return
}

logger.Info("queryDb called")
logger.Info("queryDB called", zap.String("query", query))
for rows.Next() {
var id int
var firstName string
Expand All @@ -102,6 +101,30 @@ func (s *Server) queryDb(w http.ResponseWriter, req *http.Request) {

var logger *zap.Logger

func (s *Server) selectDb(w http.ResponseWriter, req *http.Request) {
s.query(w, req, "SELECT * FROM contacts")
}

func (s *Server) insert(w http.ResponseWriter, req *http.Request) {
s.query(w, req, "INSERT INTO contacts (first_name) VALUES ('Mike')")
}

func (s *Server) update(w http.ResponseWriter, req *http.Request) {
s.query(w, req, "UPDATE contacts SET last_name = 'Santa' WHERE first_name = 'Mike'")
}

func (s *Server) delete(w http.ResponseWriter, req *http.Request) {
s.query(w, req, "DELETE FROM contacts WHERE first_name = 'Mike'")
}

func (s *Server) drop(w http.ResponseWriter, req *http.Request) {
s.query(w, req, "DROP TABLE contacts")
}

func (s *Server) invalid(w http.ResponseWriter, req *http.Request) {
s.query(w, req, "syntax error")
}

func main() {
var err error
logger, err = zap.NewDevelopment()
Expand All @@ -114,26 +137,45 @@ func main() {

s := NewServer()

http.HandleFunc("/query_db", s.queryDb)
http.HandleFunc("/query_db", s.selectDb)
http.HandleFunc("/insert", s.insert)
http.HandleFunc("/update", s.update)
http.HandleFunc("/delete", s.delete)
http.HandleFunc("/drop", s.drop)
http.HandleFunc("/invalid", s.invalid)
go func() {
_ = http.ListenAndServe(":8080", nil)
}()

tests := []struct {
url string
}{
{url: "http://localhost:8080/query_db"},
{url: "http://localhost:8080/insert"},
{url: "http://localhost:8080/update"},
{url: "http://localhost:8080/delete"},
{url: "http://localhost:8080/drop"},
{url: "http://localhost:8080/invalid"},
}

// give time for auto-instrumentation to start up
time.Sleep(5 * time.Second)

resp, err := http.Get("http://localhost:8080/query_db")
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))
for _, t := range tests {
resp, err := http.Get(t.url)
if err != nil {
logger.Error("Error performing GET", zap.Error(err))
}
if resp != nil {
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()
}
}

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)
}
Loading
Loading