{"id":14029997,"url":"https://github.com/open-telemetry/opentelemetry-ebpf-profiler","last_synced_at":"2025-12-15T20:19:17.569Z","repository":{"id":233322920,"uuid":"786905655","full_name":"open-telemetry/opentelemetry-ebpf-profiler","owner":"open-telemetry","description":"The production-scale datacenter profiler (C/C++, Go, Rust, Python, Java, NodeJS, .NET, PHP, Ruby, Perl, ...)","archived":false,"fork":false,"pushed_at":"2025-04-08T17:44:15.000Z","size":16053,"stargazers_count":2704,"open_issues_count":52,"forks_count":307,"subscribers_count":48,"default_branch":"main","last_synced_at":"2025-04-09T10:06:37.455Z","etag":null,"topics":["ebpf","profiler"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/open-telemetry.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":"support/.gitignore","governance":null,"roadmap":null,"authors":"AUTHORS.md","dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-15T14:22:53.000Z","updated_at":"2025-04-09T05:35:49.000Z","dependencies_parsed_at":"2024-04-21T19:53:23.172Z","dependency_job_id":"b8f8c119-4bc1-4e47-8bdc-b1e08fa25260","html_url":"https://github.com/open-telemetry/opentelemetry-ebpf-profiler","commit_stats":{"total_commits":161,"total_committers":21,"mean_commits":7.666666666666667,"dds":0.7267080745341614,"last_synced_commit":"371b4a081ca97a66cf6ee9b38ec40df193ebbb59"},"previous_names":["elastic/otel-profiling-agent"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/open-telemetry%2Fopentelemetry-ebpf-profiler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/open-telemetry%2Fopentelemetry-ebpf-profiler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/open-telemetry%2Fopentelemetry-ebpf-profiler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/open-telemetry%2Fopentelemetry-ebpf-profiler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/open-telemetry","download_url":"https://codeload.github.com/open-telemetry/opentelemetry-ebpf-profiler/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248287828,"owners_count":21078792,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ebpf","profiler"],"created_at":"2024-08-11T22:00:18.743Z","updated_at":"2025-12-15T20:19:17.475Z","avatar_url":"https://github.com/open-telemetry.png","language":"C","funding_links":[],"categories":["C","Go"],"sub_categories":[],"readme":"# Introduction\n\nThis repository implements a whole-system, cross-language profiler for Linux via\neBPF.\n\n## Core features and strengths\n\n- Implements the [experimental OTel profiling\n  signal](https://github.com/open-telemetry/opentelemetry-proto/pull/534)\n- Very low CPU and memory overhead (1% CPU and 250MB memory are our upper limits\n  in testing and the agent typically manages to stay way below that)\n- Support for native C/C++ executables without the need for DWARF debug\n  information (by leveraging `.eh_frame` data as described in\n  [US11604718B1](https://patents.google.com/patent/US11604718B1/en?inventor=thomas+dullien\u0026oq=thomas+dullien))\n- Support profiling of system libraries **without frame pointers** and **without\n  debug symbols on the host**.\n- Support for mixed stacktraces between runtimes - stacktraces go from Kernel\n  space through unmodified system libraries all the way into high-level\n  languages.\n- Support for native code (C/C++, Rust, Zig, Go, etc. without debug symbols on\n  host)\n- Support for a broad set of HLLs (Hotspot JVM, Python, Ruby, PHP, Node.JS, V8,\n  Perl), .NET is in preparation.\n- 100% non-intrusive: there's no need to load agents or libraries into the\n  processes that are being profiled.\n- No need for any reconfiguration, instrumentation or restarts of HLL\n  interpreters and VMs: the agent supports unwinding each of the supported\n  languages in the default configuration.\n- ARM64 support for all unwinders except NodeJS.\n- Support for native `inline frames`, which provide insights into compiler\n  optimizations and offer a higher precision of function call chains.\n\n## Building\n\n## Platform Requirements\nThe agent can be built with the provided make targets. Docker is required for containerized builds, and both amd64 and arm64 architectures are supported.\n\n For **Linux**, the following steps apply:\n  1. Build the agent for your current machine's architecture:\n     ```sh\n     make agent\n     ```\n     Or `make debug-agent` for debug build.\n  2. To cross-compile for a different architecture (e.g. arm64):\n     ```sh\n     make agent TARGET_ARCH=arm64\n     ```\nThe resulting binary will be named \u003cebpf-profiler\u003e in the current directory.\n\n## Other OSes\nSince the profiler is Linux-only, macOS and Windows users need to set up a Linux VM to build and run the agent. Ensure the appropriate architecture is specified if using cross-compilation. Use the same make targets as above after the Linux environment is configured in the VM.\n\n## Supported Linux kernel version\n\n[7ddc23ea](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/commit/7ddc23ea135a2e00fffc17850ab90534e9b63108) is the last commit with support for 4.19. Changes after this commit may require a minimal Linux kernel version of 5.4.\n\n## Alternative Build (Without Docker)\nYou can build the agent without Docker by directly installing the dependencies listed in the Dockerfile. Once dependencies are set up, simply run:\n```sh\nmake\n```\nor\n```sh\nmake debug\n```\nThis will build the profiler natively on your machine.\n\n## Running\n\nYou can start the agent with the following command:\n\n```sh\nsudo ./ebpf-profiler -collection-agent=127.0.0.1:11000 -disable-tls\n```\n\nThe agent comes with a functional but work-in-progress / evolving implementation\nof the recently released OTel profiling [signal](https://github.com/open-telemetry/opentelemetry-proto/pull/534).\n\nThe agent loads the eBPF program and its maps, starts unwinding and reports\ncaptured traces to the backend.\n\n## Agent internals\n\nThe host agent is a Go application that is deployed to all machines customers\nwish to profile. It collects, processes and pushes observed stack traces and\nrelated meta-information to a backend collector.\n\n### Concepts\n\n#### File IDs\n\nA file ID uniquely identifies an executable, kernel or script language source\nfile.\n\nFile IDs for native applications are created by taking the SHA256 checksum of a\nfile's head, tail, and size, then truncating the hash digest to 16 bytes (128\nbits):\n\n```\nInput  ← Concat(File[:4096], File[-4096:], BigEndianUInt64(Len(File)))\nDigest ← SHA256(Input)\nFileID ← Digest[:16]\n```\n\nFile IDs for script and JIT languages are created in an interpreter-specific\nfashion.\n\nFile IDs for Linux kernels are calculated by taking the FNV128 hash of their GNU\nbuild ID.\n\n#### Stack unwinding\n\nStack unwinding is the process of recovering the list of function calls that\nlead execution to the point in the program at which the profiler interrupted it.\n\nHow stacks are unwound varies depending on whether a thread is running native,\nJITed or interpreted code, but the basic idea is always the same: every language\nthat supports arbitrarily nested function calls needs a way to keep track of\nwhich function it needs to return to after the current function completes. Our\nunwinder uses that same information to repeatedly determine the caller until we\nreach the thread's entry point.\n\nIn simplified pseudo-code:\n\n```\npc ← interrupted_process.cpu.pc\nsp ← interrupted_process.cpu.sp\n\nwhile !is_entry_point(pc):\n    file_id, start_addr, interp_type ← file_id_at_pc(pc)\n    push_frame(interp_type, file_id, pc - start_addr)\n    unwinder ← unwinder_for_interp(interp_type)\n    pc, sp ← unwinder.next_frame(pc, sp)\n```\n\n#### Symbolization\n\nSymbolization is the process of assigning source line information to the raw\naddresses extracted during stack unwinding.\n\nFor script and JIT languages that always have symbol information available on\nthe customer machines, the host agent is responsible for symbolizing frames.\n\nFor native code the symbolization occurs in the backend. Stack frames are sent\nas file IDs and the offset within the file and the symbolization service is then\nresponsible for assigning the correct function name, source file and lines in\nthe background. Symbols for open-source software installed from OS package repos\nare pulled in from our global symbolization infrastructure and symbols for\nprivate executables can be manually uploaded by the customer.\n\nThe primary reason for doing native symbolization in the backend is that native\nexecutables in production will often be stripped. Asking the customer to deploy\nsymbols to production would be both wasteful in terms of disk usage and also a\nmajor friction point in initial adoption.\n\n#### Stack trace representation\n\nWe have two major representations for our stack traces.\n\nThe raw trace format produced by our BPF unwinders:\n\nhttps://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/0945fe628da5c4854d55dd95e5dc4b4cf46a3c76/host/host.go#L60-L66\n\nThe final format produced after additional processing in user-land:\n\nhttps://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/0945fe628da5c4854d55dd95e5dc4b4cf46a3c76/libpf/libpf.go#L458-L463\n\nThe two might look rather similar at first glance, but there are some important differences:\n\n- the BPF variant uses truncated 64-bit file IDs to save precious kernel memory\n- for interpreter frames the BPF variant uses the file ID and line number fields to store\n  more or less arbitrary interpreter-specific data that is needed by the user-mode code to\n  conduct symbolization\n\nA third trace representation exists within our network protocol, but it essentially\njust a deduplicated, compressed representation of the user-land trace format.\n\n#### Trace hashing\n\nIn profiling it is common to see the same trace many times. Traces can be up to\n128 entries long, and repeatedly symbolizing and sending the same traces over the\nnetwork would be very wasteful. We use trace hashing to avoid this. Different\nhashing schemes are used for the BPF and user-mode trace representations. Multiple\n64 bit hashes can end up being mapped to the same 128 bit hash, but *not* vice-versa.\n\n**BPF trace hash (64 bit):**\n\n```\nH(kernel_stack_id, frames_user, PID)\n```\n\n**User-land trace hash (128 bit)**\n\n```\nH(frames_user_kernel)\n```\n\n### User-land sub-components\n\n#### Tracer\n\nThe tracer is a central user-land component that loads and attaches our BPF\nprograms to their corresponding BPF probes during startup and then continues to\nserve as the primary event pump for BPF \u003c-\u003e user-land communication. It further\ninstantiates and owns other important subcomponents like the process manager.\n\n#### Trace handler\n\nThe trace handler is responsible for converting traces from the BPF format to\nthe user-space format. It receives raw traces [tracer](#tracer), converts them\nto the user-space format and then sends them on to the [reporter](#reporter).\nThe majority of the conversion logic happens via a call into the process\nmanager's [`ConvertTrace`] function.\n\nSince converting and enriching BPF-format traces is not a cheap operation, the\ntrace handler is also responsible for keeping a cache (mapping) of trace hashes:\nfrom 64bit BPF hash to the user-space 128bit hash.\n\n[`ConvertTrace`]: https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/0945fe628da5c4854d55dd95e5dc4b4cf46a3c76/processmanager/manager.go#L208\n\n#### Reporter\n\nThe reporter receives traces and trace counts in the user-mode format from the\n[trace handler](#trace-handler), converts them to the gRPC representation and\nthen sends them out to a backend collector.\n\nIt also receives additional meta-information (such as [metrics](metrics/metrics.json) and [host metadata](hostmetadata/hostmetadata.json))\nwhich it also converts and sends out to a backend collector over gRPC.\n\nThe reporter does not offer strong guarantees regarding reliability of\nnetwork operations and may drop data at any point, an \"eventual consistency\"\nmodel.\n\n#### Process manager\n\nThe process manager receives process creation/termination events from\n[tracer](#tracer) and is responsible for making available any information to the\nBPF code that it needs to conduct unwinding. It maintains a map of the\nexecutables mapped into each process, loads stack unwinding deltas for native\nmodules and creates interpreter handlers for each memory mapping that belongs to\na supported language interpreter.\n\nDuring trace conversion the process manager is further responsible for routing\nsymbolization requests to the correct interpreter handlers.\n\n#### Interpreter handlers\n\nEach interpreted or JITed language that we support has a corresponding type that\nimplements the interpreter handler interface. It is responsible for:\n\n- detecting the interpreter's version and structure layouts\n- placing information that the corresponding BPF interpreter unwinder needs into BPF maps\n- translating interpreter frames from the BPF format to the user-land format by symbolizing them\n\n#### Stack delta provider\n\nUnwinding the stack of native executables compiled without frame pointers\nrequires stack deltas. These deltas are essentially a mapping from each PC in an\nexecutable to instructions describing how to find the caller and how to adjust\nthe unwinder machine state in preparation of locating the next frame. Typically\nthese instructions consist of a register that is used as a base address and an\noffset (delta) that needs to be added to it -- hence the name. The stack delta\nprovider is responsible for analyzing executables and creating stack deltas for\nthem.\n\nFor most native executables, we rely on the information present in `.eh_frame`.\n`.eh_frame` was originally meant only for C++ exception unwinding, but it has\nsince been repurposed for stack unwinding in general. Even applications written\nin many other native languages like C, Zig or Rust will typically come with\n`.eh_frame`.\n\nOne important exception to this general pattern is Go. As of writing, Go\nexecutables do not come with `.eh_frame` sections unless they are built with CGo\nenabled. Even with CGo the `.eh_frame` section will only contain information for\na small subset of functions that are either written in C/C++ or part of the CGo\nruntime. For Go executables we extract the stack delta information from the\nGo-specific section called `.gopclntab`. In-depth documentation on the format is\navailable in [a separate document](doc/gopclntab.md)).\n\n### BPF components\n\nThe BPF portion of the host agent implements the actual stack unwinding. It uses\nthe eBPF virtual machine to execute our code directly in the Linux kernel. The\ncomponents are implemented in BPF C and live in the\n[`opentelemetry-ebpf-profiler/support/ebpf`](./support/ebpf) directory.\n\n#### Limitations\n\nBPF programs must adhere to various restrictions imposed by the verifier. Many\nof these limitations are significantly relaxed in newer kernel versions, but we\nstill have to stick to the old limits because we wish to continue supporting\nolder kernels.\n\nThe minimum supported Linux kernel versions are\n- 4.19 for amd64/x86_64\n- 5.5 for arm64/aarch64\n\nThe most notable limitations are the following two:\n\n- **4096 instructions per program**\\\n  A single BPF program can consist of a maximum of 4096 instructions, otherwise\n  older kernels will refuse to load it. Since BPF does not allow for loops, they\n  instead need to be unrolled.\n- **32 tail-calls**\\\n  Linux allows BPF programs to do a tail-call to another BPF program. A tail\n  call is essentially a `jmp` into another BPF program, ending execution of the\n  current handler and starting a new one. This allows us to circumvent the 4096\n  instruction limit a bit by doing a tail-call before we run into the limit.\n  There's a maximum of 32 tail calls that a BPF program can do.\n\nThese limitations mean that we generally try to prepare as much work as possible\nin user-land and then only do the minimal work necessary within BPF. We can only\nuse $O(\\log{n})$ algorithms at worst and try to stick with $O(1)$ for most things.\nAll processing that cannot be implemented like this must be delegated to\nuser-land. As a general rule of thumb, anything that needs more than 32\niterations in a loop is out of the question for BPF.\n\n#### Unwinders\n\nUnwinding always begins in [`native_tracer_entry`]. This entry point for our\ntracer starts by reading the register state of the thread that we just\ninterrupted and initializes the [`PerCPURecord`] structure. The per-CPU record\npersists data between tail-calls of the same unwinder invocation. The unwinder's\ncurrent `PC`, `SP` etc. values are initialized from register values.\n\nAfter the initial setup the entry point consults a BPF map that is maintained\nby the user-land portion of the agent to determine which interpreter unwinder\nis responsible for unwinding the code at `PC`. If a record for the memory\nregion is found, we then tail-call to the corresponding interpreter unwinder.\n\nEach interpreter unwinder has their own BPF program. The interpreter unwinders\ntypically have an unrolled main loop where they try to unwind as many frames for\nthat interpreter as they can without going over the instruction limit. After\neach iteration the unwinders will typically check whether the current PC value\nstill belongs to the current unwinder and tail-call to the right unwinder\notherwise.\n\nWhen an unwinder detects that we've reached the last frame in the trace,\nunwinding is terminated with a tail call to [`unwind_stop`]. For most traces\nthis call will happen in the native unwinder, since even JITed languages\nusually call through a few layers of native C/C++ code before entering the VM.\nWe detect the end of a trace by heuristically marking certain functions with\n`PROG_UNWIND_STOP` in the BPF maps prepared by user-land. `unwind_stop` then\nsends the completed BPF trace to user-land.\n\nIf any frame in the trace requires symbolization in user-mode, we additionally\nsend a BPF event to request an expedited read from user-land. For all other\ntraces user-land will simply read and then clear this map on a timer.\n\n[`native_tracer_entry`]: https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/0945fe628da5c4854d55dd95e5dc4b4cf46a3c76/support/ebpf/native_stack_trace.ebpf.c#L875\n[`PerCPURecord`]: https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/0945fe628da5c4854d55dd95e5dc4b4cf46a3c76/support/ebpf/types.h#L576\n[`unwind_stop`]: https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/0945fe628da5c4854d55dd95e5dc4b4cf46a3c76/support/ebpf/interpreter_dispatcher.ebpf.c#L125\n\n#### PID events\n\nThe BPF components are responsible for notifying user-land about new and exiting\nprocesses. An event about a new process is produced when we first interrupt it\nwith the unwinders. Events about exiting processes are created with a\n`sched_process_free` tracepoint. In both cases the BPF code sends a perf event to\nnotify user-land. We also re-report a PID if we detect execution in previously\nunknown memory region to prompt re-scan of the mappings. Finally, the profiler\ncan also profile processes whose main thread exits, leaving other threads running.\n\n### Network protocol\n\nAll collected information is reported to a backend collector via a push-based,\nstateless, one-way gRPC [protocol](https://github.com/open-telemetry/opentelemetry-proto/pull/534).\n\nAll data to be transmitted is stored in bounded FIFO queues (ring buffers). Old\ndata is overwritten when the queues fill up (e.g. due to a lagging or offline\nbackend collector). There is no explicit reliability or redundancy (besides\nretries internal to gRPC) and the assumption is that data will be resent\n(eventually consistent).\n\n### Trace processing pipeline\n\nThe host agent contains an internal pipeline that incrementally processes the\nraw traces that are produced by the BPF unwinders, enriches them with additional\ninformation (e.g. symbols for interpreter frames and container info), deduplicates\nknown traces and combines trace counts that occurred in the same update period.\n\nThe traces produced in BPF start out with the information shown in the following\ndiagram.\n\n\u003cdetails\u003e\n\u003csummary\u003eNote: please read this if you wish to update the diagrams\u003c/summary\u003e\n\nThe diagrams in this section were created via draw.io. The SVGs can be loaded\ninto draw.io for editing. When you're done, make sure to export via\n\u003ckbd\u003eFile\u003c/kbd\u003e -\u003e \u003ckbd\u003eExport As\u003c/kbd\u003e -\u003e \u003ckbd\u003eSVG\u003c/kbd\u003e and then select\na zoom level of 200%. If you simply save the diagram via \u003ckbd\u003eCTRL+S\u003c/kbd\u003e,\nit won't fill the whole width of the documentation page. Also make sure that\n\"Include a copy of my diagram\" remains ticked to keep the diagram editable.\n\n\u003c/details\u003e\n\n![bpf-trace-diagram](doc/bpf-trace.drawio.svg)\n\nOur backend collector expects to receive trace information in a normalized and\nenriched format. This diagram below is relatively close to the data-structures\nthat are actually sent over the network, minus the batching and domain-specific\ndeduplication that we apply prior to sending it out.\n\n![net-trace-diagram](doc/network-trace.drawio.svg)\n\nThe diagram below provides a detailed overview on how the various components of\nthe host agent interact to transform raw traces into the network format. It\nis focused around our data structures and how data flows through them. Dotted\nlines represent indirect interaction with data structures, solid ones correspond\nto code flow. \"UM\" is short for \"user mode\".\n\n![trace-pipe-diagram](doc/trace-pipe.drawio.svg)\n\n### Testing strategy\n\nThe host agent code is tested with three test suites:\n\n- **Go unit tests**\\\n  Functionality of individual functions and types is tested with regular Go unit\n  tests. This works great for the user-land portion of the agent, but is unable\n  to test any of the unwinding logic and BPF interaction.\n- **coredump test suite**\\\n  The coredump test suite (`tools/coredump`) we compile the whole BPF unwinder\n  code into a user-mode executable, then use the information from a coredump to\n  simulate a realistic environment to test the unwinder code in. The coredump\n  suite essentially implements all required BPF helper functions in user-space,\n  reading memory and thread contexts from the coredump. The resulting traces are\n  then compared to a frame list in a JSON file, serving as regression tests.\n- **BPF integration tests**\\\n  A special build of the host agent with the `integration` tag is created that\n  enables specialized test cases that actually load BPF tracers into the kernel.\n  These test cases require root privileges and thus cannot be part of the\n  regular unit test suite. The test cases focus on covering the interaction and\n  communication of BPF with user-mode code, as well as testing that our BPF code\n  passes the BPF verifier. Our CI builds the integration test executable once and\n  then executes it on a wide range of different Linux kernel versions via qemu.\n\n### Probabilistic profiling\n\nProbabilistic profiling allows you to reduce storage costs by collecting a representative\nsample of profiling data. This method decreases storage costs with a visibility trade-off,\nas not all Profiling Host Agents will have profile collection enabled at all times.\n\nProfiling Events linearly correlate with the probabilistic profiling value. The lower the value,\nthe fewer events are collected.\n\n#### Configure probabilistic profiling\n\nTo configure probabilistic profiling, set the `-probabilistic-threshold` and `-probabilistic-interval` options.\n\nSet the `-probabilistic-threshold` option to a unsigned integer between 1 and 99 to enable\n probabilistic profiling. At every probabilistic interval, a random number between 0 and 99 is chosen.\n If the probabilistic threshold that you've set is greater than this random number, the agent collects\n profiles from this system for the duration of the interval. The default value is 100.\n\nSet the `-probabilistic-interval` option to a time duration to define the time interval for which\nprobabilistic profiling is either enabled or disabled. The default value is 1 minute.\n\n#### Example\n\nThe following example shows how to configure the profiling agent with a threshold of 50 and an interval of 2 minutes and 30 seconds:\n```bash\nsudo ./ebpf-profiler -probabilistic-threshold=50 -probabilistic-interval=2m30s\n```\n\n# Legal\n\n## Licensing Information\n\nThis project is licensed under the Apache License 2.0 (Apache-2.0).\n[Apache License 2.0](LICENSE)\n\nThe eBPF source code is licensed under the GPL 2.0 license.\n[GPL 2.0](support/ebpf/LICENSE)\n\n## Licenses of dependencies\n\nTo display a summary of the dependencies' licenses:\n```sh\nmake legal\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopen-telemetry%2Fopentelemetry-ebpf-profiler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopen-telemetry%2Fopentelemetry-ebpf-profiler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopen-telemetry%2Fopentelemetry-ebpf-profiler/lists"}