Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Switch to using per header field parsing for Go versions >= 1.24 #1636

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http
- Support Go `1.22.11`. ([#1638](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1638))
- Support Go `1.23.5`. ([#1638](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1638))
- Support `google.golang.org/grpc` `1.70.0`. ([#1682](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1682))
- Support Go `1.24` swiss maps. ([#1636](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1636))

### Changed

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) {
grcevski marked this conversation as resolved.
Show resolved Hide resolved
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
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")
grcevski marked this conversation as resolved.
Show resolved Hide resolved
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.

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

28 changes: 27 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,21 @@ 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,
},
},
},
SpecFn: loadBpf,
},
Expand All @@ -130,6 +148,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
Loading