How can a FUSE userspace filesystem daemon inherit OpenTelemetry trace propagation by reading the OBI trace context map keyed by pid and tid?
Feasible, but only when two independent sides both hold. The kernel fills fuse_in_header.pid for every FUSE request, so a FUSE userspace daemon can always tell which calling thread issued the request. OpenTelemetry's eBPF instrumenter (OBI) already keeps a pinned map, traces_ctx_v1, that holds the trace id and span id of the request a thread is currently serving, keyed by that thread. A FUSE daemon can read that pin and inherit the W3C trace context. But the map is written only when OBI has captured a trace context for that thread and population is enabled; an untracked protocol or an unsupported runtime leaves it empty, and the FUSE lookup misses. The header also carries the thread id, while OBI keys on the process-and-thread pair, so the daemon has to resolve the thread group before it can build the key.
The mechanism
The boundary is between what the kernel gives the daemon and what OBI has already stored:
- FUSE side (kernel). Every request a FUSE daemon reads from
/dev/fuseopens with afuse_in_header, and the kernel fills itspidfield infuse_args_to_reqfromfuse_args.pid, whichfs/fuse/req.csets totask_pid(current)mapped into the mount's pid namespace. So the daemon receives the calling thread's thread id, in the mount's pid namespace, per request. That is a TID, not a process id: the flock path deliberately usestask_tgidinstead, and that contrast is what tells you the generic header pid is thread-scoped. - OBI side (trace context map). OBI pins a
BPF_MAP_TYPE_LRU_HASHnamedtraces_ctx_v1, pinned by name to/sys/fs/bpf/by default, with au64key and a two-field value:trace_id(16 bytes) andspan_id(8 bytes), the W3C Trace Context pair. OBI writes an entry at the moment a server span starts, keyed bybpf_get_current_pid_tgid(), which packs the thread group and the thread id into one 64-bit value. The same map feeds OBI's own log enricher, and it is the public surface an external eBPF program or a FUSE daemon can read. - The glue (daemon). To inherit the context the daemon (a) reads
fuse_in_header.pidto get the calling TID, (b) resolves the thread group id from that TID by reading theTgidfield of the caller's/procstatus page, (c) builds the 8-byte key(tgid << 32) | tid, and (d) does abpf_map_lookup_elemon thetraces_ctx_v1pin to recover the trace id and span id, then seeds or extends the span it creates for the FUSE operation from them.
Verification and debugging path
- Confirm the daemon can see the caller. Read a request header from
/dev/fuseand printfuse_in_header.pid. Compare it to the thread id of the process doing the write; on a multi-threaded caller it should track the specific worker thread, not just the process. - Confirm OBI actually populated the map. Turn on OBI's
populate_trace_contextoption, then read thetraces_ctx_v1pin (for example open/sys/fs/bpf/traces_ctx_v1and runbpf_map_lookup_elemwith a known key). A hit returnstrace_idandspan_id; a miss means no context was captured for that thread. - Build the key the way OBI does. Pack the resolved thread group and thread id into the
u64key before looking up; a TID-only key will not match OBI'sbpf_get_current_pid_tgid()key. - Isolate the two failure modes. A lookup miss with population on points at the OBI side (the thread's runtime or protocol produced no W3C context, or its language is not instrumented); a match that is stale points at the thread-affinity side (the FUSE work ran on a different thread than the one OBI stamped).
The limitation
traces_ctx_v1is opt-in. OBI leaves it off unless something reads it; enablepopulate_trace_contextor your external FUSE reader has nothing to look up.- OBI must have a trace context first. The map holds the context OBI already captured on that thread. An untracked protocol, or a runtime OBI does not instrument (the maintainers flagged C++ as not covered, against Go, Java, Node.js, and Python with asyncio), leaves the entry absent, so the FUSE lookup misses and there is nothing to inherit.
- The key is a thread-group-and-thread pair, but the header only carries the thread. Resolve the thread group from the TID; without it you cannot build the key.
- Thread affinity matters. OBI stamps the context on the thread serving the request. If the FUSE work moves to another thread, that per-thread entry is empty; widening the correlation to the whole thread group is possible but ambiguous when one process serves several concurrent requests.
- The map is
BPF_MAP_TYPE_LRU_HASH, so under load an entry for a busy thread can be evicted; read it promptly after the request lands.
References
- kernel fs/fuse/req.c -
fuse_args.pidis set to the caller thread's TID,task_pid(current)mapped into the mount's pid namespace. - kernel include/uapi/linux/fuse.h - the
fuse_in_header.pidfield a FUSE daemon reads. - OBI bpf/shared/obi_ctx.h - the pinned
traces_ctx_v1map, itsu64key andtrace_id/span_idvalue, and the OTEP that owns the spec. - OpenTelemetry OTEP 4855 - the external trace-context map spec OBI implements.
- W3C Trace Context - the
traceparentlayout (trace_id/span_id) the map carries.
Community discussion today
Two opt-in archive channels (the two CNCF OpenTelemetry instrumentation channels) produced eleven messages this run. The thread this page answers: a practitioner asked how feasible it is to add OpenTelemetry trace propagation to a FUSE program by exporting OBI's trace information through its trace-context map and collecting it in the FUSE daemon using the pid and tid. The archive carried the question and the maintainers' follow-ups - that FUSE is a userspace filesystem that can read the caller's thread, that a thread-group id is reachable through the caller's status page under /proc, that Go and Python with asyncio are instrumented while C++ is not, and that the OBI map is populated only when a trace context already exists on that thread. The other two threads in the window had already been answered by an earlier post, or were a review-status update rather than a question, so this run selected the non-duplicative FUSE and OBI trace-propagation question.
Channel coverage for this run: the two opt-in archives supplied eleven messages. The visible-browser-only sources - the eunomia-bpf and sched-ext Discord servers, the bpf mailing list, and r/eBPF - could not be reviewed this run because no visible-browser session was available, so they are marked not-covered, not quiet.
Continue exploring
Back to index
eBPF Q&A
Daily, source-grounded answers to recurring questions about eBPF, Linux observability, profiling, runtime extension, and security.
Previous
Why does a tracepoint eBPF program attached through the Cilium eBPF Go library fire only on CPU 0 on a new kernel, when the identical program logged system calls system-wide on older distributions?
It is not a bug in the library and not something your code needs to work around by opening one perf event per CPU. link.Tracepoint opens exactly one tracepoint-type perf event and passes pid == -1 with cpu == 0 to
Next
Why do eBPF kprobe string reads truncate long open and stat paths, and how do you read the full path?
Because the string read's destination buffer has to live inside the BPF program's 512-byte stack, a single bpfprobereaduserstr call can capture at most 511 characters of a user string, while the kernel allows a full
- Last updated
- Oct 15, 2026
- First published
- Oct 15, 2026
- Contributors
- Yunwei 123
Was this page helpful?