-
Notifications
You must be signed in to change notification settings - Fork 118
feat: support JSON-RPC 2.0 over http in golang's uprobe #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
grcevski
merged 41 commits into
open-telemetry:main
from
titaneric:feat/otel-bpf-patch-4
Jul 7, 2025
Merged
Changes from all commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
cd7ab13
apply patch from beyla
titaneric 77d6069
fix clang lint error
titaneric d371169
avoid variable length buffer read in readContinuedLineSlice
titaneric dd7d476
convert const char to const unsigned char
titaneric 1f762dc
slightly type change in bpf_memstr
titaneric b90326d
make both update traceparent and content type to const unsigned char
titaneric 5849e66
add temp bpf map and refactor buffer read in readContinuedLineSlice
titaneric e17ccc8
break early if ongoing_server_connections not found
titaneric 5233693
calculate http header by sizeof
titaneric 93ed4fb
refactor readContinuedLineSlice
titaneric 9a7cb1b
rename temp_mem map
titaneric 0461ae2
attempt jsonrpc tail code and introduce body bpg map
titaneric 706e0c0
detect json content type in ServeHTTP
titaneric 199b7e4
use `bpf_memicmp` instead of `json_str_value` to validate jsonrpc value
titaneric e427d2e
add early bound check and json_value_eq helper function
titaneric 1d1e93b
Add bound check and rewrite method value extraction
titaneric d735845
opening bracket check in extract_json_string
titaneric df327ca
tidy up jsonrpc parser
titaneric 5d0b7ff
reorder field in `server_http_func_invocation` struct
titaneric c9c563a
Add jsonrpc test server
titaneric 173f47e
add basic jsonrpc integration test
titaneric 2dfff52
move test json body into file
titaneric 9a1a656
restore suite test
titaneric 7818ca9
Add testSpanMetricsForJSONRPCHTTP
titaneric 1980292
Add json-rpc in multiprocess test
titaneric 437ecb3
move headers into static instead of stack, and format
titaneric 8c55f26
remove commented code
titaneric 3ec2691
fix the clang tidy error
titaneric dfb47c4
fix golint issue
titaneric ab710b6
overwrite jsonrpc port in duplicate testserver
titaneric be60731
attempt nested trace test for jsonrpc (WIP)
titaneric 01e12d5
fix jsonrpc payload and update jsonrpc logger
titaneric 286df52
add k8s integration test for jsonrpc server
titaneric 06d93ac
fix testcase format
titaneric f69506f
Increase test timeout to 5 min for k8s integration test
titaneric cfa2be1
restore timeout
titaneric f96fba9
Add delicated pattern for jsonrpc path in beyla's integration test
titaneric 5a9be01
fix prometheus metrics name in integration test
titaneric 969b5d5
Merge branch 'main' into feat/otel-bpf-patch-4
titaneric 1bc2d88
replace bpf_memicmp with stricmp
titaneric cce786c
update type handling for stricmp
titaneric File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,8 @@ | |
| #include <gotracer/go_str.h> | ||
| #include <gotracer/go_stream_key.h> | ||
| #include <gotracer/hpack.h> | ||
| #include <gotracer/protocol_jsonrpc.h> | ||
| #include <gotracer/maps/jsonrpc_jump_table.h> | ||
|
|
||
| #include <logger/bpf_dbg.h> | ||
|
|
||
|
|
@@ -35,6 +37,10 @@ | |
|
|
||
| #include <pid/pid_helpers.h> | ||
|
|
||
| enum { k_tail_jsonrpc = 0 }; | ||
| static const char traceparent[] = "traceparent: "; | ||
| static const char content_type[] = "content-type: "; | ||
|
|
||
| typedef struct http_client_data { | ||
| s64 content_length; | ||
| pid_info pid; | ||
|
|
@@ -64,10 +70,13 @@ typedef struct server_http_func_invocation { | |
| u64 content_length; | ||
| u64 response_length; | ||
| u64 status; | ||
| u64 body_addr; // pointer to the body buffer | ||
| tp_info_t tp; | ||
| unsigned char method[METHOD_MAX_LEN]; | ||
| unsigned char path[PATH_MAX_LEN]; | ||
| u8 _pad[5]; | ||
| unsigned char content_type[HTTP_CONTENT_TYPE_MAX_LEN]; | ||
| u8 method[METHOD_MAX_LEN]; | ||
| u8 path[PATH_MAX_LEN]; | ||
| u8 json_content_type; | ||
| u8 _pad[4]; | ||
| } server_http_func_invocation_t; | ||
|
|
||
| struct { | ||
|
|
@@ -77,6 +86,30 @@ struct { | |
| __uint(max_entries, MAX_CONCURRENT_REQUESTS); | ||
| } ongoing_http_server_requests SEC(".maps"); | ||
|
|
||
| struct { | ||
| __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); | ||
| __type(key, u32); | ||
| __type(value, unsigned char[HTTP_HEADER_MAX_LEN]); | ||
| __uint(max_entries, 1); | ||
| } temp_header_mem_store SEC(".maps"); | ||
|
|
||
| static __always_inline unsigned char *temp_header_mem() { | ||
| const u32 zero = 0; | ||
| return bpf_map_lookup_elem(&temp_header_mem_store, &zero); | ||
| } | ||
|
|
||
| struct { | ||
| __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); | ||
| __type(key, u32); | ||
| __type(value, unsigned char[HTTP_BODY_MAX_LEN]); | ||
| __uint(max_entries, 1); | ||
| } temp_body_mem_store SEC(".maps"); | ||
|
|
||
| static __always_inline unsigned char *temp_body_mem() { | ||
| const u32 zero = 0; | ||
| return bpf_map_lookup_elem(&temp_body_mem_store, &zero); | ||
| } | ||
|
|
||
| /* HTTP Server */ | ||
|
|
||
| // This instrumentation attaches uprobe to the following function: | ||
|
|
@@ -94,12 +127,12 @@ int beyla_uprobe_ServeHTTP(struct pt_regs *ctx) { | |
|
|
||
| off_table_t *ot = get_offsets_table(); | ||
|
|
||
| // Lookup any traceparent information setup for us by readContinuedLineSlice | ||
| server_http_func_invocation_t *tp_inv = | ||
| // Lookup any header information setup for us by readContinuedLineSlice | ||
| server_http_func_invocation_t *header_inv = | ||
| bpf_map_lookup_elem(&ongoing_http_server_requests, &g_key); | ||
| tp_info_t *decoded_tp = 0; | ||
| if (tp_inv && valid_trace(tp_inv->tp.trace_id)) { | ||
| decoded_tp = &tp_inv->tp; | ||
| if (header_inv && valid_trace(header_inv->tp.trace_id)) { | ||
| decoded_tp = &header_inv->tp; | ||
| } | ||
|
|
||
| server_http_func_invocation_t invocation = { | ||
|
|
@@ -118,6 +151,15 @@ int beyla_uprobe_ServeHTTP(struct pt_regs *ctx) { | |
| // TODO: if context propagation is supported, overwrite the header value in the map with the | ||
| // new span context and the same thread id. | ||
|
|
||
| // get content-type from readContinuedLineSlice | ||
| if (header_inv && header_inv->content_type[0]) { | ||
| bpf_dbg_printk("Found content type in ongoing request: %s", header_inv->content_type); | ||
| if (is_json_content_type((void *)header_inv->content_type, | ||
| sizeof(header_inv->content_type))) { | ||
| invocation.json_content_type = 1; | ||
| } | ||
| } | ||
|
|
||
| // Get method from Request.Method | ||
| if (!read_go_str("method", | ||
| req, | ||
|
|
@@ -263,31 +305,90 @@ int beyla_uprobe_http2Server_processHeaders(struct pt_regs *ctx) { | |
| return 0; | ||
| } | ||
|
|
||
| static __always_inline void update_traceparent(server_http_func_invocation_t *inv, | ||
| const unsigned char *header_start) { | ||
| decode_go_traceparent(header_start, inv->tp.trace_id, inv->tp.parent_id, &inv->tp.flags); | ||
| bpf_dbg_printk("Found traceparent in header %s", header_start); | ||
| } | ||
|
|
||
| static __always_inline void update_content_type(server_http_func_invocation_t *inv, | ||
| const unsigned char *header_start) { | ||
| __builtin_memset(inv->content_type, 0, sizeof(inv->content_type)); | ||
| __builtin_memcpy(inv->content_type, header_start, sizeof(inv->content_type)); | ||
| bpf_dbg_printk("Found content-type in header %s", inv->content_type); | ||
| } | ||
|
|
||
| static __always_inline void handle_traceparent_header(server_http_func_invocation_t *inv, | ||
| go_addr_key_t *g_key, | ||
| unsigned char *traceparent_start) { | ||
| if (inv) { | ||
| update_traceparent(inv, traceparent_start); | ||
| } else { | ||
| server_http_func_invocation_t minimal_inv = {}; | ||
| update_traceparent(&minimal_inv, traceparent_start); | ||
| bpf_map_update_elem(&ongoing_http_server_requests, g_key, &minimal_inv, BPF_ANY); | ||
| } | ||
| } | ||
|
|
||
| static __always_inline void handle_content_type_header(server_http_func_invocation_t *inv, | ||
| go_addr_key_t *g_key, | ||
| unsigned char *content_type_start) { | ||
| if (inv) { | ||
| update_content_type(inv, content_type_start); | ||
| } else { | ||
| server_http_func_invocation_t minimal_inv = {}; | ||
| update_content_type(&minimal_inv, content_type_start); | ||
| bpf_map_update_elem(&ongoing_http_server_requests, g_key, &minimal_inv, BPF_ANY); | ||
| } | ||
| } | ||
|
|
||
| // Matches the header in the buffer and returns a pointer to the value part of the header. | ||
| static __always_inline unsigned char *match_header( | ||
| const unsigned char *buf, u32 safe_len, const char *header, u32 header_len, u32 value_len) { | ||
| if (safe_len >= header_len + value_len && stricmp((const char *)buf, header, header_len)) { | ||
| return (unsigned char *)(buf + header_len); | ||
| } | ||
| return NULL; | ||
| } | ||
|
|
||
| SEC("uprobe/readContinuedLineSlice") | ||
| int beyla_uprobe_readContinuedLineSliceReturns(struct pt_regs *ctx) { | ||
| bpf_dbg_printk("=== uprobe/proc readContinuedLineSlice returns === "); | ||
|
|
||
| void *goroutine_addr = GOROUTINE_PTR(ctx); | ||
| bpf_dbg_printk("goroutine_addr %lx", goroutine_addr); | ||
| go_addr_key_t g_key = {}; | ||
| go_addr_key_from_id(&g_key, goroutine_addr); | ||
| connection_info_t *existing = bpf_map_lookup_elem(&ongoing_server_connections, &g_key); | ||
| if (!existing) { | ||
| return 0; | ||
| } | ||
|
titaneric marked this conversation as resolved.
|
||
|
|
||
| u64 len = (u64)GO_PARAM2(ctx); | ||
| const unsigned char *buf = (const unsigned char *)GO_PARAM1(ctx); | ||
|
|
||
| if (len >= (W3C_KEY_LENGTH + W3C_VAL_LENGTH + 2)) { | ||
| unsigned char temp[W3C_KEY_LENGTH + W3C_VAL_LENGTH + 2]; | ||
| bpf_probe_read(temp, sizeof(temp), buf); | ||
| bpf_dbg_printk("goroutine_addr %lx", goroutine_addr); | ||
| go_addr_key_t g_key = {}; | ||
| go_addr_key_from_id(&g_key, goroutine_addr); | ||
| unsigned char *temp = temp_header_mem(); | ||
| const u32 safe_len = len > HTTP_HEADER_MAX_LEN ? HTTP_HEADER_MAX_LEN : len; | ||
| if (!temp || bpf_probe_read_user(temp, safe_len, buf) != 0) { | ||
| bpf_dbg_printk("failed to read buffer"); | ||
| return 0; | ||
| }; | ||
|
|
||
| connection_info_t *existing = bpf_map_lookup_elem(&ongoing_server_connections, &g_key); | ||
| if (existing) { | ||
| if (stricmp((const char *)temp, "traceparent: ", W3C_KEY_LENGTH + 2)) { | ||
| server_http_func_invocation_t inv = {}; | ||
| decode_go_traceparent( | ||
| temp + W3C_KEY_LENGTH + 2, inv.tp.trace_id, inv.tp.parent_id, &inv.tp.flags); | ||
| bpf_dbg_printk("Found traceparent in header %s", temp); | ||
| bpf_map_update_elem(&ongoing_http_server_requests, &g_key, &inv, BPF_ANY); | ||
| } | ||
| } | ||
| const u32 w3c_value_start = sizeof(traceparent) - 1; | ||
| const u32 content_type_value_start = sizeof(content_type) - 1; | ||
|
|
||
| server_http_func_invocation_t *inv = bpf_map_lookup_elem(&ongoing_http_server_requests, &g_key); | ||
|
|
||
| unsigned char *traceparent_start = | ||
| match_header(temp, safe_len, traceparent, w3c_value_start, W3C_VAL_LENGTH); | ||
| if (traceparent_start) { | ||
| handle_traceparent_header(inv, &g_key, traceparent_start); | ||
| } | ||
|
|
||
| unsigned char *content_type_start = match_header( | ||
| temp, safe_len, content_type, content_type_value_start, HTTP_CONTENT_TYPE_MAX_LEN); | ||
| if (content_type_start) { | ||
| handle_content_type_header(inv, &g_key, content_type_start); | ||
| } | ||
|
|
||
| return 0; | ||
|
|
@@ -1182,6 +1283,102 @@ int beyla_uprobe_netFdRead(struct pt_regs *ctx) { | |
| return 0; | ||
| } | ||
|
|
||
| SEC("uprobe/bodyRead") | ||
| int beyla_uprobe_bodyRead(struct pt_regs *ctx) { | ||
| void *goroutine_addr = GOROUTINE_PTR(ctx); | ||
| bpf_dbg_printk("=== uprobe/proc body read goroutine === "); | ||
| go_addr_key_t g_key = {}; | ||
| go_addr_key_from_id(&g_key, goroutine_addr); | ||
|
|
||
| // Get the address of the slice struct (p) | ||
| u64 body_addr = (u64)GO_PARAM2(ctx); | ||
|
|
||
| server_http_func_invocation_t *invocation = | ||
| bpf_map_lookup_elem(&ongoing_http_server_requests, &g_key); | ||
| if (!invocation) { | ||
| return 0; | ||
| } | ||
| invocation->body_addr = body_addr; | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| SEC("uprobe/bodyReadRet") | ||
| int beyla_uprobe_bodyReadReturn(struct pt_regs *ctx) { | ||
| bpf_dbg_printk("=== uprobe/proc body read returns goroutine === "); | ||
| void *goroutine_addr = GOROUTINE_PTR(ctx); | ||
| go_addr_key_t g_key = {}; | ||
| go_addr_key_from_id(&g_key, goroutine_addr); | ||
|
|
||
| u64 n = (u64)GO_PARAM1(ctx); | ||
| bpf_dbg_printk("n is %llu", n); | ||
|
|
||
| server_http_func_invocation_t *invocation = | ||
| bpf_map_lookup_elem(&ongoing_http_server_requests, &g_key); | ||
| if (!invocation) { | ||
| return 0; | ||
| } | ||
| if (n <= 0 || !invocation->body_addr) { | ||
| return 0; | ||
| } | ||
| // json_content-type is set in invocation in ServeHTTP | ||
| if (invocation->json_content_type != 1) { | ||
| bpf_dbg_printk("content type is not json, skipping"); | ||
| return 0; | ||
| } | ||
|
|
||
| unsigned char *body_buf = temp_body_mem(); | ||
| if (!body_buf) { | ||
| return 0; | ||
| } | ||
|
|
||
| const u32 safe_len = n > HTTP_BODY_MAX_LEN ? HTTP_BODY_MAX_LEN : n; | ||
| if (!read_go_str_n("http body", (void *)invocation->body_addr, n, body_buf, safe_len)) { | ||
| bpf_dbg_printk("failed to read body, n=%llu, body_addr=%llx", n, invocation->body_addr); | ||
| return 0; | ||
| } | ||
| // bpf_dbg_printk("body is %s", body_buf); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I leave the commented code here fo future debug purpose |
||
| bpf_tail_call(ctx, &jsonrpc_jump_table, k_tail_jsonrpc); | ||
| return 0; | ||
| } | ||
|
|
||
| //k_tail_jsonrpc | ||
| SEC("uprobe/readJsonrpcMethod") | ||
| int beyla_read_jsonrpc_method(struct pt_regs *ctx) { | ||
| bpf_dbg_printk("=== uprobe/proc read jsonrpc method === "); | ||
| void *goroutine_addr = GOROUTINE_PTR(ctx); | ||
| go_addr_key_t g_key = {}; | ||
| go_addr_key_from_id(&g_key, goroutine_addr); | ||
|
|
||
| server_http_func_invocation_t *invocation = | ||
| bpf_map_lookup_elem(&ongoing_http_server_requests, &g_key); | ||
| if (!invocation) { | ||
| bpf_dbg_printk("can't find invocation info for server call"); | ||
| return 0; | ||
| } | ||
|
|
||
| // tail call is guaranteed to run on the same CPU as its caller | ||
| // so we can shared the same buffer via a per-CPU map | ||
| unsigned char *body_buf = temp_body_mem(); | ||
| if (!body_buf) { | ||
| return 0; | ||
| } | ||
| if (is_jsonrpc2_body((const unsigned char *)body_buf, HTTP_BODY_MAX_LEN)) { | ||
| unsigned char method_buf[JSONRPC_METHOD_BUF_SIZE] = {}; | ||
| u32 method_len = extract_jsonrpc2_method( | ||
| (const unsigned char *)body_buf, HTTP_BODY_MAX_LEN, method_buf, sizeof(method_buf)); | ||
| if (method_len > 0) { | ||
| bpf_dbg_printk("JSON-RPC method: %s", method_buf); | ||
| read_go_str_n("JSON-RPC method", | ||
| (void *)method_buf, | ||
| method_len, | ||
| invocation->method, | ||
| sizeof(invocation->method)); | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| SEC("uprobe/connServeRet") | ||
| int beyla_uprobe_connServeRet(struct pt_regs *ctx) { | ||
| bpf_dbg_printk("=== uprobe/proc http conn serve ret === "); | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.