Skip to content

Commit

Permalink
Switch to using per header field parsing for Go versions >= 1.24 (#1636)
Browse files Browse the repository at this point in the history
* switch to using per header field parsing

* address review feedback

* update change log

* fix lint issue

* add depends on for the new probe

* Update internal/pkg/instrumentation/bpf/net/http/server/probe.go

Co-authored-by: Tyler Yahn <[email protected]>

* update change log

* Update changelog after 2.0

---------

Co-authored-by: Tyler Yahn <[email protected]>
  • Loading branch information
grcevski and MrAlias authored Jan 30, 2025
1 parent 85b7615 commit 23291df
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http

## [Unreleased]

### Added
- Preemptively update instrumentation for `net/http` to support Go `1.24` when swiss maps are going to be added. ([#1636](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1636))

## [v0.20.0] - 2025-01-29

### Added
Expand Down
12 changes: 12 additions & 0 deletions internal/include/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ static __always_inline bool bpf_memcmp(char *s1, char *s2, s32 size)
return true;
}

// assumes s2 is all lowercase
static __always_inline int bpf_memicmp(const char *s1, const char *s2, s32 size) {
for (int i = 0; i < size; i++) {
if (s1[i] != s2[i] && s1[i] != (s2[i] - 32)) // compare with each uppercase character
{
return i + 1;
}
}

return 0;
}

static __always_inline void generate_random_bytes(unsigned char *buff, u32 size)
{
for (int i = 0; i < (size / 4); i++)
Expand Down
67 changes: 65 additions & 2 deletions internal/pkg/instrumentation/bpf/net/http/server/bpf/probe.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ struct
__uint(max_entries, MAX_CONCURRENT);
} http_server_uprobes SEC(".maps");

struct
{
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, void *);
__type(value, struct span_context);
__uint(max_entries, MAX_CONCURRENT);
} http_server_context_headers SEC(".maps");

struct
{
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
Expand Down Expand Up @@ -84,11 +92,13 @@ volatile const bool pattern_path_supported;
// In case pattern handlers are supported the following offsets will be used:
volatile const u64 req_pat_pos;
volatile const u64 pat_str_pos;
// A flag indicating whether the Go version is using swiss maps
volatile const bool swiss_maps_used;

// Extracts the span context from the request headers by looking for the 'traceparent' header.
// Fills the parent_span_context with the extracted span context.
// Returns 0 on success, negative value on error.
static __always_inline long extract_context_from_req_headers(void *headers_ptr_ptr, struct span_context *parent_span_context)
static __always_inline long extract_context_from_req_headers_go_map(void *headers_ptr_ptr, struct span_context *parent_span_context)
{
void *headers_ptr;
long res;
Expand Down Expand Up @@ -178,6 +188,23 @@ static __always_inline long extract_context_from_req_headers(void *headers_ptr_p
return -1;
}

static __always_inline long extract_context_from_req_headers_pre_parsed(void *key, struct span_context *parent_span_context) {
struct span_context *parsed_header_context = bpf_map_lookup_elem(&http_server_context_headers, &key);
if (!parsed_header_context) {
return -1;
}

__builtin_memcpy(parent_span_context, parsed_header_context, sizeof(struct span_context));
return 0;
}

static __always_inline long extract_context_from_req_headers(void *key, struct span_context *parent_span_context) {
if (swiss_maps_used) {
return extract_context_from_req_headers_pre_parsed(key, parent_span_context);
}
return extract_context_from_req_headers_go_map(key, parent_span_context);
}

static __always_inline void read_go_string(void *base, int offset, char *output, int maxLen, const char *errorMsg) {
void *ptr = (void *)(base + offset);
if (!get_go_string_from_user_ptr(ptr, output, maxLen)) {
Expand Down Expand Up @@ -225,8 +252,17 @@ int uprobe_serverHandler_ServeHTTP(struct pt_regs *ctx)
.psc = &http_server_span->psc,
.sc = &http_server_span->sc,
.get_parent_span_context_fn = extract_context_from_req_headers,
.get_parent_span_context_arg = (void*)(req_ptr + headers_ptr_pos),
};

// If Go is using swiss maps, we currently rely on the uretprobe setup
// on readContinuedLineSlice to store the parsed value in a map, which
// we query with the same goroutine/context key.
if (swiss_maps_used) {
start_span_params.get_parent_span_context_arg = key;
} else {
start_span_params.get_parent_span_context_arg = (void*)(req_ptr + headers_ptr_pos);
}

start_span(&start_span_params);

bpf_map_update_elem(&http_server_uprobes, &key, uprobe_data, 0);
Expand All @@ -246,6 +282,7 @@ int uprobe_serverHandler_ServeHTTP_Returns(struct pt_regs *ctx) {
struct uprobe_data_t *uprobe_data = bpf_map_lookup_elem(&http_server_uprobes, &key);
if (uprobe_data == NULL) {
bpf_printk("uprobe/HandlerFunc_ServeHTTP_Returns: entry_state is NULL");
bpf_map_delete_elem(&http_server_context_headers, &key);
return 0;
}

Expand Down Expand Up @@ -280,5 +317,31 @@ int uprobe_serverHandler_ServeHTTP_Returns(struct pt_regs *ctx) {

stop_tracking_span(&http_server_span->sc, &http_server_span->psc);
bpf_map_delete_elem(&http_server_uprobes, &key);
bpf_map_delete_elem(&http_server_context_headers, &key);
return 0;
}

// This instrumentation attaches uprobe to the following function:
// func (r *Reader) readContinuedLineSlice(lim int64, validateFirstLine func([]byte) error) ([]byte, error) {
SEC("uprobe/textproto_Reader_readContinuedLineSlice")
int uprobe_textproto_Reader_readContinuedLineSlice_Returns(struct pt_regs *ctx) {
struct go_iface go_context = {0};
get_Go_context(ctx, 4, ctx_ptr_pos, false, &go_context);
void *key = get_consistent_key(ctx, go_context.data);

u64 len = (u64)GO_PARAM2(ctx);
u8 *buf = (u8 *)GO_PARAM1(ctx);

if (len >= (W3C_KEY_LENGTH + W3C_VAL_LENGTH + 2)) {
u8 temp[W3C_KEY_LENGTH + W3C_VAL_LENGTH + 2];
bpf_probe_read(temp, sizeof(temp), buf);

if (!bpf_memicmp((const char *)temp, "traceparent: ", W3C_KEY_LENGTH + 2)) {
struct span_context parent_span_context = {};
w3c_string_to_span_context((char *)(temp + W3C_KEY_LENGTH + 2), &parent_span_context);
bpf_map_update_elem(&http_server_context_headers, &key, &parent_span_context, BPF_ANY);
}
}

return 0;
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions internal/pkg/instrumentation/bpf/net/http/server/bpf_x86_bpfel.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 28 additions & 1 deletion internal/pkg/instrumentation/bpf/net/http/server/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,19 @@ import (

const (
// pkg is the package being instrumented.
pkg = "net/http"
pkg = "net/http"
minGoSwissMaps = "1.24.0"
)

var goWithSwissMaps = probe.PackageConstrainst{
Package: "std",
Constraints: version.MustConstraints(
version.NewConstraint(">= " + minGoSwissMaps),
),
// Don't warn, we have a backup path.
FailureMode: probe.FailureModeIgnore,
}

// New returns a new [probe.Probe].
func New(logger *slog.Logger, version string) probe.Probe {
id := probe.ID{
Expand Down Expand Up @@ -102,13 +112,22 @@ func New(logger *slog.Logger, version string) probe.Probe {
MinVersion: patternPathMinVersion,
},
patternPathSupportedConst{},
swissMapsUsedConst{},
},
Uprobes: []*probe.Uprobe{
{
Sym: "net/http.serverHandler.ServeHTTP",
EntryProbe: "uprobe_serverHandler_ServeHTTP",
ReturnProbe: "uprobe_serverHandler_ServeHTTP_Returns",
},
{
Sym: "net/textproto.(*Reader).readContinuedLineSlice",
ReturnProbe: "uprobe_textproto_Reader_readContinuedLineSlice_Returns",
PackageConstrainsts: []probe.PackageConstrainst{
goWithSwissMaps,
},
DependsOn: []string{"net/http.serverHandler.ServeHTTP"},
},
},
SpecFn: loadBpf,
},
Expand All @@ -130,6 +149,14 @@ func (c patternPathSupportedConst) InjectOption(td *process.TargetDetails) (inje
return inject.WithKeyValue("pattern_path_supported", isPatternPathSupported), nil
}

type swissMapsUsedConst struct{}

func (c swissMapsUsedConst) InjectOption(td *process.TargetDetails) (inject.Option, error) {
minGoSwissMapsVersion := version.Must(version.NewVersion(minGoSwissMaps))
isUsingGoSwissMaps := td.GoVersion.GreaterThanOrEqual(minGoSwissMapsVersion)
return inject.WithKeyValue("swiss_maps_used", isUsingGoSwissMaps), nil
}

// event represents an event in an HTTP server during an HTTP
// request-response.
type event struct {
Expand Down
3 changes: 2 additions & 1 deletion internal/pkg/instrumentation/probe/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func (i *Base[BPFObj, BPFEvent]) loadUprobes(exec *link.Executable, td *process.
logFn = i.Logger.Warn
default:
// Unknown and FailureModeError.
return fmt.Errorf("uprobe %s package constraint (%s) not meet", up.Sym, pc.Constraints.String())
return fmt.Errorf("uprobe %s package constraint (%s) not met, version %v", up.Sym, pc.Constraints.String(), td.Modules[pc.Package])
}

logFn(
Expand All @@ -197,6 +197,7 @@ func (i *Base[BPFObj, BPFEvent]) loadUprobes(exec *link.Executable, td *process.
"symbol", up.Sym,
"package", pc.Package,
"constraint", pc.Constraints.String(),
"version", td.Modules[pc.Package],
)

skip = true
Expand Down

0 comments on commit 23291df

Please sign in to comment.