diff --git a/.github/workflows/e2e/k8s/sample-job.yml b/.github/workflows/e2e/k8s/sample-job.yml index 6414ef806..0dc4aa8ec 100644 --- a/.github/workflows/e2e/k8s/sample-job.yml +++ b/.github/workflows/e2e/k8s/sample-job.yml @@ -35,6 +35,8 @@ spec: value: "sample-app" - name: OTEL_PROPAGATORS value: "tracecontext,baggage" + - name: OTEL_GO_AUTO_INCLUDE_DB_STATEMENT + value: "true" resources: {} securityContext: runAsUser: 0 diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml index 4230faa68..710d99238 100644 --- a/.github/workflows/kind.yml +++ b/.github/workflows/kind.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: k8s-version: ["v1.26.0"] - library: ["gorillamux", "nethttp", "gin"] + library: ["gorillamux", "nethttp", "gin", "databasesql"] runs-on: ubuntu-latest steps: - name: Checkout Repo diff --git a/.gitignore b/.gitignore index 5d1ef747b..b64131b13 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ launcher/ opentelemetry-helm-charts/ # don't track temp e2e trace json files -test/**/traces-orig.json \ No newline at end of file +test/**/traces-orig.json + +# ignore db files created by the example or tests +*.db \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5afc51771..7e145d60c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http ## [Unreleased] +### Added +- Add database/sql instrumentation ([#240](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/240)) + ### Changed - The function signature of `"go.opentelemetry.io/auto/offsets-tracker/downloader".DownloadBinary` has changed. diff --git a/Makefile b/Makefile index ae0469c79..496169251 100644 --- a/Makefile +++ b/Makefile @@ -118,10 +118,11 @@ license-header-check: exit 1; \ fi -.PHONY: fixture-nethttp fixture-gorillamux fixture-gin +.PHONY: fixture-nethttp fixture-gorillamux fixture-gin fixture-databasesql fixture-nethttp: fixtures/nethttp fixture-gorillamux: fixtures/gorillamux fixture-gin: fixtures/gin +fixture-databasesql: fixtures/databasesql fixtures/%: LIBRARY=$* fixtures/%: $(MAKE) docker-build diff --git a/examples/httpPlusdb/Dockerfile b/examples/httpPlusdb/Dockerfile new file mode 100644 index 000000000..cd20e5722 --- /dev/null +++ b/examples/httpPlusdb/Dockerfile @@ -0,0 +1,5 @@ +FROM golang:1.20 +WORKDIR /app +COPY . . +RUN go build -o main +ENTRYPOINT ["/app/main"] diff --git a/examples/httpPlusdb/README.md b/examples/httpPlusdb/README.md new file mode 100644 index 000000000..70f672722 --- /dev/null +++ b/examples/httpPlusdb/README.md @@ -0,0 +1,19 @@ +# Example of Auto instrumentation of HTTP server + SQL database + +This example shows a trace being generated which is composed of a http request and sql db handling - +both visible in the trace. + +For testing auto instrumentation, we can use the docker compose. + +To run the example, bring up the services using the command. + +``` +docker compose up +``` + +Now, you can hit the server using the below command +``` +curl localhost:8080/query_db +Which will query the dummy sqlite database named test.db +``` +Every hit to the server should generate a trace that we can observe in [Jaeger UI](http://localhost:16686/) diff --git a/examples/httpPlusdb/docker-compose.yaml b/examples/httpPlusdb/docker-compose.yaml new file mode 100644 index 000000000..f764897ed --- /dev/null +++ b/examples/httpPlusdb/docker-compose.yaml @@ -0,0 +1,56 @@ +version: "3.9" + +networks: + default: + name: http-db + driver: bridge + +services: + http-plus-db: + depends_on: + - jaeger + build: + context: . + dockerfile: ./Dockerfile + pid: "host" + ports: + - "8080:8080" + volumes: + - shared-data:/app + - /proc:/host/proc + go-auto: + depends_on: + - http-plus-db + build: + context: ../.. + dockerfile: Dockerfile + privileged: true + pid: "host" + environment: + - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317 + - OTEL_GO_AUTO_TARGET_EXE=/app/main + - OTEL_GO_AUTO_INCLUDE_DB_STATEMENT=true + - OTEL_SERVICE_NAME=httpPlusdb + - OTEL_PROPAGATORS=tracecontext,baggage + - CGO_ENABLED=1 + volumes: + - shared-data:/app + - /proc:/host/proc + + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268:14268" + environment: + - COLLECTOR_OTLP_ENABLED=true + - LOG_LEVEL=debug + deploy: + resources: + limits: + memory: 300M + restart: unless-stopped + + +volumes: + shared-data: diff --git a/examples/httpPlusdb/go.mod b/examples/httpPlusdb/go.mod new file mode 100644 index 000000000..8d9a50f11 --- /dev/null +++ b/examples/httpPlusdb/go.mod @@ -0,0 +1,12 @@ +module go.opentelemetry.io/auto/examples/httpPlusdb + +go 1.20 + +require go.uber.org/zap v1.24.0 + +require ( + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect +) + +require github.com/mattn/go-sqlite3 v1.14.17 diff --git a/examples/httpPlusdb/go.sum b/examples/httpPlusdb/go.sum new file mode 100644 index 000000000..94e89795c --- /dev/null +++ b/examples/httpPlusdb/go.sum @@ -0,0 +1,20 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +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/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/examples/httpPlusdb/http_Db_traces.jpg b/examples/httpPlusdb/http_Db_traces.jpg new file mode 100644 index 000000000..f346e25e3 Binary files /dev/null and b/examples/httpPlusdb/http_Db_traces.jpg differ diff --git a/examples/httpPlusdb/main.go b/examples/httpPlusdb/main.go new file mode 100644 index 000000000..7839f6229 --- /dev/null +++ b/examples/httpPlusdb/main.go @@ -0,0 +1,136 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "database/sql" + "fmt" + "net/http" + "os" + + _ "github.com/mattn/go-sqlite3" + "go.uber.org/zap" +) + +const sqlQuery = "SELECT * FROM contacts" +const dbName = "test.db" +const tableDefinition = `CREATE TABLE contacts ( + contact_id INTEGER PRIMARY KEY, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + phone TEXT NOT NULL UNIQUE);` +const tableInsertion = `INSERT INTO 'contacts' + ('first_name', 'last_name', 'email', 'phone') VALUES + ('Moshe', 'Levi', 'moshe@gmail.com', '052-1234567');` + +// Server is Http server that exposes multiple endpoints. +type Server struct { + db *sql.DB +} + +// Create the db file. +func CreateDb() { + file, err := os.Create(dbName) + if err != nil { + panic(err) + } + err = file.Close() + if err != nil { + panic(err) + } +} + +// NewServer creates a server struct after creating the DB and initializing it +// and creating a table named 'contacts' and adding a single row to it. +func NewServer() *Server { + CreateDb() + + database, err := sql.Open("sqlite3", dbName) + + if err != nil { + panic(err) + } + + _, err = database.Exec(tableDefinition) + + if err != nil { + panic(err) + } + + _, err = database.Exec(tableInsertion) + + if err != nil { + panic(err) + } + + return &Server{ + db: database, + } +} + +func (s *Server) queryDb(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(), sqlQuery) + 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 setupHandler(s *Server) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/query_db", s.queryDb) + return mux +} + +func main() { + var err error + logger, err = zap.NewDevelopment() + if err != nil { + fmt.Printf("error creating zap logger, error:%v", err) + return + } + port := fmt.Sprintf(":%d", 8080) + logger.Info("starting http server", zap.String("port", port)) + + s := NewServer() + mux := setupHandler(s) + if err := http.ListenAndServe(port, mux); err != nil { + logger.Error("error running server", zap.Error(err)) + } +} diff --git a/pkg/inject/injector.go b/pkg/inject/injector.go index c7296d69a..c9d263bd4 100644 --- a/pkg/inject/injector.go +++ b/pkg/inject/injector.go @@ -67,8 +67,14 @@ type StructField struct { Field string } +// FlagField is used for configuring the ebpf programs by injecting boolean values. +type FlagField struct { + VarName string + Value bool +} + // Inject injects instrumentation for the provided library data type. -func (i *Injector) Inject(loadBpf loadBpfFunc, library string, libVersion string, fields []*StructField, initAlloc bool) (*ebpf.CollectionSpec, error) { +func (i *Injector) Inject(loadBpf loadBpfFunc, library string, libVersion string, fields []*StructField, flagFields []*FlagField, initAlloc bool) (*ebpf.CollectionSpec, error) { spec, err := loadBpf() if err != nil { return nil, err @@ -88,6 +94,11 @@ func (i *Injector) Inject(loadBpf loadBpfFunc, library string, libVersion string if err := i.addCommonInjections(injectedVars, initAlloc); err != nil { return nil, fmt.Errorf("adding instrumenter injections: %w", err) } + + if err := i.addConfigInjections(injectedVars, flagFields); err != nil { + return nil, fmt.Errorf("adding flags injections: %w", err) + } + log.Logger.V(0).Info("Injecting variables", "vars", injectedVars) if len(injectedVars) > 0 { err = spec.RewriteConstants(injectedVars) @@ -112,6 +123,13 @@ func (i *Injector) addCommonInjections(varsMap map[string]interface{}, initAlloc return nil } +func (i *Injector) addConfigInjections(varsMap map[string]interface{}, flagFields []*FlagField) error { + for _, dm := range flagFields { + varsMap[dm.VarName] = dm.Value + } + return nil +} + func (i *Injector) getFieldOffset(structName string, fieldName string, libVersion string) (uint64, bool) { strct, ok := i.data.Data[structName] if !ok { diff --git a/pkg/instrumentors/bpf/database/sql/bpf/probe.bpf.c b/pkg/instrumentors/bpf/database/sql/bpf/probe.bpf.c new file mode 100644 index 000000000..bff811f20 --- /dev/null +++ b/pkg/instrumentors/bpf/database/sql/bpf/probe.bpf.c @@ -0,0 +1,89 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "arguments.h" +#include "span_context.h" +#include "go_context.h" +#include "go_types.h" +#include "uprobe.h" + +char __license[] SEC("license") = "Dual MIT/GPL"; + +#define MAX_QUERY_SIZE 100 +#define MAX_CONCURRENT 50 + +struct sql_request_t { + BASE_SPAN_PROPERTIES + char query[MAX_QUERY_SIZE]; +}; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, void*); + __type(value, struct sql_request_t); + __uint(max_entries, MAX_CONCURRENT); +} sql_events SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); +} events SEC(".maps"); + +// Injected in init +volatile const bool should_include_db_statement; + +// This instrumentation attaches uprobe to the following function: +// func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []any) +SEC("uprobe/queryDC") +int uprobe_queryDC(struct pt_regs *ctx) { + // argument positions + u64 context_ptr_pos = 3; + u64 query_str_ptr_pos = 8; + u64 query_str_len_pos = 9; + + struct sql_request_t sql_request = {0}; + sql_request.start_time = bpf_ktime_get_ns(); + + if (should_include_db_statement) { + // Read Query string + void *query_str_ptr = get_argument(ctx, query_str_ptr_pos); + u64 query_str_len = (u64)get_argument(ctx, query_str_len_pos); + u64 query_size = MAX_QUERY_SIZE < query_str_len ? MAX_QUERY_SIZE : query_str_len; + bpf_probe_read(sql_request.query, query_size, query_str_ptr); + } + + // Get parent if exists + void *context_ptr = get_argument(ctx, context_ptr_pos); + void *context_ptr_val = 0; + bpf_probe_read(&context_ptr_val, sizeof(context_ptr_val), context_ptr); + struct span_context *span_ctx = get_parent_span_context(context_ptr_val); + if (span_ctx != NULL) { + // Set the parent context + bpf_probe_read(&sql_request.psc, sizeof(sql_request.psc), span_ctx); + copy_byte_arrays(sql_request.psc.TraceID, sql_request.sc.TraceID, TRACE_ID_SIZE); + generate_random_bytes(sql_request.sc.SpanID, SPAN_ID_SIZE); + } else { + sql_request.sc = generate_span_context(); + } + + // Get key + void *key = get_consistent_key(ctx, context_ptr); + + bpf_map_update_elem(&sql_events, &key, &sql_request, 0); + start_tracking_span(context_ptr_val, &sql_request.sc); + return 0; +} + +// This instrumentation attaches uprobe to the following function: +// func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []any) +UPROBE_RETURN(queryDC, struct sql_request_t, 3, 0, sql_events, events) \ No newline at end of file diff --git a/pkg/instrumentors/bpf/database/sql/bpf_bpfel_arm64.go b/pkg/instrumentors/bpf/database/sql/bpf_bpfel_arm64.go new file mode 100644 index 000000000..18fa1aa93 --- /dev/null +++ b/pkg/instrumentors/bpf/database/sql/bpf_bpfel_arm64.go @@ -0,0 +1,148 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build arm64 + +package sql + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type bpfSpanContext struct { + TraceID [16]uint8 + SpanID [8]uint8 +} + +type bpfSqlRequestT struct { + StartTime uint64 + EndTime uint64 + Sc bpfSpanContext + Psc bpfSpanContext + Query [100]int8 + _ [4]byte +} + +// loadBpf returns the embedded CollectionSpec for bpf. +func loadBpf() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_BpfBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load bpf: %w", err) + } + + return spec, err +} + +// loadBpfObjects loads bpf and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *bpfObjects +// *bpfPrograms +// *bpfMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadBpf() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// bpfSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfSpecs struct { + bpfProgramSpecs + bpfMapSpecs +} + +// bpfSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfProgramSpecs struct { + UprobeQueryDC *ebpf.ProgramSpec `ebpf:"uprobe_queryDC"` + UprobeQueryDC_Returns *ebpf.ProgramSpec `ebpf:"uprobe_queryDC_Returns"` +} + +// bpfMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfMapSpecs struct { + AllocMap *ebpf.MapSpec `ebpf:"alloc_map"` + Events *ebpf.MapSpec `ebpf:"events"` + SqlEvents *ebpf.MapSpec `ebpf:"sql_events"` + TrackedSpans *ebpf.MapSpec `ebpf:"tracked_spans"` + TrackedSpansBySc *ebpf.MapSpec `ebpf:"tracked_spans_by_sc"` +} + +// bpfObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfObjects struct { + bpfPrograms + bpfMaps +} + +func (o *bpfObjects) Close() error { + return _BpfClose( + &o.bpfPrograms, + &o.bpfMaps, + ) +} + +// bpfMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfMaps struct { + AllocMap *ebpf.Map `ebpf:"alloc_map"` + Events *ebpf.Map `ebpf:"events"` + SqlEvents *ebpf.Map `ebpf:"sql_events"` + TrackedSpans *ebpf.Map `ebpf:"tracked_spans"` + TrackedSpansBySc *ebpf.Map `ebpf:"tracked_spans_by_sc"` +} + +func (m *bpfMaps) Close() error { + return _BpfClose( + m.AllocMap, + m.Events, + m.SqlEvents, + m.TrackedSpans, + m.TrackedSpansBySc, + ) +} + +// bpfPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfPrograms struct { + UprobeQueryDC *ebpf.Program `ebpf:"uprobe_queryDC"` + UprobeQueryDC_Returns *ebpf.Program `ebpf:"uprobe_queryDC_Returns"` +} + +func (p *bpfPrograms) Close() error { + return _BpfClose( + p.UprobeQueryDC, + p.UprobeQueryDC_Returns, + ) +} + +func _BpfClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed bpf_bpfel_arm64.o +var _BpfBytes []byte diff --git a/pkg/instrumentors/bpf/database/sql/bpf_bpfel_x86.go b/pkg/instrumentors/bpf/database/sql/bpf_bpfel_x86.go new file mode 100644 index 000000000..86185db34 --- /dev/null +++ b/pkg/instrumentors/bpf/database/sql/bpf_bpfel_x86.go @@ -0,0 +1,148 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 + +package sql + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type bpfSpanContext struct { + TraceID [16]uint8 + SpanID [8]uint8 +} + +type bpfSqlRequestT struct { + StartTime uint64 + EndTime uint64 + Sc bpfSpanContext + Psc bpfSpanContext + Query [100]int8 + _ [4]byte +} + +// loadBpf returns the embedded CollectionSpec for bpf. +func loadBpf() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_BpfBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load bpf: %w", err) + } + + return spec, err +} + +// loadBpfObjects loads bpf and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *bpfObjects +// *bpfPrograms +// *bpfMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadBpf() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// bpfSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfSpecs struct { + bpfProgramSpecs + bpfMapSpecs +} + +// bpfSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfProgramSpecs struct { + UprobeQueryDC *ebpf.ProgramSpec `ebpf:"uprobe_queryDC"` + UprobeQueryDC_Returns *ebpf.ProgramSpec `ebpf:"uprobe_queryDC_Returns"` +} + +// bpfMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfMapSpecs struct { + AllocMap *ebpf.MapSpec `ebpf:"alloc_map"` + Events *ebpf.MapSpec `ebpf:"events"` + SqlEvents *ebpf.MapSpec `ebpf:"sql_events"` + TrackedSpans *ebpf.MapSpec `ebpf:"tracked_spans"` + TrackedSpansBySc *ebpf.MapSpec `ebpf:"tracked_spans_by_sc"` +} + +// bpfObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfObjects struct { + bpfPrograms + bpfMaps +} + +func (o *bpfObjects) Close() error { + return _BpfClose( + &o.bpfPrograms, + &o.bpfMaps, + ) +} + +// bpfMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfMaps struct { + AllocMap *ebpf.Map `ebpf:"alloc_map"` + Events *ebpf.Map `ebpf:"events"` + SqlEvents *ebpf.Map `ebpf:"sql_events"` + TrackedSpans *ebpf.Map `ebpf:"tracked_spans"` + TrackedSpansBySc *ebpf.Map `ebpf:"tracked_spans_by_sc"` +} + +func (m *bpfMaps) Close() error { + return _BpfClose( + m.AllocMap, + m.Events, + m.SqlEvents, + m.TrackedSpans, + m.TrackedSpansBySc, + ) +} + +// bpfPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfPrograms struct { + UprobeQueryDC *ebpf.Program `ebpf:"uprobe_queryDC"` + UprobeQueryDC_Returns *ebpf.Program `ebpf:"uprobe_queryDC_Returns"` +} + +func (p *bpfPrograms) Close() error { + return _BpfClose( + p.UprobeQueryDC, + p.UprobeQueryDC_Returns, + ) +} + +func _BpfClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed bpf_bpfel_x86.o +var _BpfBytes []byte diff --git a/pkg/instrumentors/bpf/database/sql/probe.go b/pkg/instrumentors/bpf/database/sql/probe.go new file mode 100644 index 000000000..486b51112 --- /dev/null +++ b/pkg/instrumentors/bpf/database/sql/probe.go @@ -0,0 +1,238 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sql + +import ( + "bytes" + "encoding/binary" + "errors" + "os" + "strconv" + + "go.opentelemetry.io/auto/pkg/inject" + "go.opentelemetry.io/auto/pkg/instrumentors/bpffs" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + "github.com/cilium/ebpf/perf" + "golang.org/x/sys/unix" + + "go.opentelemetry.io/auto/pkg/instrumentors/context" + "go.opentelemetry.io/auto/pkg/instrumentors/events" + "go.opentelemetry.io/auto/pkg/instrumentors/utils" + "go.opentelemetry.io/auto/pkg/log" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.18.0" + "go.opentelemetry.io/otel/trace" +) + +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64,arm64 -cc clang -cflags $CFLAGS bpf ./bpf/probe.bpf.c + +const instrumentedPkg = "database/sql" + +// Event represents an event in an SQL database +// request-response. +type Event struct { + context.BaseSpanProperties + Query [100]byte +} + +// Instrumentor is the database/sql instrumentor. +type Instrumentor struct { + bpfObjects *bpfObjects + uprobes []link.Link + returnProbs []link.Link + eventsReader *perf.Reader +} + +// IncludeDBStatementEnvVar is the environment variable to opt-in for sql query inclusion in the trace. +const IncludeDBStatementEnvVar = "OTEL_GO_AUTO_INCLUDE_DB_STATEMENT" + +// New returns a new [Instrumentor]. +func New() *Instrumentor { + return &Instrumentor{} +} + +// LibraryName returns the database/sql/ package name. +func (h *Instrumentor) LibraryName() string { + return instrumentedPkg +} + +// FuncNames returns the function names from "database/sql" that are instrumented. +func (h *Instrumentor) FuncNames() []string { + return []string{"database/sql.(*DB).queryDC"} +} + +// Load loads all instrumentation offsets. +func (h *Instrumentor) Load(ctx *context.InstrumentorContext) error { + spec, err := ctx.Injector.Inject(loadBpf, "go", ctx.TargetDetails.GoVersion.Original(), nil, []*inject.FlagField{ + { + VarName: "should_include_db_statement", + Value: shouldIncludeDBStatement(), + }}, true) + + if err != nil { + return err + } + + h.bpfObjects = &bpfObjects{} + + err = utils.LoadEBPFObjects(spec, h.bpfObjects, &ebpf.CollectionOptions{ + Maps: ebpf.MapOptions{ + PinPath: bpffs.PathForTargetApplication(ctx.TargetDetails), + }, + }) + + if err != nil { + return err + } + + offset, err := ctx.TargetDetails.GetFunctionOffset(h.FuncNames()[0]) + + if err != nil { + return err + } + + up, err := ctx.Executable.Uprobe("", h.bpfObjects.UprobeQueryDC, &link.UprobeOptions{ + Address: offset, + }) + + if err != nil { + return err + } + + h.uprobes = append(h.uprobes, up) + + retOffsets, err := ctx.TargetDetails.GetFunctionReturns(h.FuncNames()[0]) + + if err != nil { + return err + } + + for _, ret := range retOffsets { + retProbe, err := ctx.Executable.Uprobe("", h.bpfObjects.UprobeQueryDC_Returns, &link.UprobeOptions{ + Address: ret, + }) + if err != nil { + return err + } + h.returnProbs = append(h.returnProbs, retProbe) + } + + rd, err := perf.NewReader(h.bpfObjects.Events, os.Getpagesize()) + if err != nil { + return err + } + h.eventsReader = rd + + return nil +} + +// Run runs the events processing loop. +func (h *Instrumentor) Run(eventsChan chan<- *events.Event) { + logger := log.Logger.WithName("database/sql/sql-instrumentor") + var event Event + for { + record, err := h.eventsReader.Read() + if err != nil { + if errors.Is(err, perf.ErrClosed) { + return + } + logger.Error(err, "error reading from perf reader") + continue + } + + if record.LostSamples != 0 { + logger.V(0).Info("perf event ring buffer full", "dropped", record.LostSamples) + continue + } + + if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil { + logger.Error(err, "error parsing perf event") + continue + } + + eventsChan <- h.convertEvent(&event) + } +} + +func (h *Instrumentor) convertEvent(e *Event) *events.Event { + query := unix.ByteSliceToString(e.Query[:]) + + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: e.SpanContext.TraceID, + SpanID: e.SpanContext.SpanID, + TraceFlags: trace.FlagsSampled, + }) + + var pscPtr *trace.SpanContext + if e.ParentSpanContext.TraceID.IsValid() { + psc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: e.ParentSpanContext.TraceID, + SpanID: e.ParentSpanContext.SpanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }) + pscPtr = &psc + } else { + pscPtr = nil + } + + return &events.Event{ + Library: h.LibraryName(), + Name: "DB", + Kind: trace.SpanKindClient, + StartTime: int64(e.StartTime), + EndTime: int64(e.EndTime), + SpanContext: &sc, + Attributes: []attribute.KeyValue{ + semconv.DBStatementKey.String(query), + }, + ParentSpanContext: pscPtr, + } +} + +// Close stops the Instrumentor. +func (h *Instrumentor) Close() { + log.Logger.V(0).Info("closing database/sql/sql instrumentor") + if h.eventsReader != nil { + h.eventsReader.Close() + } + + for _, r := range h.uprobes { + r.Close() + } + + for _, r := range h.returnProbs { + r.Close() + } + + if h.bpfObjects != nil { + h.bpfObjects.Close() + } +} + +// shouldIncludeDBStatement returns if the user has configured SQL queries to be included. +func shouldIncludeDBStatement() bool { + val := os.Getenv(IncludeDBStatementEnvVar) + if val != "" { + boolVal, err := strconv.ParseBool(val) + if err == nil { + return boolVal + } + } + + return false +} diff --git a/pkg/instrumentors/bpf/database/sql/probe_test.go b/pkg/instrumentors/bpf/database/sql/probe_test.go new file mode 100644 index 000000000..d7826c395 --- /dev/null +++ b/pkg/instrumentors/bpf/database/sql/probe_test.go @@ -0,0 +1,65 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sql + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/auto/pkg/instrumentors/context" + "go.opentelemetry.io/auto/pkg/instrumentors/events" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.7.0" + "go.opentelemetry.io/otel/trace" +) + +func TestInstrumentorConvertEvent(t *testing.T) { + start := time.Now() + end := start.Add(1 * time.Second) + + traceID := trace.TraceID{1} + spanID := trace.SpanID{1} + + i := New() + got := i.convertEvent(&Event{ + BaseSpanProperties: context.BaseSpanProperties{ + StartTime: uint64(start.UnixNano()), + EndTime: uint64(end.UnixNano()), + SpanContext: context.EBPFSpanContext{TraceID: traceID, SpanID: spanID}, + }, + // "SELECT * FROM foo" + Query: [100]byte{0x53, 0x45, 0x4c, 0x45, 0x43, 0x54, 0x20, 0x2a, 0x20, 0x46, 0x52, 0x4f, 0x4d, 0x20, 0x66, 0x6f, 0x6f}, + }) + + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }) + want := &events.Event{ + Library: instrumentedPkg, + Name: "DB", + Kind: trace.SpanKindClient, + StartTime: int64(start.UnixNano()), + EndTime: int64(end.UnixNano()), + SpanContext: &sc, + Attributes: []attribute.KeyValue{ + semconv.DBStatementKey.String("SELECT * FROM foo"), + }, + } + assert.Equal(t, want, got) +} diff --git a/pkg/instrumentors/bpf/github.com/gin-gonic/gin/probe.go b/pkg/instrumentors/bpf/github.com/gin-gonic/gin/probe.go index 3f2176702..27d35ae1b 100644 --- a/pkg/instrumentors/bpf/github.com/gin-gonic/gin/probe.go +++ b/pkg/instrumentors/bpf/github.com/gin-gonic/gin/probe.go @@ -97,12 +97,7 @@ func (h *Instrumentor) Load(ctx *context.InstrumentorContext) error { StructName: "net/url.URL", Field: "Path", }, - { - VarName: "ctx_ptr_pos", - StructName: "net/http.Request", - Field: "ctx", - }, - }, false) + }, nil, false) if err != nil { return err diff --git a/pkg/instrumentors/bpf/github.com/gorilla/mux/probe.go b/pkg/instrumentors/bpf/github.com/gorilla/mux/probe.go index 9b14a38c5..5617eb8f1 100644 --- a/pkg/instrumentors/bpf/github.com/gorilla/mux/probe.go +++ b/pkg/instrumentors/bpf/github.com/gorilla/mux/probe.go @@ -96,7 +96,7 @@ func (g *Instrumentor) Load(ctx *context.InstrumentorContext) error { StructName: "net/url.URL", Field: "Path", }, - }, false) + }, nil, false) if err != nil { return err diff --git a/pkg/instrumentors/bpf/google/golang/org/grpc/probe.go b/pkg/instrumentors/bpf/google/golang/org/grpc/probe.go index 3e2877cac..45acae8ab 100644 --- a/pkg/instrumentors/bpf/google/golang/org/grpc/probe.go +++ b/pkg/instrumentors/bpf/google/golang/org/grpc/probe.go @@ -100,7 +100,7 @@ func (g *Instrumentor) Load(ctx *context.InstrumentorContext) error { StructName: "google.golang.org/grpc/internal/transport.headerFrame", Field: "streamID", }, - }, true) + }, nil, true) if err != nil { return err diff --git a/pkg/instrumentors/bpf/google/golang/org/grpc/server/probe.go b/pkg/instrumentors/bpf/google/golang/org/grpc/server/probe.go index 7d4096e3b..89f1c3024 100644 --- a/pkg/instrumentors/bpf/google/golang/org/grpc/server/probe.go +++ b/pkg/instrumentors/bpf/google/golang/org/grpc/server/probe.go @@ -104,7 +104,7 @@ func (g *Instrumentor) Load(ctx *context.InstrumentorContext) error { StructName: "golang.org/x/net/http2.FrameHeader", Field: "StreamID", }, - }, true) + }, nil, true) if err != nil { return err diff --git a/pkg/instrumentors/bpf/net/http/client/probe.go b/pkg/instrumentors/bpf/net/http/client/probe.go index b13dd6355..6aa3786c5 100644 --- a/pkg/instrumentors/bpf/net/http/client/probe.go +++ b/pkg/instrumentors/bpf/net/http/client/probe.go @@ -97,7 +97,7 @@ func (h *Instrumentor) Load(ctx *context.InstrumentorContext) error { StructName: "net/http.Request", Field: "ctx", }, - }, true) + }, nil, true) if err != nil { return err diff --git a/pkg/instrumentors/bpf/net/http/server/probe.go b/pkg/instrumentors/bpf/net/http/server/probe.go index 23f336353..07c2a38e2 100644 --- a/pkg/instrumentors/bpf/net/http/server/probe.go +++ b/pkg/instrumentors/bpf/net/http/server/probe.go @@ -105,7 +105,7 @@ func (h *Instrumentor) Load(ctx *context.InstrumentorContext) error { StructName: "net/http.Request", Field: "Header", }, - }, false) + }, nil, false) if err != nil { return err diff --git a/pkg/instrumentors/manager.go b/pkg/instrumentors/manager.go index d96148140..3f7c56ff2 100644 --- a/pkg/instrumentors/manager.go +++ b/pkg/instrumentors/manager.go @@ -18,6 +18,7 @@ import ( "fmt" "go.opentelemetry.io/auto/pkg/instrumentors/allocator" + dbSql "go.opentelemetry.io/auto/pkg/instrumentors/bpf/database/sql" "go.opentelemetry.io/auto/pkg/instrumentors/bpf/github.com/gin-gonic/gin" gorillaMux "go.opentelemetry.io/auto/pkg/instrumentors/bpf/github.com/gorilla/mux" "go.opentelemetry.io/auto/pkg/instrumentors/bpf/google/golang/org/grpc" @@ -117,6 +118,7 @@ func registerInstrumentors(m *Manager) error { httpClient.New(), gorillaMux.New(), gin.New(), + dbSql.New(), } for _, i := range insts { diff --git a/test/e2e/databasesql/Dockerfile b/test/e2e/databasesql/Dockerfile new file mode 100644 index 000000000..646ec7a43 --- /dev/null +++ b/test/e2e/databasesql/Dockerfile @@ -0,0 +1,4 @@ +FROM golang:1.20 +WORKDIR /sample-app +COPY . . +RUN go build -o main diff --git a/test/e2e/databasesql/go.mod b/test/e2e/databasesql/go.mod new file mode 100644 index 000000000..a95c57552 --- /dev/null +++ b/test/e2e/databasesql/go.mod @@ -0,0 +1,13 @@ +module main + +go 1.20 + +require ( + github.com/mattn/go-sqlite3 v1.14.17 + go.uber.org/zap v1.24.0 +) + +require ( + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect +) diff --git a/test/e2e/databasesql/go.sum b/test/e2e/databasesql/go.sum new file mode 100644 index 000000000..94e89795c --- /dev/null +++ b/test/e2e/databasesql/go.sum @@ -0,0 +1,20 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +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/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/test/e2e/databasesql/main.go b/test/e2e/databasesql/main.go new file mode 100644 index 000000000..b119c2ec2 --- /dev/null +++ b/test/e2e/databasesql/main.go @@ -0,0 +1,150 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "database/sql" + "fmt" + "io" + "net/http" + "os" + "time" + + _ "github.com/mattn/go-sqlite3" + "go.uber.org/zap" +) + +const sqlQuery = "SELECT * FROM contacts" +const dbName = "test.db" +const tableDefinition = `CREATE TABLE contacts ( + contact_id INTEGER PRIMARY KEY, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + phone TEXT NOT NULL UNIQUE);` +const tableInsertion = `INSERT INTO 'contacts' + ('first_name', 'last_name', 'email', 'phone') VALUES + ('Moshe', 'Levi', 'moshe@gmail.com', '052-1234567');` + +// Server is Http server that exposes multiple endpoints. +type Server struct { + db *sql.DB +} + +// Create the db file. +func CreateDb() { + file, err := os.Create(dbName) + if err != nil { + panic(err) + } + err = file.Close() + if err != nil { + panic(err) + } +} + +// NewServer creates a server struct after initialing rand. +func NewServer() *Server { + CreateDb() + + database, err := sql.Open("sqlite3", dbName) + + if err != nil { + panic(err) + } + + _, err = database.Exec(tableDefinition) + + if err != nil { + panic(err) + } + + _, err = database.Exec(tableInsertion) + + if err != nil { + panic(err) + } + + return &Server{ + db: database, + } +} + +func (s *Server) queryDb(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(), sqlQuery) + 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() { + var err error + logger, err = zap.NewDevelopment() + if err != nil { + fmt.Printf("error creating zap logger, error:%v", err) + return + } + port := fmt.Sprintf(":%d", 8080) + logger.Info("starting http server", zap.String("port", port)) + + s := NewServer() + + http.HandleFunc("/query_db", s.queryDb) + go func() { + _ = http.ListenAndServe(":8080", nil) + }() + + // 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)) + } + + 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/test/e2e/databasesql/traces.json b/test/e2e/databasesql/traces.json new file mode 100644 index 000000000..2d26c3170 --- /dev/null +++ b/test/e2e/databasesql/traces.json @@ -0,0 +1,111 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "sample-app" + } + }, + { + "key": "telemetry.auto.version", + "value": { + "stringValue": "v0.2.2-alpha" + } + }, + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "go" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "database/sql" + }, + "spans": [ + { + "attributes": [ + { + "key": "db.statement", + "value": { + "stringValue": "SELECT * FROM contacts" + } + } + ], + "kind": 3, + "name": "DB", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + } + ] + }, + { + "scope": { + "name": "net/http" + }, + "spans": [ + { + "attributes": [ + { + "key": "http.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "http.target", + "value": { + "stringValue": "/query_db" + } + } + ], + "kind": 2, + "name": "GET", + "parentSpanId": "xxxxx", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + } + ] + }, + { + "scope": { + "name": "net/http/client" + }, + "spans": [ + { + "attributes": [ + { + "key": "http.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "http.target", + "value": { + "stringValue": "/query_db" + } + } + ], + "kind": 3, + "name": "/query_db", + "parentSpanId": "", + "spanId": "xxxxx", + "status": {}, + "traceId": "xxxxx" + } + ] + } + ] + } + ] +} diff --git a/test/e2e/databasesql/verify.bats b/test/e2e/databasesql/verify.bats new file mode 100644 index 000000000..306d66b76 --- /dev/null +++ b/test/e2e/databasesql/verify.bats @@ -0,0 +1,30 @@ +#!/usr/bin/env bats + +load ../../test_helpers/utilities + +LIBRARY_NAME="database/sql" + +@test "${LIBRARY_NAME} :: includes db.statement attribute" { + result=$(span_attributes_for ${LIBRARY_NAME} | jq "select(.key == \"db.statement\").value.stringValue") + assert_equal "$result" '"SELECT * FROM contacts"' +} + +@test "${LIBRARY_NAME} :: trace ID present and valid in all spans" { + trace_id=$(spans_from_scope_named ${LIBRARY_NAME} | jq ".traceId") + assert_regex "$trace_id" ${MATCH_A_TRACE_ID} +} + +@test "${LIBRARY_NAME} :: span ID present and valid in all spans" { + span_id=$(spans_from_scope_named ${LIBRARY_NAME} | jq ".spanId") + assert_regex "$span_id" ${MATCH_A_SPAN_ID} +} + +@test "${LIBRARY_NAME} :: parent span ID present and valid in all spans" { + parent_span_id=$(spans_from_scope_named ${LIBRARY_NAME} | jq ".parentSpanId") + assert_regex "$parent_span_id" ${MATCH_A_SPAN_ID} +} + +@test "${LIBRARY_NAME} :: expected (redacted) trace output" { + redact_json + assert_equal "$(git --no-pager diff ${BATS_TEST_DIRNAME}/traces.json)" "" +}