From 47a32e402587b0a8561af77a00c10a5509cfac18 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski <6207777+grcevski@users.noreply.github.com> Date: Wed, 24 Apr 2024 14:28:51 -0400 Subject: [PATCH] Switch HTTP client to not use external memory for header injection (#705) * WIP: Switch writing HTTP Traceparent without external memory * Remove old code, optional probes * update offsets * remove prints, add dependents probe filtering * Add changelog statement * add manager tests * add license to manager tests * format new test file * Address PR feedback * Update offsets * simplify the changed code, since we don't need to count probes now * atomic writes for traceparent * add dependents validation and tests * apply pr feedback --- CHANGELOG.md | 1 + internal/pkg/inject/offset_results.json | 328 ++++++++++++++++++ .../bpf/net/http/client/bpf/probe.bpf.c | 188 +++++----- .../bpf/net/http/client/bpf_bpfel_arm64.go | 6 + .../bpf/net/http/client/bpf_bpfel_x86.go | 6 + .../net/http/client/bpf_no_tp_bpfel_arm64.go | 169 +++++++++ .../net/http/client/bpf_no_tp_bpfel_x86.go | 169 +++++++++ .../bpf/net/http/client/probe.go | 56 ++- internal/pkg/instrumentation/manager.go | 32 +- internal/pkg/instrumentation/manager_test.go | 199 +++++++++++ .../pkg/instrumentation/probe/manifest.go | 14 +- .../instrumentation/probe/manifest_test.go | 8 +- internal/pkg/instrumentation/probe/probe.go | 7 +- internal/pkg/opentelemetry/controller.go | 2 +- internal/tools/inspect/cmd/offsetgen/main.go | 2 + .../cmd/offsetgen/{verions.go => versions.go} | 0 16 files changed, 1062 insertions(+), 125 deletions(-) create mode 100644 internal/pkg/instrumentation/bpf/net/http/client/bpf_no_tp_bpfel_arm64.go create mode 100644 internal/pkg/instrumentation/bpf/net/http/client/bpf_no_tp_bpfel_x86.go create mode 100644 internal/pkg/instrumentation/manager_test.go rename internal/tools/inspect/cmd/offsetgen/{verions.go => versions.go} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2d85dabe..44f67c185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http - Base Dockerfile and build caching ([#683](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/683)) - Add `server.address`, `server.port` and `network.protocol.version` to HTTP client spans ([#696](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/696)) - Update http server attributes to latest semantic conventions ([#708](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/708)) +- Don't use external memory for http client header injection ([#705](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/705)) ### Fixed diff --git a/internal/pkg/inject/offset_results.json b/internal/pkg/inject/offset_results.json index 76634bbb9..c29c5f6f8 100644 --- a/internal/pkg/inject/offset_results.json +++ b/internal/pkg/inject/offset_results.json @@ -2085,6 +2085,334 @@ { "module": "std", "packages": [ + { + "package": "bufio", + "structs": [ + { + "struct": "Writer", + "fields": [ + { + "field": "buf", + "offsets": [ + { + "offset": 16, + "versions": [ + "1.12.0", + "1.12.1", + "1.12.2", + "1.12.3", + "1.12.4", + "1.12.5", + "1.12.6", + "1.12.7", + "1.12.8", + "1.12.9", + "1.12.10", + "1.12.11", + "1.12.12", + "1.12.13", + "1.12.14", + "1.12.15", + "1.12.16", + "1.12.17", + "1.13.0", + "1.13.1", + "1.13.2", + "1.13.3", + "1.13.4", + "1.13.5", + "1.13.6", + "1.13.7", + "1.13.8", + "1.13.9", + "1.13.10", + "1.13.11", + "1.13.12", + "1.13.13", + "1.13.14", + "1.13.15", + "1.14.0", + "1.14.1", + "1.14.2", + "1.14.3", + "1.14.4", + "1.14.5", + "1.14.6", + "1.14.7", + "1.14.8", + "1.14.9", + "1.14.10", + "1.14.11", + "1.14.12", + "1.14.13", + "1.14.14", + "1.14.15", + "1.15.0", + "1.15.1", + "1.15.2", + "1.15.3", + "1.15.4", + "1.15.5", + "1.15.6", + "1.15.7", + "1.15.8", + "1.15.9", + "1.15.10", + "1.15.11", + "1.15.12", + "1.15.13", + "1.15.14", + "1.15.15", + "1.16.0", + "1.16.1", + "1.16.2", + "1.16.3", + "1.16.4", + "1.16.5", + "1.16.6", + "1.16.7", + "1.16.8", + "1.16.9", + "1.16.10", + "1.16.11", + "1.16.12", + "1.16.13", + "1.16.14", + "1.16.15", + "1.17.0", + "1.17.1", + "1.17.2", + "1.17.3", + "1.17.4", + "1.17.5", + "1.17.6", + "1.17.7", + "1.17.8", + "1.17.9", + "1.17.10", + "1.17.11", + "1.17.12", + "1.17.13", + "1.18.0", + "1.18.1", + "1.18.2", + "1.18.3", + "1.18.4", + "1.18.5", + "1.18.6", + "1.18.7", + "1.18.8", + "1.18.9", + "1.18.10", + "1.19.0", + "1.19.1", + "1.19.2", + "1.19.3", + "1.19.4", + "1.19.5", + "1.19.6", + "1.19.7", + "1.19.8", + "1.19.9", + "1.19.10", + "1.19.11", + "1.19.12", + "1.19.13", + "1.20.0", + "1.20.1", + "1.20.2", + "1.20.3", + "1.20.4", + "1.20.5", + "1.20.6", + "1.20.7", + "1.20.8", + "1.20.9", + "1.20.10", + "1.20.11", + "1.20.12", + "1.20.13", + "1.20.14", + "1.21.0", + "1.21.1", + "1.21.2", + "1.21.3", + "1.21.4", + "1.21.5", + "1.21.6", + "1.21.7", + "1.21.8", + "1.21.9", + "1.22.0", + "1.22.1", + "1.22.2" + ] + } + ] + }, + { + "field": "n", + "offsets": [ + { + "offset": 40, + "versions": [ + "1.12.0", + "1.12.1", + "1.12.2", + "1.12.3", + "1.12.4", + "1.12.5", + "1.12.6", + "1.12.7", + "1.12.8", + "1.12.9", + "1.12.10", + "1.12.11", + "1.12.12", + "1.12.13", + "1.12.14", + "1.12.15", + "1.12.16", + "1.12.17", + "1.13.0", + "1.13.1", + "1.13.2", + "1.13.3", + "1.13.4", + "1.13.5", + "1.13.6", + "1.13.7", + "1.13.8", + "1.13.9", + "1.13.10", + "1.13.11", + "1.13.12", + "1.13.13", + "1.13.14", + "1.13.15", + "1.14.0", + "1.14.1", + "1.14.2", + "1.14.3", + "1.14.4", + "1.14.5", + "1.14.6", + "1.14.7", + "1.14.8", + "1.14.9", + "1.14.10", + "1.14.11", + "1.14.12", + "1.14.13", + "1.14.14", + "1.14.15", + "1.15.0", + "1.15.1", + "1.15.2", + "1.15.3", + "1.15.4", + "1.15.5", + "1.15.6", + "1.15.7", + "1.15.8", + "1.15.9", + "1.15.10", + "1.15.11", + "1.15.12", + "1.15.13", + "1.15.14", + "1.15.15", + "1.16.0", + "1.16.1", + "1.16.2", + "1.16.3", + "1.16.4", + "1.16.5", + "1.16.6", + "1.16.7", + "1.16.8", + "1.16.9", + "1.16.10", + "1.16.11", + "1.16.12", + "1.16.13", + "1.16.14", + "1.16.15", + "1.17.0", + "1.17.1", + "1.17.2", + "1.17.3", + "1.17.4", + "1.17.5", + "1.17.6", + "1.17.7", + "1.17.8", + "1.17.9", + "1.17.10", + "1.17.11", + "1.17.12", + "1.17.13", + "1.18.0", + "1.18.1", + "1.18.2", + "1.18.3", + "1.18.4", + "1.18.5", + "1.18.6", + "1.18.7", + "1.18.8", + "1.18.9", + "1.18.10", + "1.19.0", + "1.19.1", + "1.19.2", + "1.19.3", + "1.19.4", + "1.19.5", + "1.19.6", + "1.19.7", + "1.19.8", + "1.19.9", + "1.19.10", + "1.19.11", + "1.19.12", + "1.19.13", + "1.20.0", + "1.20.1", + "1.20.2", + "1.20.3", + "1.20.4", + "1.20.5", + "1.20.6", + "1.20.7", + "1.20.8", + "1.20.9", + "1.20.10", + "1.20.11", + "1.20.12", + "1.20.13", + "1.20.14", + "1.21.0", + "1.21.1", + "1.21.2", + "1.21.3", + "1.21.4", + "1.21.5", + "1.21.6", + "1.21.7", + "1.21.8", + "1.21.9", + "1.22.0", + "1.22.1", + "1.22.2" + ] + } + ] + } + ] + } + ] + }, { "package": "net/http", "structs": [ diff --git a/internal/pkg/instrumentation/bpf/net/http/client/bpf/probe.bpf.c b/internal/pkg/instrumentation/bpf/net/http/client/bpf/probe.bpf.c index 1c93be7fc..25273c5f9 100644 --- a/internal/pkg/instrumentation/bpf/net/http/client/bpf/probe.bpf.c +++ b/internal/pkg/instrumentation/bpf/net/http/client/bpf/probe.bpf.c @@ -43,6 +43,13 @@ struct __uint(max_entries, 1); } http_client_uprobe_storage_map SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __type(key, void*); // the headers ptr + __type(value, void*); // request key, goroutine or context ptr + __uint(max_entries, MAX_CONCURRENT); +} http_headers SEC(".maps"); + struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); } events SEC(".maps"); @@ -57,107 +64,8 @@ volatile const u64 buckets_ptr_pos; volatile const u64 status_code_pos; volatile const u64 request_host_pos; volatile const u64 request_proto_pos; - -static __always_inline long inject_header(void* headers_ptr, struct span_context* propagated_ctx) { - // Read the key-value count - this field must be the first one in the hmap struct as documented in src/runtime/map.go - u64 curr_keyvalue_count = 0; - long res = bpf_probe_read_user(&curr_keyvalue_count, sizeof(curr_keyvalue_count), headers_ptr); - - if (res < 0) { - bpf_printk("Couldn't read map key-value count from user"); - return -1; - } - - if (curr_keyvalue_count >= 8) { - bpf_printk("Map size is bigger than 8, skipping context propagation"); - return -1; - } - - // Get pointer to temp bucket struct we store in a map (avoiding large local variable on the stack) - // Performing read-modify-write on the bucket - u32 map_id = 0; - struct map_bucket *bucket_map_value = bpf_map_lookup_elem(&golang_mapbucket_storage_map, &map_id); - if (!bucket_map_value) { - return -1; - } - - void *buckets_ptr_ptr = (void*) (headers_ptr + buckets_ptr_pos); - void *bucket_ptr = 0; // The actual pointer to the buckets - - if (curr_keyvalue_count == 0) { - // No key-value pairs in the Go map, need to "allocate" memory for the user - bucket_ptr = write_target_data(bucket_map_value, sizeof(struct map_bucket)); - if (bucket_ptr == NULL) { - bpf_printk("inject_header: Failed to write bucket to user"); - return -1; - } - // Update the buckets pointer in the hmap struct to point to newly allocated bucket - res = bpf_probe_write_user(buckets_ptr_ptr, &bucket_ptr, sizeof(bucket_ptr)); - if (res < 0) { - bpf_printk("Failed to update the map bucket pointer for the user"); - return -1; - } - } else { - // There is at least 1 key-value pair, hence the bucket pointer from the user is valid - // Read the bucket pointer - res = bpf_probe_read_user(&bucket_ptr, sizeof(bucket_ptr), buckets_ptr_ptr); - // Read the user's bucket to the eBPF map entry - bpf_probe_read_user(bucket_map_value, sizeof(struct map_bucket), bucket_ptr); - } - - u8 bucket_index = curr_keyvalue_count & 0x7; - - char traceparent_tophash = 0xee; - bucket_map_value->tophash[bucket_index] = traceparent_tophash; - - // Prepare the key string for the user - char key[W3C_KEY_LENGTH] = "traceparent"; - void *ptr = write_target_data(key, W3C_KEY_LENGTH); - if (ptr == NULL) { - bpf_printk("inject_header: Failed to write key to user"); - return -1; - } - bucket_map_value->keys[bucket_index] = (struct go_string) {.len = W3C_KEY_LENGTH, .str = ptr}; - - // Prepare the value string slice - // First the value string which constains the span context - char val[W3C_VAL_LENGTH]; - span_context_to_w3c_string(propagated_ctx, val); - ptr = write_target_data(val, sizeof(val)); - if(ptr == NULL) { - bpf_printk("inject_header: Failed to write value to user"); - return -1; - } - - // The go string pointing to the above val - struct go_string header_value = {.len = W3C_VAL_LENGTH, .str = ptr}; - ptr = write_target_data((void*)&header_value, sizeof(header_value)); - if(ptr == NULL) { - bpf_printk("inject_header: Failed to write go_string to user"); - return -1; - } - - // Last, go_slice pointing to the above go_string - bucket_map_value->values[bucket_index] = (struct go_slice) {.array = ptr, .cap = 1, .len = 1}; - - // Update the map header count field - curr_keyvalue_count += 1; - res = bpf_probe_write_user(headers_ptr, &curr_keyvalue_count, sizeof(curr_keyvalue_count)); - if (res < 0) { - bpf_printk("Failed to update key-value count in map header"); - return -1; - } - - // Update the bucket - res = bpf_probe_write_user(bucket_ptr, bucket_map_value, sizeof(struct map_bucket)); - if (res < 0) { - bpf_printk("Failed to update bucket content"); - return -1; - } - - bpf_memset((unsigned char *)bucket_map_value, sizeof(struct map_bucket), 0); - return 0; -} +volatile const u64 io_writer_buf_ptr_pos; +volatile const u64 io_writer_n_pos; // This instrumentation attaches uprobe to the following function: // func net/http/transport.roundTrip(req *Request) (*Response, error) @@ -226,9 +134,8 @@ int uprobe_Transport_roundTrip(struct pt_regs *ctx) { // get headers from Request void *headers_ptr = 0; bpf_probe_read(&headers_ptr, sizeof(headers_ptr), (void *)(req_ptr+headers_ptr_pos)); - long res = inject_header(headers_ptr, &httpReq->sc); - if (res < 0) { - bpf_printk("uprobe_Transport_roundTrip: Failed to inject header"); + if (headers_ptr) { + bpf_map_update_elem(&http_headers, &headers_ptr, &key, 0); } // Write event @@ -265,4 +172,75 @@ int uprobe_Transport_roundTrip_Returns(struct pt_regs *ctx) { bpf_map_delete_elem(&http_events, &key); return 0; -} \ No newline at end of file +} + +#ifndef NO_HEADER_PROPAGATION +// This instrumentation attaches uprobe to the following function: +// func (h Header) net/http.Header.writeSubset(w io.Writer, exclude map[string]bool, trace *httptrace.ClientTrace) error +SEC("uprobe/header_writeSubset") +int uprobe_writeSubset(struct pt_regs *ctx) { + u64 headers_pos = 1; + void *headers_ptr = get_argument(ctx, headers_pos); + + u64 io_writer_pos = 3; + void *io_writer_ptr = get_argument(ctx, io_writer_pos); + + void **key_ptr = bpf_map_lookup_elem(&http_headers, &headers_ptr); + if (key_ptr) { + void *key = *key_ptr; + + struct http_request_t *http_req_span = bpf_map_lookup_elem(&http_events, &key); + if (http_req_span) { + char tp[W3C_VAL_LENGTH]; + span_context_to_w3c_string(&http_req_span->sc, tp); + + void *buf_ptr = 0; + bpf_probe_read(&buf_ptr, sizeof(buf_ptr), (void *)(io_writer_ptr + io_writer_buf_ptr_pos)); // grab buf ptr + if (!buf_ptr) { + bpf_printk("uprobe_writeSubset: Failed to get buf from io writer"); + goto done; + } + + s64 size = 0; + if (bpf_probe_read(&size, sizeof(s64), (void *)(io_writer_ptr + io_writer_buf_ptr_pos + offsetof(struct go_slice, cap)))) { // grab capacity + bpf_printk("uprobe_writeSubset: Failed to get size from io writer"); + goto done; + } + + s64 len = 0; + if (bpf_probe_read(&len, sizeof(s64), (void *)(io_writer_ptr + io_writer_n_pos))) { // grab len + bpf_printk("uprobe_writeSubset: Failed to get len from io writer"); + goto done; + } + + if (len < (size - W3C_VAL_LENGTH - W3C_KEY_LENGTH - 4)) { // 4 = strlen(":_") + strlen("\r\n") + char tp_str[W3C_KEY_LENGTH + 2 + W3C_VAL_LENGTH + 2] = "Traceparent: "; + char end[2] = "\r\n"; + __builtin_memcpy(&tp_str[W3C_KEY_LENGTH + 2], tp, sizeof(tp)); + __builtin_memcpy(&tp_str[W3C_KEY_LENGTH + 2 + W3C_VAL_LENGTH], end, sizeof(end)); + if (bpf_probe_write_user(buf_ptr + (len & 0x0ffff), tp_str, sizeof(tp_str))) { + bpf_printk("uprobe_writeSubset: Failed to write trace parent key in buffer"); + goto done; + } + len += W3C_KEY_LENGTH + 2 + W3C_VAL_LENGTH + 2; + if (bpf_probe_write_user((void *)(io_writer_ptr + io_writer_n_pos), &len, sizeof(len))) { + bpf_printk("uprobe_writeSubset: Failed to change io writer n"); + goto done; + } + } + } + } + +done: + bpf_map_delete_elem(&http_headers, &headers_ptr); + return 0; +} +#else +// Not used at all, empty stub needed to ensure both versions of the bpf program are +// able to compile with bpf2go. The userspace code will avoid loading the probe if +// context propagation is not enabled. +SEC("uprobe/header_writeSubset") +int uprobe_writeSubset(struct pt_regs *ctx) { + return 0; +} +#endif diff --git a/internal/pkg/instrumentation/bpf/net/http/client/bpf_bpfel_arm64.go b/internal/pkg/instrumentation/bpf/net/http/client/bpf_bpfel_arm64.go index 2811d12d5..71f2da972 100644 --- a/internal/pkg/instrumentation/bpf/net/http/client/bpf_bpfel_arm64.go +++ b/internal/pkg/instrumentation/bpf/net/http/client/bpf_bpfel_arm64.go @@ -75,6 +75,7 @@ type bpfSpecs struct { type bpfProgramSpecs struct { UprobeTransportRoundTrip *ebpf.ProgramSpec `ebpf:"uprobe_Transport_roundTrip"` UprobeTransportRoundTripReturns *ebpf.ProgramSpec `ebpf:"uprobe_Transport_roundTrip_Returns"` + UprobeWriteSubset *ebpf.ProgramSpec `ebpf:"uprobe_writeSubset"` } // bpfMapSpecs contains maps before they are loaded into the kernel. @@ -86,6 +87,7 @@ type bpfMapSpecs struct { GolangMapbucketStorageMap *ebpf.MapSpec `ebpf:"golang_mapbucket_storage_map"` HttpClientUprobeStorageMap *ebpf.MapSpec `ebpf:"http_client_uprobe_storage_map"` HttpEvents *ebpf.MapSpec `ebpf:"http_events"` + HttpHeaders *ebpf.MapSpec `ebpf:"http_headers"` SliceArrayBuffMap *ebpf.MapSpec `ebpf:"slice_array_buff_map"` TrackedSpans *ebpf.MapSpec `ebpf:"tracked_spans"` TrackedSpansBySc *ebpf.MapSpec `ebpf:"tracked_spans_by_sc"` @@ -115,6 +117,7 @@ type bpfMaps struct { GolangMapbucketStorageMap *ebpf.Map `ebpf:"golang_mapbucket_storage_map"` HttpClientUprobeStorageMap *ebpf.Map `ebpf:"http_client_uprobe_storage_map"` HttpEvents *ebpf.Map `ebpf:"http_events"` + HttpHeaders *ebpf.Map `ebpf:"http_headers"` SliceArrayBuffMap *ebpf.Map `ebpf:"slice_array_buff_map"` TrackedSpans *ebpf.Map `ebpf:"tracked_spans"` TrackedSpansBySc *ebpf.Map `ebpf:"tracked_spans_by_sc"` @@ -127,6 +130,7 @@ func (m *bpfMaps) Close() error { m.GolangMapbucketStorageMap, m.HttpClientUprobeStorageMap, m.HttpEvents, + m.HttpHeaders, m.SliceArrayBuffMap, m.TrackedSpans, m.TrackedSpansBySc, @@ -139,12 +143,14 @@ func (m *bpfMaps) Close() error { type bpfPrograms struct { UprobeTransportRoundTrip *ebpf.Program `ebpf:"uprobe_Transport_roundTrip"` UprobeTransportRoundTripReturns *ebpf.Program `ebpf:"uprobe_Transport_roundTrip_Returns"` + UprobeWriteSubset *ebpf.Program `ebpf:"uprobe_writeSubset"` } func (p *bpfPrograms) Close() error { return _BpfClose( p.UprobeTransportRoundTrip, p.UprobeTransportRoundTripReturns, + p.UprobeWriteSubset, ) } diff --git a/internal/pkg/instrumentation/bpf/net/http/client/bpf_bpfel_x86.go b/internal/pkg/instrumentation/bpf/net/http/client/bpf_bpfel_x86.go index 0a4e5275f..e77cf8f80 100644 --- a/internal/pkg/instrumentation/bpf/net/http/client/bpf_bpfel_x86.go +++ b/internal/pkg/instrumentation/bpf/net/http/client/bpf_bpfel_x86.go @@ -75,6 +75,7 @@ type bpfSpecs struct { type bpfProgramSpecs struct { UprobeTransportRoundTrip *ebpf.ProgramSpec `ebpf:"uprobe_Transport_roundTrip"` UprobeTransportRoundTripReturns *ebpf.ProgramSpec `ebpf:"uprobe_Transport_roundTrip_Returns"` + UprobeWriteSubset *ebpf.ProgramSpec `ebpf:"uprobe_writeSubset"` } // bpfMapSpecs contains maps before they are loaded into the kernel. @@ -86,6 +87,7 @@ type bpfMapSpecs struct { GolangMapbucketStorageMap *ebpf.MapSpec `ebpf:"golang_mapbucket_storage_map"` HttpClientUprobeStorageMap *ebpf.MapSpec `ebpf:"http_client_uprobe_storage_map"` HttpEvents *ebpf.MapSpec `ebpf:"http_events"` + HttpHeaders *ebpf.MapSpec `ebpf:"http_headers"` SliceArrayBuffMap *ebpf.MapSpec `ebpf:"slice_array_buff_map"` TrackedSpans *ebpf.MapSpec `ebpf:"tracked_spans"` TrackedSpansBySc *ebpf.MapSpec `ebpf:"tracked_spans_by_sc"` @@ -115,6 +117,7 @@ type bpfMaps struct { GolangMapbucketStorageMap *ebpf.Map `ebpf:"golang_mapbucket_storage_map"` HttpClientUprobeStorageMap *ebpf.Map `ebpf:"http_client_uprobe_storage_map"` HttpEvents *ebpf.Map `ebpf:"http_events"` + HttpHeaders *ebpf.Map `ebpf:"http_headers"` SliceArrayBuffMap *ebpf.Map `ebpf:"slice_array_buff_map"` TrackedSpans *ebpf.Map `ebpf:"tracked_spans"` TrackedSpansBySc *ebpf.Map `ebpf:"tracked_spans_by_sc"` @@ -127,6 +130,7 @@ func (m *bpfMaps) Close() error { m.GolangMapbucketStorageMap, m.HttpClientUprobeStorageMap, m.HttpEvents, + m.HttpHeaders, m.SliceArrayBuffMap, m.TrackedSpans, m.TrackedSpansBySc, @@ -139,12 +143,14 @@ func (m *bpfMaps) Close() error { type bpfPrograms struct { UprobeTransportRoundTrip *ebpf.Program `ebpf:"uprobe_Transport_roundTrip"` UprobeTransportRoundTripReturns *ebpf.Program `ebpf:"uprobe_Transport_roundTrip_Returns"` + UprobeWriteSubset *ebpf.Program `ebpf:"uprobe_writeSubset"` } func (p *bpfPrograms) Close() error { return _BpfClose( p.UprobeTransportRoundTrip, p.UprobeTransportRoundTripReturns, + p.UprobeWriteSubset, ) } diff --git a/internal/pkg/instrumentation/bpf/net/http/client/bpf_no_tp_bpfel_arm64.go b/internal/pkg/instrumentation/bpf/net/http/client/bpf_no_tp_bpfel_arm64.go new file mode 100644 index 000000000..e22fd907e --- /dev/null +++ b/internal/pkg/instrumentation/bpf/net/http/client/bpf_no_tp_bpfel_arm64.go @@ -0,0 +1,169 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build arm64 + +package client + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type bpf_no_tpHttpRequestT struct { + StartTime uint64 + EndTime uint64 + Sc bpf_no_tpSpanContext + Psc bpf_no_tpSpanContext + Host [256]int8 + Proto [8]int8 + StatusCode uint64 + Method [10]int8 + Path [100]int8 + _ [2]byte +} + +type bpf_no_tpSliceArrayBuff struct{ Buff [1024]uint8 } + +type bpf_no_tpSpanContext struct { + TraceID [16]uint8 + SpanID [8]uint8 +} + +// loadBpf_no_tp returns the embedded CollectionSpec for bpf_no_tp. +func loadBpf_no_tp() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_Bpf_no_tpBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load bpf_no_tp: %w", err) + } + + return spec, err +} + +// loadBpf_no_tpObjects loads bpf_no_tp and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *bpf_no_tpObjects +// *bpf_no_tpPrograms +// *bpf_no_tpMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadBpf_no_tpObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadBpf_no_tp() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// bpf_no_tpSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpf_no_tpSpecs struct { + bpf_no_tpProgramSpecs + bpf_no_tpMapSpecs +} + +// bpf_no_tpSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpf_no_tpProgramSpecs struct { + UprobeTransportRoundTrip *ebpf.ProgramSpec `ebpf:"uprobe_Transport_roundTrip"` + UprobeTransportRoundTripReturns *ebpf.ProgramSpec `ebpf:"uprobe_Transport_roundTrip_Returns"` + UprobeWriteSubset *ebpf.ProgramSpec `ebpf:"uprobe_writeSubset"` +} + +// bpf_no_tpMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpf_no_tpMapSpecs struct { + AllocMap *ebpf.MapSpec `ebpf:"alloc_map"` + Events *ebpf.MapSpec `ebpf:"events"` + GolangMapbucketStorageMap *ebpf.MapSpec `ebpf:"golang_mapbucket_storage_map"` + HttpClientUprobeStorageMap *ebpf.MapSpec `ebpf:"http_client_uprobe_storage_map"` + HttpEvents *ebpf.MapSpec `ebpf:"http_events"` + HttpHeaders *ebpf.MapSpec `ebpf:"http_headers"` + SliceArrayBuffMap *ebpf.MapSpec `ebpf:"slice_array_buff_map"` + TrackedSpans *ebpf.MapSpec `ebpf:"tracked_spans"` + TrackedSpansBySc *ebpf.MapSpec `ebpf:"tracked_spans_by_sc"` +} + +// bpf_no_tpObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadBpf_no_tpObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpf_no_tpObjects struct { + bpf_no_tpPrograms + bpf_no_tpMaps +} + +func (o *bpf_no_tpObjects) Close() error { + return _Bpf_no_tpClose( + &o.bpf_no_tpPrograms, + &o.bpf_no_tpMaps, + ) +} + +// bpf_no_tpMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadBpf_no_tpObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpf_no_tpMaps struct { + AllocMap *ebpf.Map `ebpf:"alloc_map"` + Events *ebpf.Map `ebpf:"events"` + GolangMapbucketStorageMap *ebpf.Map `ebpf:"golang_mapbucket_storage_map"` + HttpClientUprobeStorageMap *ebpf.Map `ebpf:"http_client_uprobe_storage_map"` + HttpEvents *ebpf.Map `ebpf:"http_events"` + HttpHeaders *ebpf.Map `ebpf:"http_headers"` + SliceArrayBuffMap *ebpf.Map `ebpf:"slice_array_buff_map"` + TrackedSpans *ebpf.Map `ebpf:"tracked_spans"` + TrackedSpansBySc *ebpf.Map `ebpf:"tracked_spans_by_sc"` +} + +func (m *bpf_no_tpMaps) Close() error { + return _Bpf_no_tpClose( + m.AllocMap, + m.Events, + m.GolangMapbucketStorageMap, + m.HttpClientUprobeStorageMap, + m.HttpEvents, + m.HttpHeaders, + m.SliceArrayBuffMap, + m.TrackedSpans, + m.TrackedSpansBySc, + ) +} + +// bpf_no_tpPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadBpf_no_tpObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpf_no_tpPrograms struct { + UprobeTransportRoundTrip *ebpf.Program `ebpf:"uprobe_Transport_roundTrip"` + UprobeTransportRoundTripReturns *ebpf.Program `ebpf:"uprobe_Transport_roundTrip_Returns"` + UprobeWriteSubset *ebpf.Program `ebpf:"uprobe_writeSubset"` +} + +func (p *bpf_no_tpPrograms) Close() error { + return _Bpf_no_tpClose( + p.UprobeTransportRoundTrip, + p.UprobeTransportRoundTripReturns, + p.UprobeWriteSubset, + ) +} + +func _Bpf_no_tpClose(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_no_tp_bpfel_arm64.o +var _Bpf_no_tpBytes []byte diff --git a/internal/pkg/instrumentation/bpf/net/http/client/bpf_no_tp_bpfel_x86.go b/internal/pkg/instrumentation/bpf/net/http/client/bpf_no_tp_bpfel_x86.go new file mode 100644 index 000000000..1df381384 --- /dev/null +++ b/internal/pkg/instrumentation/bpf/net/http/client/bpf_no_tp_bpfel_x86.go @@ -0,0 +1,169 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 + +package client + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type bpf_no_tpHttpRequestT struct { + StartTime uint64 + EndTime uint64 + Sc bpf_no_tpSpanContext + Psc bpf_no_tpSpanContext + Host [256]int8 + Proto [8]int8 + StatusCode uint64 + Method [10]int8 + Path [100]int8 + _ [2]byte +} + +type bpf_no_tpSliceArrayBuff struct{ Buff [1024]uint8 } + +type bpf_no_tpSpanContext struct { + TraceID [16]uint8 + SpanID [8]uint8 +} + +// loadBpf_no_tp returns the embedded CollectionSpec for bpf_no_tp. +func loadBpf_no_tp() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_Bpf_no_tpBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load bpf_no_tp: %w", err) + } + + return spec, err +} + +// loadBpf_no_tpObjects loads bpf_no_tp and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *bpf_no_tpObjects +// *bpf_no_tpPrograms +// *bpf_no_tpMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadBpf_no_tpObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadBpf_no_tp() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// bpf_no_tpSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpf_no_tpSpecs struct { + bpf_no_tpProgramSpecs + bpf_no_tpMapSpecs +} + +// bpf_no_tpSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpf_no_tpProgramSpecs struct { + UprobeTransportRoundTrip *ebpf.ProgramSpec `ebpf:"uprobe_Transport_roundTrip"` + UprobeTransportRoundTripReturns *ebpf.ProgramSpec `ebpf:"uprobe_Transport_roundTrip_Returns"` + UprobeWriteSubset *ebpf.ProgramSpec `ebpf:"uprobe_writeSubset"` +} + +// bpf_no_tpMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpf_no_tpMapSpecs struct { + AllocMap *ebpf.MapSpec `ebpf:"alloc_map"` + Events *ebpf.MapSpec `ebpf:"events"` + GolangMapbucketStorageMap *ebpf.MapSpec `ebpf:"golang_mapbucket_storage_map"` + HttpClientUprobeStorageMap *ebpf.MapSpec `ebpf:"http_client_uprobe_storage_map"` + HttpEvents *ebpf.MapSpec `ebpf:"http_events"` + HttpHeaders *ebpf.MapSpec `ebpf:"http_headers"` + SliceArrayBuffMap *ebpf.MapSpec `ebpf:"slice_array_buff_map"` + TrackedSpans *ebpf.MapSpec `ebpf:"tracked_spans"` + TrackedSpansBySc *ebpf.MapSpec `ebpf:"tracked_spans_by_sc"` +} + +// bpf_no_tpObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadBpf_no_tpObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpf_no_tpObjects struct { + bpf_no_tpPrograms + bpf_no_tpMaps +} + +func (o *bpf_no_tpObjects) Close() error { + return _Bpf_no_tpClose( + &o.bpf_no_tpPrograms, + &o.bpf_no_tpMaps, + ) +} + +// bpf_no_tpMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadBpf_no_tpObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpf_no_tpMaps struct { + AllocMap *ebpf.Map `ebpf:"alloc_map"` + Events *ebpf.Map `ebpf:"events"` + GolangMapbucketStorageMap *ebpf.Map `ebpf:"golang_mapbucket_storage_map"` + HttpClientUprobeStorageMap *ebpf.Map `ebpf:"http_client_uprobe_storage_map"` + HttpEvents *ebpf.Map `ebpf:"http_events"` + HttpHeaders *ebpf.Map `ebpf:"http_headers"` + SliceArrayBuffMap *ebpf.Map `ebpf:"slice_array_buff_map"` + TrackedSpans *ebpf.Map `ebpf:"tracked_spans"` + TrackedSpansBySc *ebpf.Map `ebpf:"tracked_spans_by_sc"` +} + +func (m *bpf_no_tpMaps) Close() error { + return _Bpf_no_tpClose( + m.AllocMap, + m.Events, + m.GolangMapbucketStorageMap, + m.HttpClientUprobeStorageMap, + m.HttpEvents, + m.HttpHeaders, + m.SliceArrayBuffMap, + m.TrackedSpans, + m.TrackedSpansBySc, + ) +} + +// bpf_no_tpPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadBpf_no_tpObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpf_no_tpPrograms struct { + UprobeTransportRoundTrip *ebpf.Program `ebpf:"uprobe_Transport_roundTrip"` + UprobeTransportRoundTripReturns *ebpf.Program `ebpf:"uprobe_Transport_roundTrip_Returns"` + UprobeWriteSubset *ebpf.Program `ebpf:"uprobe_writeSubset"` +} + +func (p *bpf_no_tpPrograms) Close() error { + return _Bpf_no_tpClose( + p.UprobeTransportRoundTrip, + p.UprobeTransportRoundTripReturns, + p.UprobeWriteSubset, + ) +} + +func _Bpf_no_tpClose(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_no_tp_bpfel_x86.o +var _Bpf_no_tpBytes []byte diff --git a/internal/pkg/instrumentation/bpf/net/http/client/probe.go b/internal/pkg/instrumentation/bpf/net/http/client/probe.go index 39033379b..e39a6883a 100644 --- a/internal/pkg/instrumentation/bpf/net/http/client/probe.go +++ b/internal/pkg/instrumentation/bpf/net/http/client/probe.go @@ -37,6 +37,7 @@ import ( ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64,arm64 -cc clang -cflags $CFLAGS bpf ./bpf/probe.bpf.c +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64,arm64 -cc clang -cflags $CFLAGS bpf_no_tp ./bpf/probe.bpf.c -- -DNO_HEADER_PROPAGATION const ( // pkg is the package being instrumented. @@ -49,6 +50,29 @@ func New(logger logr.Logger) probe.Probe { SpanKind: trace.SpanKindClient, InstrumentedPkg: pkg, } + + uprobes := []probe.Uprobe[bpfObjects]{ + { + Sym: "net/http.(*Transport).roundTrip", + Fn: uprobeRoundTrip, + }, + } + + // If the kernel supports context propagation, we enable the + // probe which writes the data in the outgoing buffer. + if utils.SupportsContextPropagation() { + uprobes = append(uprobes, + probe.Uprobe[bpfObjects]{ + Sym: "net/http.Header.writeSubset", + Fn: uprobeWriteSubset, + // We mark this probe as dependent on roundTrip, so we don't accidentally + // enable this bpf program, if the executable has compiled in writeSubset, + // but doesn't have any http roundTrip. + DependsOn: []string{"net/http.(*Transport).roundTrip"}, + }, + ) + } + return &probe.Base[bpfObjects, event]{ ID: id, Logger: logger.WithName(id.String()), @@ -91,14 +115,16 @@ func New(logger logr.Logger) probe.Probe { Key: "request_proto_pos", Val: structfield.NewID("std", "net/http", "Request", "Proto"), }, - }, - Uprobes: []probe.Uprobe[bpfObjects]{ - { - Sym: "net/http.(*Transport).roundTrip", - Fn: uprobeRoundTrip, + probe.StructFieldConst{ + Key: "io_writer_buf_ptr_pos", + Val: structfield.NewID("std", "bufio", "Writer", "buf"), + }, + probe.StructFieldConst{ + Key: "io_writer_n_pos", + Val: structfield.NewID("std", "bufio", "Writer", "n"), }, }, - + Uprobes: uprobes, ReaderFn: func(obj bpfObjects) (*perf.Reader, error) { return perf.NewReader(obj.Events, os.Getpagesize()) }, @@ -109,7 +135,8 @@ func New(logger logr.Logger) probe.Probe { func verifyAndLoadBpf() (*ebpf.CollectionSpec, error) { if !utils.SupportsContextPropagation() { - return nil, fmt.Errorf("the Linux Kernel doesn't support context propagation, please check if the kernel is in lockdown mode (/sys/kernel/security/lockdown)") + fmt.Fprintf(os.Stderr, "the Linux Kernel doesn't support context propagation, please check if the kernel is in lockdown mode (/sys/kernel/security/lockdown)") + return loadBpf_no_tp() } return loadBpf() @@ -145,6 +172,21 @@ func uprobeRoundTrip(name string, exec *link.Executable, target *process.TargetD return links, nil } +func uprobeWriteSubset(name string, exec *link.Executable, target *process.TargetDetails, obj *bpfObjects) ([]link.Link, error) { + offset, err := target.GetFunctionOffset(name) + if err != nil { + return nil, err + } + + opts := &link.UprobeOptions{Address: offset} + l, err := exec.Uprobe("", obj.UprobeWriteSubset, opts) + if err != nil { + return nil, err + } + + return []link.Link{l}, nil +} + // event represents an event in an HTTP server during an HTTP // request-response. type event struct { diff --git a/internal/pkg/instrumentation/manager.go b/internal/pkg/instrumentation/manager.go index 0166ec936..33efd83c4 100644 --- a/internal/pkg/instrumentation/manager.go +++ b/internal/pkg/instrumentation/manager.go @@ -71,12 +71,34 @@ func NewManager(logger logr.Logger, otelController *opentelemetry.Controller, gl return m, nil } +func (m *Manager) validateProbeDependents(id probe.ID, symbols []probe.FunctionSymbol) error { + // Validate that dependent probes point to real standalone probes. + funcsMap := make(map[string]interface{}) + for _, s := range symbols { + funcsMap[s.Symbol] = nil + } + + for _, s := range symbols { + for _, d := range s.DependsOn { + if _, exists := funcsMap[d]; !exists { + return fmt.Errorf("library %s has declared a dependent function %s for probe %s which does not exist, aborting", id, d, s.Symbol) + } + } + } + + return nil +} + func (m *Manager) registerProbe(p probe.Probe) error { id := p.Manifest().Id if _, exists := m.probes[id]; exists { return fmt.Errorf("library %s registered twice, aborting", id) } + if err := m.validateProbeDependents(id, p.Manifest().Symbols); err != nil { + return err + } + m.probes[id] = p return nil } @@ -86,7 +108,7 @@ func (m *Manager) GetRelevantFuncs() map[string]interface{} { funcsMap := make(map[string]interface{}) for _, i := range m.probes { for _, s := range i.Manifest().Symbols { - funcsMap[s] = nil + funcsMap[s.Symbol] = nil } } @@ -104,9 +126,11 @@ func (m *Manager) FilterUnusedProbes(target *process.TargetDetails) { for name, inst := range m.probes { funcsFound := false for _, s := range inst.Manifest().Symbols { - if _, exists := existingFuncMap[s]; exists { - funcsFound = true - break + if len(s.DependsOn) == 0 { + if _, exists := existingFuncMap[s.Symbol]; exists { + funcsFound = true + break + } } } diff --git a/internal/pkg/instrumentation/manager_test.go b/internal/pkg/instrumentation/manager_test.go new file mode 100644 index 000000000..21c9da328 --- /dev/null +++ b/internal/pkg/instrumentation/manager_test.go @@ -0,0 +1,199 @@ +// 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 instrumentation + +import ( + "log" + "os" + "testing" + + "github.com/go-logr/stdr" + "github.com/hashicorp/go-version" + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/auto/internal/pkg/instrumentation/probe" + "go.opentelemetry.io/auto/internal/pkg/process" + "go.opentelemetry.io/auto/internal/pkg/process/binary" +) + +func TestProbeFiltering(t *testing.T) { + ver, err := version.NewVersion("1.20.0") + assert.NoError(t, err) + + t.Run("empty target details", func(t *testing.T) { + m := fakeManager(t) + + td := process.TargetDetails{ + PID: 1, + Functions: []*binary.Func{}, + GoVersion: ver, + Libraries: map[string]*version.Version{}, + AllocationDetails: nil, + } + m.FilterUnusedProbes(&td) + assert.Equal(t, 0, len(m.probes)) + }) + + t.Run("only HTTP client target details", func(t *testing.T) { + m := fakeManager(t) + + httpFuncs := []*binary.Func{ + {Name: "net/http.(*Transport).roundTrip"}, + } + + td := process.TargetDetails{ + PID: 1, + Functions: httpFuncs, + GoVersion: ver, + Libraries: map[string]*version.Version{}, + AllocationDetails: nil, + } + m.FilterUnusedProbes(&td) + assert.Equal(t, 1, len(m.probes)) // one function, single probe + }) + + t.Run("HTTP server and client target details", func(t *testing.T) { + m := fakeManager(t) + + httpFuncs := []*binary.Func{ + {Name: "net/http.(*Transport).roundTrip"}, + {Name: "net/http.serverHandler.ServeHTTP"}, + } + + td := process.TargetDetails{ + PID: 1, + Functions: httpFuncs, + GoVersion: ver, + Libraries: map[string]*version.Version{}, + AllocationDetails: nil, + } + m.FilterUnusedProbes(&td) + assert.Equal(t, 2, len(m.probes)) + }) + + t.Run("HTTP server and client dependent function only target details", func(t *testing.T) { + m := fakeManager(t) + + httpFuncs := []*binary.Func{ + // writeSubset depends on "net/http.(*Transport).roundTrip", it should be ignored without roundTrip + {Name: "net/http.Header.writeSubset"}, + {Name: "net/http.serverHandler.ServeHTTP"}, + } + + td := process.TargetDetails{ + PID: 1, + Functions: httpFuncs, + GoVersion: ver, + Libraries: map[string]*version.Version{}, + AllocationDetails: nil, + } + m.FilterUnusedProbes(&td) + assert.Equal(t, 1, len(m.probes)) + }) +} + +func TestDependencyChecks(t *testing.T) { + m := fakeManager(t) + + t.Run("Dependent probes match", func(t *testing.T) { + syms := []probe.FunctionSymbol{ + { + Symbol: "A", + DependsOn: nil, + }, + { + Symbol: "B", + DependsOn: []string{"A"}, + }, + } + + assert.Nil(t, m.validateProbeDependents(probe.ID{InstrumentedPkg: "test"}, syms)) + }) + + t.Run("Second dependent missing", func(t *testing.T) { + syms := []probe.FunctionSymbol{ + { + Symbol: "A", + DependsOn: nil, + }, + { + Symbol: "B", + DependsOn: []string{"A", "C"}, + }, + } + + assert.NotNil(t, m.validateProbeDependents(probe.ID{InstrumentedPkg: "test"}, syms)) + }) + + t.Run("Second dependent present", func(t *testing.T) { + syms := []probe.FunctionSymbol{ + { + Symbol: "A", + DependsOn: nil, + }, + { + Symbol: "B", + DependsOn: []string{"A", "C"}, + }, + { + Symbol: "C", + DependsOn: []string{"A"}, + }, + } + + assert.Nil(t, m.validateProbeDependents(probe.ID{InstrumentedPkg: "test"}, syms)) + }) + + t.Run("Dependent wrong", func(t *testing.T) { + syms := []probe.FunctionSymbol{ + { + Symbol: "A", + DependsOn: nil, + }, + { + Symbol: "B", + DependsOn: []string{"A1"}, + }, + } + + assert.NotNil(t, m.validateProbeDependents(probe.ID{InstrumentedPkg: "test"}, syms)) + }) + + t.Run("Two probes without dependents", func(t *testing.T) { + syms := []probe.FunctionSymbol{ + { + Symbol: "A", + DependsOn: nil, + }, + { + Symbol: "B", + DependsOn: []string{}, + }, + } + + assert.Nil(t, m.validateProbeDependents(probe.ID{InstrumentedPkg: "test"}, syms)) + }) +} + +func fakeManager(t *testing.T) *Manager { + logger := stdr.New(log.New(os.Stderr, "", log.LstdFlags)) + logger = logger.WithName("Instrumentation") + + m, err := NewManager(logger, nil, true) + assert.NoError(t, err) + assert.NotNil(t, m) + + return m +} diff --git a/internal/pkg/instrumentation/probe/manifest.go b/internal/pkg/instrumentation/probe/manifest.go index 2d6e11ef5..0602690a7 100644 --- a/internal/pkg/instrumentation/probe/manifest.go +++ b/internal/pkg/instrumentation/probe/manifest.go @@ -23,6 +23,12 @@ import ( "go.opentelemetry.io/auto/internal/pkg/structfield" ) +// Wrapper object for the Manifest function symbol. +type FunctionSymbol struct { + Symbol string + DependsOn []string +} + // Manifest contains information about a package being instrumented. type Manifest struct { // ID is a unique identifier for the probe. @@ -34,7 +40,7 @@ type Manifest struct { // Symbols are the runtime symbols that are used to attach a probe's eBPF // program to a perf events. - Symbols []string + Symbols []FunctionSymbol } // ID is a unique identifier for a probe. @@ -52,7 +58,7 @@ func (id ID) String() string { // NewManifest returns a new Manifest for the instrumentation probe with name // that instruments pkg. The structfields and symbols will be sorted in-place // and added directly to the returned Manifest. -func NewManifest(id ID, structfields []structfield.ID, symbols []string) Manifest { +func NewManifest(id ID, structfields []structfield.ID, symbols []FunctionSymbol) Manifest { sort.Slice(structfields, func(i, j int) bool { if structfields[i].ModPath == structfields[j].ModPath { if structfields[i].PkgPath == structfields[j].PkgPath { @@ -66,7 +72,9 @@ func NewManifest(id ID, structfields []structfield.ID, symbols []string) Manifes return structfields[i].ModPath < structfields[j].ModPath }) - sort.Strings(symbols) + sort.Slice(symbols, func(i, j int) bool { + return symbols[i].Symbol < symbols[j].Symbol + }) return Manifest{ Id: id, diff --git a/internal/pkg/instrumentation/probe/manifest_test.go b/internal/pkg/instrumentation/probe/manifest_test.go index b6ce2c1b7..b90501d50 100644 --- a/internal/pkg/instrumentation/probe/manifest_test.go +++ b/internal/pkg/instrumentation/probe/manifest_test.go @@ -23,6 +23,10 @@ import ( "go.opentelemetry.io/auto/internal/pkg/structfield" ) +func fs(s string) FunctionSymbol { + return FunctionSymbol{Symbol: s, DependsOn: nil} +} + func TestNewManifest(t *testing.T) { const ( spanKind = trace.SpanKindServer @@ -48,12 +52,12 @@ func TestNewManifest(t *testing.T) { got := NewManifest( ID{spanKind, pkg}, []structfield.ID{sAABB, sABAA, sAAAA, sAAAC, sBAAA, sAAAB, sAABA, sAABC}, - []string{d, a, c, b}, + []FunctionSymbol{fs(d), fs(a), fs(c), fs(b)}, ) want := Manifest{ Id: ID{spanKind, pkg}, StructFields: []structfield.ID{sAAAA, sAAAB, sAAAC, sAABA, sAABB, sAABC, sABAA, sBAAA}, - Symbols: []string{a, b, c, d}, + Symbols: []FunctionSymbol{fs(a), fs(b), fs(c), fs(d)}, } assert.Equal(t, want, got) } diff --git a/internal/pkg/instrumentation/probe/probe.go b/internal/pkg/instrumentation/probe/probe.go index f553dc7ad..2ffdf31ba 100644 --- a/internal/pkg/instrumentation/probe/probe.go +++ b/internal/pkg/instrumentation/probe/probe.go @@ -86,9 +86,9 @@ type Base[BPFObj any, BPFEvent any] struct { func (i *Base[BPFObj, BPFEvent]) Manifest() Manifest { structfields := consts(i.Consts).structFields() - symbols := make([]string, 0, len(i.Uprobes)) + symbols := make([]FunctionSymbol, 0, len(i.Uprobes)) for _, up := range i.Uprobes { - symbols = append(symbols, up.Sym) + symbols = append(symbols, FunctionSymbol{Symbol: up.Sym, DependsOn: up.DependsOn}) } return NewManifest(i.ID, structfields, symbols) @@ -235,7 +235,8 @@ type Uprobe[BPFObj any] struct { // Optional is a boolean flag informing if the Uprobe is optional. If the // Uprobe is optional and fails to attach, the error is logged and // processing continues. - Optional bool + Optional bool + DependsOn []string } // Const is an constant that needs to be injected into an eBPF program. diff --git a/internal/pkg/opentelemetry/controller.go b/internal/pkg/opentelemetry/controller.go index d34dae24f..da678c955 100644 --- a/internal/pkg/opentelemetry/controller.go +++ b/internal/pkg/opentelemetry/controller.go @@ -52,7 +52,7 @@ func (c *Controller) getTracer(pkg string) trace.Tracer { // Trace creates a trace span for event. func (c *Controller) Trace(event *probe.Event) { for _, se := range event.SpanEvents { - c.logger.Info("got event", "kind", event.Kind.String(), "pkg", event.Package, "attrs", se.Attributes) + c.logger.Info("got event", "kind", event.Kind.String(), "pkg", event.Package, "attrs", se.Attributes, "traceID", se.SpanContext.TraceID().String(), "spanID", se.SpanContext.SpanID().String()) ctx := context.Background() if se.SpanContext == nil { diff --git a/internal/tools/inspect/cmd/offsetgen/main.go b/internal/tools/inspect/cmd/offsetgen/main.go index 93e42b67a..8d8e58019 100644 --- a/internal/tools/inspect/cmd/offsetgen/main.go +++ b/internal/tools/inspect/cmd/offsetgen/main.go @@ -123,6 +123,8 @@ func manifests() ([]inspect.Manifest, error) { structfield.NewID("std", "net/http", "Request", "RequestURI"), structfield.NewID("std", "net/http", "Request", "Host"), structfield.NewID("std", "net/url", "URL", "Path"), + structfield.NewID("std", "bufio", "Writer", "buf"), + structfield.NewID("std", "bufio", "Writer", "n"), }, }, { diff --git a/internal/tools/inspect/cmd/offsetgen/verions.go b/internal/tools/inspect/cmd/offsetgen/versions.go similarity index 100% rename from internal/tools/inspect/cmd/offsetgen/verions.go rename to internal/tools/inspect/cmd/offsetgen/versions.go