<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Jonas Kunz - Elastic Observability Labs]]></title>
    <description><![CDATA[Trusted security news & research from the team at Elastic.]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Jonas Kunz - Elastic Observability Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad972c1c27dbefc6/6a88d9782904ea5e8511d473/observability-labs-thumbnail.png</url>
      <link>https://www.elastic.co/observability-labs/author/jonas-kunz</link>
    </image>
    <link>https://www.elastic.co/observability-labs/author/jonas-kunz</link>
    <atom:link href="https://www.elastic.co/observability-labs/rss/author/jonas-kunz.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Fri, 18 Sep 2026 19:15:59 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Combining Elastic Universal Profiling with Java APM Services and Traces]]></title>
    <description><![CDATA[Learn how to combine the power of Elastic universal profiling with APM data from Java services to easily pinpoint CPU bottlenecks. Compatible with both OpenTelemetry and the classic Elastic APM Agent.]]></description>
    <content:encoded><![CDATA[<p>In <a href="https://www.elastic.co/observability-labs/blog/continuous-profiling-distributed-tracing-correlation">a previous blog post</a>, we introduced the technical details of how we managed to correlate eBPF profiling data with APM traces.
This time, we'll show you how to get this feature up and running to pinpoint CPU bottlenecks in your Java services! The correlation is supported for both OpenTelemetry and the classic Elastic APM Agent. We'll show you how to enable it for both.</p>
<h2 id="demoapplication">Demo Application</h2>
<p>For this blog post, we’ll be using the <a href="https://github.com/JonasKunz/cpu-burner">cpu-burner demo application</a> to showcase the correlation capabilities of APM, tracing, and profiling in Elastic. This application was built to continuously execute several CPU-intensive tasks:</p>
<ul>
<li>It computes Fibonacci numbers using the naive, recursive algorithm.</li>
<li>It hashes random data with the SHA-2 and SHA-3 hashing algorithms.</li>
<li>It performs numerous large background allocations to stress the garbage collector.</li>
</ul>
<p>The computations of the Fibonacci numbers and the hashing will each be visible as transactions in Elastic: They have been manually instrumented using the OpenTelemetry API.</p>
<h2 id="settingupprofilingandapm">Setting up Profiling and APM</h2>
<p>First, we’ll need to set up the universal profiling host agent on the host where the demo application will run. Starting from version 8.14.0, correlation with APM data is supported and enabled out of the box for the profiler. There is no special configuration needed; we can just follow the <a href="https://www.elastic.co/guide/en/observability/current/profiling-get-started.html">standard setup guide</a>.
Note that at the time of writing, universal profiling only supports Linux.
On Windows, you'll have to use a VM to try the demo.
On macOS, you can use <a href="https://github.com/abiosoft/colima">colima</a> as docker engine and run the profiling host agent and the demo app in container images.</p>
<p>In addition, we’ll need to instrument our demo application with an APM agent. We can either use the <a href="https://github.com/elastic/apm-agent-java">classic Elastic APM agent</a> or the <a href="https://github.com/elastic/elastic-otel-java">Elastic OpenTelemetry Distribution</a>.</p>
<h3 id="usingtheclassicelasticapmagent">Using the Classic Elastic APM Agent</h3>
<p>Starting with version 1.50.0, the classic Elastic APM agent ships with the capability to correlate the traces it captures with the profiling data from universal profiling. We’ll just need to enable it explicitly via the <strong>universal_profiling_integration_enabled</strong> config option. Here is the standard command line for running the demo application with the setting enabled:</p>
<pre><code>curl -o 'elastic-apm-agent.jar' -L 'https://oss.sonatype.org/service/local/artifact/maven/redirect?r=releases&amp;g=co.elastic.apm&amp;a=elastic-apm-agent&amp;v=LATEST'
java -javaagent:elastic-apm-agent.jar \
-Delastic.apm.service_name=cpu-burner-elastic \
-Delastic.apm.secret_token=XXXXX \
-Delastic.apm.server_url=&lt;elastic-apm-server-endpoint&gt; \
-Delastic.apm.application_packages=co.elastic.demo \
-Delastic.apm.universal_profiling_integration_enabled=true \
-jar ./target/cpu-burner.jar
</code></pre>
<h3 id="usingopentelemetry">Using OpenTelemetry</h3>
<p>The feature is also available as an OpenTelemetry SDK extension.
This means you can use it as a plugin for the vanilla OpenTelemetry agent or add it to your OpenTelemetry SDK if you are not using an agent.
In addition, the feature ships by default with the Elastic OpenTelemetry Distribution for Java and can be used via any of the <a href="https://www.elastic.co/observability-labs/blog/elastic-distribution-opentelemetry-java-agent">possible usage methods</a>.
While the extension is currently Elastic-specific, we are already working with the various OpenTelemetry SIGs on standardizing the correlation mechanism, especially now after the <a href="https://www.elastic.co/observability-labs/blog/elastic-profiling-agent-acceptance-opentelemetry">eBPF profiling agent has been contributed</a>.</p>
<p>For this demo, we’ll be using the Elastic OpenTelemetry Distro Java agent to run the extension:</p>
<pre><code>curl -o 'elastic-otel-javaagent.jar' -L 'https://oss.sonatype.org/service/local/artifact/maven/redirect?r=releases&amp;g=co.elastic.otel&amp;a=elastic-otel-javaagent&amp;v=LATEST'
java -javaagent:./elastic-otel-javaagent.jar \
-Dotel.exporter.otlp.endpoint=&lt;elastic-cloud-OTLP-endpoint&gt; \
"-Dotel.exporter.otlp.headers=Authorization=Bearer XXXX" \
-Dotel.service.name=cpu-burner-otel \
-Delastic.otel.universal.profiling.integration.enabled=true \
-jar ./target/cpu-burner.jar
</code></pre>
<p>Here, we explicitly enabled the profiling integration feature via the <strong>elastic.otel.universal.profiling.integration.enabled</strong> property. Note that with an upcoming release of the universal profiling feature, this won’t be necessary anymore! The OpenTelemetry extension will then automatically detect the presence of the profiler and enable the correlation feature based on that.</p>
<p>The demo repository also comes with a Dockerfile, so you can alternatively build and run the app in docker:</p>
<pre><code>docker build -t cpu-burner .
docker run --rm -e OTEL_EXPORTER_OTLP_ENDPOINT=&lt;elastic-cloud-OTLP-endpoint&gt; -e OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer XXXX" cpu-burner
</code></pre>
<p>And that’s it for setup; we are now ready to inspect the correlated profiling data!</p>
<h2 id="analyzingservicecpuusage">Analyzing Service CPU Usage</h2>
<p>The first thing we can do now is head to the “Flamegraph” view in Universal Profiling and inspect flamegraphs filtered on APM services. Without the APM correlation, universal profiling is limited to filtering on infrastructure concepts, such as hosts, containers, and processes.
Below is a screencast showing a flamegraph filtered on the service name of our demo application:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7fc9f1694923ff35/6a7f1be296b5a666c387b8bd/service-profiling.gif" alt="Universal Profiling Flamegraph filtered on the service name of our demo application" /></p>
<p>With this filter applied, we get a flamegraph aggregated over all instances of our service. If that is not desired, we could narrow down the filter, e.g. based on the host or container names. Note that the same service-level flamegraph view is also available on the “Universal Profiling” tab in the APM service UI.</p>
<p>The flamegraphs show exactly how the demo application is spending its CPU time, independently of whether it is covered by instrumentation or not. From left to right, we can first see the time spent in application tasks: We can identify the background allocations not covered by APM transactions as well as the SHA-computation and Fibonacci transactions.
Interestingly, this application logic only covers roughly 60% of the total CPU time! The remaining time is spent mostly in the G1 garbage collector due to the high allocation rate of our application. The flamegraph shows all G1-related activities and the timing of the individual phases of concurrent tasks. We can easily identify those based on the native function names. This is made possible by universal profiling being capable of profiling and symbolizing the JVM’s C++ code in addition to the Java code.</p>
<h2 id="pinpointingtransactionbottlenecks">Pinpointing Transaction Bottlenecks</h2>
<p>While the service-level flamegraph already gives good insights on where our transactions consume the most CPU, this is mainly due to the simplicity of the demo application. In real-world applications, it can be much harder to pinpoint that certain stack frames come mostly from certain transactions. For this reason, the APM agent also correlates CPU profiling data from universal profiling on the transaction level.</p>
<p>We can navigate to the “Universal Profiling” tab on the transaction details page to get per-transaction flamegraphs:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte8b420da4c4d739e/6a7f1be633fa8ae8a1202ba0/navigate-to-transaction-profiles.gif" alt="Navigation to per-transaction profiling flamegraphs" /></p>
<p>For example, let’s have a look at the flamegraph of our transaction computing SHA-2 and SHA-3 hashes of randomly generated data:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2887385ca24655a5/6a7f1be96c6eac770cf145c5/tx-unfiltered.png" alt="Flamegraph for the hashing transaction" /></p>
<p>Interestingly, the flamegraph uncovers some unexpected results: The transactions spend more time computing the random bytes to be hashed rather than on the hashing itself! So if this were a real-world application, a possible optimization could be to use a more performant random number generator.</p>
<p>In addition, we can see that the MessageDigest.update call for computing the hash values fans out into two different code paths: One is a call into the <a href="https://www.bouncycastle.org/">BouncyCastle cryptography library</a>, the other one is a JVM stub routine, meaning that the JIT compiler has inserted special assembly code for a function.</p>
<p>The flamegraph shown in the screenshot displays the aggregated data for all “shaShenanigans” transactions in the given time filter. We can further filter this down using the transaction filter bar at the top. To make the best use of this, the demo application annotates the transactions with the hashing algorithm used via OpenTelemetry attributes:</p>
<pre><code>public static void shaShenanigans(MessageDigest digest) {
    Span span = tracer.spanBuilder("shaShenanigans")
        .setAttribute("algorithm", digest.getAlgorithm())
        .startSpan();
    ...
    span.end()
}
</code></pre>
<p>So, let’s filter our flamegraph based on the used hashing algorithm:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2886c86a7b533c09/6a7f1bec1967ea42e1330b9e/tx-filter-bar.png" alt="Transaction Filter Bar" /></p>
<p>Note that “SHA-256” is the name of the JVM built-in SHA-2 256-bit implementation. This now gives the following flamegraph:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1d5ae1b2fb5017f7/6a7f1bee3ce8e25d9ecf57c8/tx-sha-256.png" alt="Transaction Filter Bar" /></p>
<p>We can see that the BouncyCastle stack frames are gone and MessageDigest.update spends all its time in the JVM stub routines. Therefore, the stub routine is likely hand-crafted assembly from the JVM maintainers for the SHA2 algorithm.</p>
<p>If we instead filter on “SHA3-256”, we get the following result:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb3d9edb17ea1ad0a/6a7f1bf1bd219839697584d5/tx-sha3.png" alt="Transaction Filter Bar" /></p>
<p>Now, as expected, MessageDigest.update spends all its time in the BouncyCastle library for the SHA3 implementation. Note that the hashing here takes up more time in relation to the random data generation, showing that the SHA2 JVM stub routine is significantly faster than the BouncyCastle Java SHA3 implementation.</p>
<p>This filtering is not limited to custom attributes like those shown in this demo. You can filter on any transaction attributes, including latency, HTTP headers, and so on. For example, for typical HTTP applications, it allows analyzing the efficiency of the used JSON serializer based on the payload size.
Note that while it is possible to filter on single transaction instances (e.g. based on trace.id), this is not recommended: To allow continuous profiling in production systems, the profiler by default runs with a low sampling rate of 20hz. This means that for typical real-world applications, this will not yield enough data when looking at a single transaction execution. Instead, we gain insights by monitoring multiple executions of a group of transactions over time and aggregating their samples, for example in a flamegraph.</p>
<h2 id="summary">Summary</h2>
<p>A common reason for applications to degrade is overly high CPU usage. In this blog post, we showed how to combine universal profiling with APM to find the actual root cause in such cases: We explained how to analyze the CPU time using profiling flamegraphs on service and transaction levels.
In addition, we further drilled down into data using custom filters.
We used a simple demo application for this purpose, so go ahead and try it yourself with your own, real-world applications to uncover the actual power of the feature!</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/universal-profiling-with-java-apm-services-traces</link>
    <guid isPermaLink="false">universal-profiling-with-java-apm-services-traces</guid>
    <category><![CDATA[APM]]></category>
    <category><![CDATA[OpenTelemetry]]></category>
    <dc:creator><![CDATA[Jonas Kunz]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc825e711e84ad7a0/6a7f1bf3bdcff00c0ec432d9/blog-header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 20 Jun 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Revealing unknowns in your tracing data with inferred spans in OpenTelemetry]]></title>
    <description><![CDATA[Distributed tracing is essential in understanding complex systems, but it can miss latency issue details. By combining profiling techniques with distributed tracing, Elastic provides the inferred spans feature as an extension for the OTel Java SDK.]]></description>
    <content:encoded><![CDATA[<p>In the complex world of microservices and distributed systems, achieving transparency and understanding the intricacies and inefficiencies of service interactions and request flows has become a paramount challenge. Distributed tracing is essential in understanding distributed systems. But distributed tracing, whether manually applied or auto-instrumented, is usually rather coarse-grained. Hence, distributed tracing covers only a limited fraction of the system and can easily miss parts of the system that are the most useful to trace.</p>
<p>Addressing this gap, Elastic developed the concept of inferred spans as a powerful enhancement to traditional instrumentation-based tracing as an extension for the OpenTelemetry Java SDK/Agent. We are in the process of contributing this back to OpenTelemetry, until then our <a href="https://github.com/elastic/elastic-otel-java/tree/main/inferred-spans">extension</a> can be seamlessly used with the existing OpenTelelemetry Java SDK (as described below).</p>
<p>Inferred spans are designed to augment the visibility provided by instrumentation-based traces, shedding light on latency sources within the application or libraries that were previously uninstrumented. This feature significantly expands the utility of distributed tracing, allowing for a more comprehensive understanding of system behavior and facilitating a deeper dive into performance optimization.</p>
<h2 id="whatisinferredspans">What is inferred spans?</h2>
<p>Inferred spans is an observability technique that combines distributed tracing with profiling techniques to illuminate the darker, unobserved corners of your application — areas where standard instrumentation techniques fall short. The inferred spans feature interweaves information derived from profiling stacktraces with instrumentation-based tracing data, allowing for the generation of new spans based on the insights drawn from profiling data.</p>
<p>This feature proves invaluable when dealing with custom code or third-party libraries that significantly contribute to the request latency but lack built-in or external instrumentation support. Often, identifying or crafting specific instrumentation for these segments can range from challenging to outright unfeasible. Moreover, certain scenarios exist where implementing instrumentation is impractical due to the potential for substantial performance overhead. For instance, instrumenting application locking mechanisms, despite their critical role, is not viable because of their ubiquitous nature and the significant latency overhead the instrumentation can introduce to application requests. Still, ideally, such latency issues would be visible within your distributed traces.</p>
<p>Inferred spans ensures a deeper visibility into your application’s performance dynamics including the above-mentioned scenarios.</p>
<h2 id="inferredspansinaction">Inferred spans in action</h2>
<p>To demonstrate the inferred spans feature we will use the Java implementation of the <a href="https://github.com/elastic/observability-examples/tree/main/Elastiflix/java-favorite">Elastiflix demo application</a>. Elasticflix has an endpoint called favorites that does some Redis calls and also includes an artificial delay. First, we use the plain OpenTelemetry Java Agent to instrument our application:</p>
<pre><code>java -javaagent:/path/to/otel-javaagent-&lt;version&gt;.jar \
-Dotel.service.name=my-service-name \
-Dotel.exporter.otlp.endpoint=https://&lt;our-elastic-apm-endpoint&gt; \
"-Dotel.exporter.otlp.headers=Authorization=Bearer SECRETTOKENHERE" \
-jar my-service-name.jar
</code></pre>
<p>With the OpenTelemetry Java Agent we get out-of-the-box instrumentation for HTTP entry points and calls to Redis for our Elastiflix application. The resulting traces contain spans for the POST /favorites entrypoint, as well as a few short spans for the calls to Redis.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt68a2ac377960ee63/6a7f1b9deab5be0d5220ab16/image2.png" alt="POST /favorites entrypoint" /></p>
<p>As you can see in the trace above, it’s not clear where most of the time is spent within the POST /favorites request.</p>
<p>Let’s see how inferred spans can shed light into these areas. You can use the inferred spans feature either manually with your OpenTelemetry SDK (see section below), package it as a drop-in extension for the upstream OpenTelemetry Java agent, or just use <a href="https://github.com/elastic/elastic-otel-java/tree/main">Elastic’s distribution of the OpenTelemetry Java agent</a> that comes with the inferred spans feature.</p>
<p>For convenience, we just download the <a href="https://mvnrepository.com/artifact/co.elastic.otel/elastic-otel-javaagent/0.0.1">agent jar</a> of the Elastic distribution and extend the configuration to enable the inferred spans feature:</p>
<pre><code>java -javaagent:/path/to/elastic-otel-javaagent-&lt;version&gt;.jar \
-Dotel.service.name=my-service-name \
-Dotel.exporter.otlp.endpoint=https://XX.apm.europe-west3.gcp.cloud.es.io:443 \
"-Dotel.exporter.otlp.headers=Authorization=Bearer SECRETTOKENHERE" \
-Delastic.otel.inferred.spans.enabled=true \
-jar my-service-name.jar
</code></pre>
<p>The only non-standard option here is elastic.otel.inferred.spans.enabled: The inferred spans Feature is currently opt-in and therefore needs to be enabled explicitly. Running the same application with the inferred spans feature enabled yields more comprehensive traces:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1288446f29c1e24e/6a7f1ba096b5a6727c87b8b5/image1.png" alt="more comprehensive traces" /></p>
<p>The inferred-spans (colored blue in the above screenshot) follow the naming pattern Class#method. With that, the inferred spans feature helps us pinpoint the exact methods that contribute the most to the overall latency of the request. Note that the parent-child relationship between the HTTP entry span, the Redis spans, and the inferred spans is reconstructed correctly, resulting in a fully functional trace structure.</p>
<p>Examining the handleDelay method within the Elastiflix application reveals the use of a straightforward sleep statement. Although the sleep method is not CPU-bound, the full duration of this delay is captured as inferred spans. This stems from employing the async-profiler's wall clock time profiling, as opposed to solely relying on CPU profiling. The ability of the inferred spans feature to reflect actual latency, including for I/O operations and other non-CPU-bound tasks, represents a significant advancement. It allows for diagnosing and resolving performance issues that extend beyond CPU limitations, offering a more nuanced view of system behavior.</p>
<h2 id="usinginferredspanswithyourownopentelemetrysdk">Using inferred spans with your own OpenTelemetry SDK</h2>
<p>OpenTelemetry is a highly extensible framework: Elastic embraces this extensibility by also publishing most extensions shipped with our OpenTelemetry Java Distro as standalone-extensions to the <a href="https://github.com/open-telemetry/opentelemetry-java">OpenTelemetry Java SDK</a>.</p>
<p>As a result, if you do not want to use our distro (e.g., because you don’t need or want bytecode instrumentation in your project), you can still use our extensions, such as the extension for the inferred spans feature. All you need to do is set up the <a href="https://opentelemetry.io/docs/languages/java/instrumentation/#initialize-the-sdk">OpenTelemetry SDK in your code</a> and add the inferred spans extension as a dependency:</p>
<pre><code>&lt;dependency&gt;
    &lt;groupId&gt;co.elastic.otel&lt;/groupId&gt;
    &lt;artifactId&gt;inferred-spans&lt;/artifactId&gt;
    &lt;version&gt;{latest version}&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>During your SDK setup, you’ll have to initialize and register the extension:</p>
<pre><code>InferredSpansProcessor inferredSpans = InferredSpansProcessor.builder()
  .samplingInterval(Duration.ofMillis(10)) //the builder offers all config options
  .build();
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
  .addSpanProcessor(inferredSpans)
.addSpanProcessor(BatchSpanProcessor.builder(OtlpGrpcSpanExporter.builder()
    .setEndpoint("https://&lt;your-elastic-apm-endpoint&gt;")
    .addHeader("Authorization", "Bearer &lt;secrettoken&gt;")
    .build()).build())
  .build();
inferredSpans.setTracerProvider(tracerProvider);
</code></pre>
<p>The inferred spans extension seamlessly integrates with the <a href="https://opentelemetry.io/docs/languages/java/instrumentation/#automatic-configuration">OpenTelemetry SDK Autoconfiguration mechanism</a>. By incorporating the OpenTelemetry SDK and its extensions as dependencies within your application code — rather than through an external agent — you gain the flexibility to configure them using the same environment variables or JVM properties. Once the inferred spans extension is included in your classpath, activating it for autoconfigured SDKs becomes straightforward. Simply enable it using the elastic.otel.inferred.spans.enabled property, as previously described, to leverage the full capabilities of this feature with minimal setup.</p>
<h2 id="howdoesinferredspanswork">How does inferred spans work?</h2>
<p>The inferred spans feature leverages the capabilities of collecting wall clock time profiling data of the widely-used <a href="https://github.com/async-profiler/async-profiler">async-profiler</a>, a low-overhead, popular production-time profiler in the Java ecosystem. It then transforms the profiling data into actionable spans as part of the distributed traces. But what mechanism allows for this transformation?</p>
<p>Essentially, the inferred spans extension engages with the lifecycle of span events, specifically when a span is either activated or deactivated across any thread via the <a href="https://opentelemetry.io/docs/specs/otel/context/">OpenTelemetry context</a>. Upon the activation of the initial span within a transaction, the extension commences a session of wall-clock profiling via the async-profiler, set to a predetermined duration. Concurrently, it logs the details of all span activations and deactivations, capturing their respective timestamps and the threads on which they occurred.</p>
<p>Following the completion of the profiling session, the extension processes the profiling data alongside the log of span events. By correlating the data, it reconstructs the inferred spans. It's important to note that, in certain complex scenarios, the correlation may assign an incorrect name to a span. To mitigate this and aid in accurate identification, the extension enriches the inferred spans with stacktrace segments under the code.stacktrace attribute, offering users clarity and insight into the precise methods implicated.</p>
<h2 id="inferredspansvscorrelationoftraceswithprofilingdata">Inferred spans vs. correlation of traces with profiling data</h2>
<p>In the wake of OpenTelemetry's recent <a href="https://opentelemetry.io/blog/2024/profiling/">announcement of the profiling signal</a>, coupled with <a href="https://www.elastic.co/blog/elastic-donation-proposal-to-contribute-profiling-agent-to-opentelemetry">Elastic's commitment to donating the Universal Profiling Agent</a> to OpenTelemetry, you might be wondering about how the inferred spans feature differentiates from merely correlating profiling data with distributed traces using span IDs and trace IDs. Rather than viewing these as competing functionalities, it's more accurate to consider them complementary.</p>
<p>The inferred spans feature and the correlation of tracing with profiling data both employ similar methodologies — melding tracing information with profiling data. However, they each shine in distinct areas. Inferred spans excels at identifying long-running methods that could escape notice with traditional CPU profiling, which is more adept at pinpointing CPU bottlenecks. A unique advantage of inferred spans is its ability to account for I/O time, capturing delays caused by operations like disk access that wouldn't typically be visible in CPU profiling flamegraphs.</p>
<p>However, the inferred spans feature has its limitations, notably in detecting latency issues arising from "death by a thousand cuts" — where a method, although not time-consuming per invocation, significantly impacts total latency due to being called numerous times across a request. While individual calls might not be captured as inferred spans due to their brevity, CPU-bound methods contributing to latency are unveiled through CPU profiling, as flamegraphs display the aggregate CPU time consumed by these methods.</p>
<p>An additional strength of the inferred spans feature lies in its data structure, offering a simplified tracing model that outlines typical parent-child relationships, execution order, and good latency estimates. This structure is achieved by integrating tracing data with span activation/deactivation events and profiling data, facilitating straightforward navigation and troubleshooting of latency issues within individual traces.</p>
<p>Correlating distributed tracing data with profiling data comes with a different set of advantages. Learn more about it in our related blog post, <a href="https://www.elastic.co/blog/continuous-profiling-distributed-tracing-correlation">Beyond the trace: Pinpointing performance culprits with continuous profiling and distributed tracing correlation</a>.</p>
<h2 id="whatabouttheperformanceoverhead">What about the performance overhead?</h2>
<p>As mentioned before, the inferred spans functionality is based on the widely used async-profiler, known for its minimal impact on performance. However, the efficiency of profiling operations is not without its caveats, largely influenced by the specific configurations employed. A pivotal factor in this balancing act is the sampling interval — the longer the interval between samples, the lower the incurred overhead, albeit at the expense of potentially overlooking shorter methods that could be critical to the inferred spans feature discovery process.</p>
<p>Adjusting the probability-based trace sampling presents another way for optimization, directly influencing the overhead. For instance, setting trace sampling to 50% effectively halves the profiling load, making the inferred spans feature even more resource-efficient on average per request. This nuanced approach to tuning ensures that the inferred spans feature can be leveraged in real-world, production environments with a manageable performance footprint. When properly configured, this feature offers a potent, low-overhead solution for enhancing observability and diagnostic capabilities within production applications.</p>
<h2 id="whatsnextforinferredspansandopentelemetry">What’s next for inferred spans and OpenTelemetry?</h2>
<p>This blog post outlined and introduced the inferred spans feature available as an extension for the OpenTelemetry Java SDK and built into the newly introduced Elastic OpenTelemetry Java Distro. Inferred spans allows users to troubleshoot latency issues in areas of code that are not explicitly instrumented while utilizing traditional tracing data.</p>
<p>The feature is currently merely a port of the existing feature from the proprietary Elastic APM Agent. With Elastic embracing OpenTelemetry, we plan on contributing this extension to the upstream OpenTelemetry project. For that, we also plan on migrating the extension to the latest async-profiler 3.x release. <a href="https://github.com/elastic/elastic-otel-java/tree/main/inferred-spans">Try out inferred spans for yourself</a> and see how it can help you diagnose performance problems in your applications.</p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/tracing-data-inferred-spans-opentelemetry</link>
    <guid isPermaLink="false">tracing-data-inferred-spans-opentelemetry</guid>
    <category><![CDATA[OpenTelemetry]]></category>
    <category><![CDATA[APM]]></category>
    <dc:creator><![CDATA[Jonas Kunz,Alexander Wert]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt97993d2b178280f9/6a7f1ba2bdcff03963c432c7/148360-Blog-header-image--Revealing-Unknowns-in-your-Tracing-Data-with-Inferred-Spans-in-OpenTelemetry_V1.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 22 Apr 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Beyond the trace: Pinpointing performance culprits with continuous profiling and distributed tracing correlation]]></title>
    <description><![CDATA[Frustrated by slow traces but unsure where the code bottleneck lies? Elastic Universal Profiling correlates profiling stacktraces with OpenTelemetry (OTel) traces, helping you identify and pinpoint the exact lines of code causing performance issues.]]></description>
    <content:encoded><![CDATA[<p>Observability goes beyond monitoring; it's about truly understanding your system. To achieve this comprehensive view, practitioners need a unified observability solution that natively combines insights from metrics, logs, traces, and crucially, <strong>continuous profiling</strong>. While metrics, logs, and traces offer valuable insights, they can't answer the all-important "why." Continuous profiling signals act as a magnifying glass, providing granular code visibility into the system's hidden complexities. They fill the gap left by other data sources, enabling you to answer critical questions –– why is this trace slow? Where exactly in the code is the bottleneck residing?</p>
<p>Traces provide the "what" and "where" — what happened and where in your system. Continuous profiling refines this understanding by pinpointing the "why" and validating your hypotheses about the "what." Just like a full-body MRI scan, Elastic's whole-system continuous profiling (powered by eBPF) uncovers unknown-unknowns in your system. This includes not just your code, but also third-party libraries and kernel activity triggered by your application transactions. This comprehensive visibility improves your mean-time-to-detection (MTTD) and mean-time-to-recovery (MTTR) KPIs.</p>
<p><em>[Related article:</em> <a href="https://www.elastic.co/blog/observability-profiling-metrics-logs-traces"><em>Why metrics, logs, and traces aren’t enough</em></a><em>]</em></p>
<h2 id="bridgingthedisconnectbetweencontinuousprofilingandoteltraces">Bridging the disconnect between continuous profiling and OTel traces</h2>
<p>Historically, continuous profiling signals have been largely disconnected from OpenTelemetry (OTel) traces. Here's the exciting news: we're bridging this gap! We're introducing native correlation between continuous profiling signals and OTel traces, starting with Java.</p>
<p>Imagine this: You're troubleshooting a performance issue and identify a slow trace. Whole-system continuous profiling steps in, acting like an MRI scan for your entire codebase and system. It narrows down the culprit to the specific lines of code hogging CPU time within the context of your distributed trace. This empowers you to answer the "why" question with minimal effort and confidence, all within the same troubleshooting context.</p>
<p>Furthermore, by correlating continuous profiling with distributed tracing, Elastic Observability customers can measure the cloud cost and CO<sub>2</sub> impact of every code change at the service and transaction level.</p>
<p>This milestone is significant, especially considering the recent developments in the OTel community. With <a href="https://www.cncf.io/blog/2024/03/19/opentelemetry-announces-support-for-profiling/">OTel adopting profiling</a> and Elastic <a href="https://www.elastic.co/blog/elastic-donation-proposal-to-contribute-profiling-agent-to-opentelemetry">donating the industry’s most advanced eBPF-based continuous profiling agent to OTel</a>, we're set for a game-changer in observability — empowering OTel end users with a correlated system visibility that goes from a trace span in the userspace down to the kernel.</p>
<p>Furthermore, achieving this goal, especially with Java, presented significant challenges and demanded serious engineering R&amp;D. This blog post will delve into these challenges, explore the approaches we considered in our proof-of-concepts, and explain how we arrived at a solution that can be easily extended to other OTel language agents. Most importantly, this solution correlates traces with profiling signals at the agent, not in the backend — to ensure optimal query performance and minimal reliance on vendor backend storage architectures.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbeb5f5909292eadb/6a7f0465e02fac7d7d5d61fd/trace.png" alt="Profiling flamegraph for a specific trace.id" /></p>
<h2 id="figuringouttheactiveoteltraceandspan">Figuring out the active OTel trace and span</h2>
<p>The primary technical challenge in this endeavor is essentially the following: whenever the profiler interrupts an OTel instrumented process to capture a stacktrace, we need to be able to efficiently determine the active span and trace ID (per-thread) and the service name (per-process).</p>
<p>For the purpose of this blog, we'll focus on the recently released <a href="https://github.com/elastic/elastic-otel-java">Elastic distribution of the OTel Java instrumentation</a>, but the approach that we ended up with generalizes to any language that can load and call into a native library. So, how do we get our hands on those IDs?</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0b4ed35c8633ca78/6a7f0469e88c656dd800b2a6/service-popout.png" alt="Profiling correlated with service.name, showing  CO2 and cloud cost impact by line of code." /></p>
<p>The OTel Java agent itself keeps track of the active span by storing a stack of spans in the <a href="https://opentelemetry.io/docs/concepts/context-propagation/#context">OpenTelemetryContext</a>, which itself is stored in a <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html">ThreadLocal</a> variable. We originally considered reading these Java structures directly from BPF, but we eventually decided against that approach. There is no documented specification on how ThreadLocals are implemented, and reliably reading and following the JVM's internal data-structures would incur a high maintenance burden. Any minor update to the JVM could change details of the structure layouts. To add to this, we would also have to reverse engineer how each JVM version lays out Java class fields in memory, as well as how all the high-level Java types used in the context objects are actually implemented under the hood. This approach further wouldn't generalize to any non-JVM language and needs to be repeated for any language that we wish to support.</p>
<p>After we had convinced ourselves that reading Java ThreadLocal directly is not the answer, we decided to look for more portable alternatives instead. The option that we ultimately settled with is to load and call into a C++ library that is responsible for making the required information available via a known and defined interface whenever the span changes.</p>
<p>Other than with Java's ThreadLocals, the details on how a native shared library should expose per-process and per-thread data are well-defined in the System V ABI specification and the architecture specific ELF ABI documents.</p>
<h2 id="exposingperprocessinformation">Exposing per-process information</h2>
<p>Exposing per-process data is easy: we simply declare a global variable . . .</p>
<pre><code>void* elastic_tracecorr_process_storage_v1 = nullptr;
</code></pre>
<p>. . . and expose it via ELF symbols. When the user initializes the OTel library to set the service name, we allocate a buffer and populate it with data in a <a href="https://github.com/elastic/apm/blob/149cd3e39a77a58002344270ed2ad35357bdd02d/specs/agents/universal-profiling-integration.md#process-storage-layout">protocol that we defined for this purpose</a>. Once the buffer is fully populated, we update the global pointer to point to the buffer.</p>
<p>On the profiling agent side, we already have code in place that detects libraries and executables loaded into any process's address space. We normally use this mechanism to detect and analyze high-level language interpreters (e.g., libpython, libjvm) when they are loaded, but it also turned out to be a perfect fit to detect the OTel trace correlation library. When the library is detected in a process, we scan the exports, resolve the symbol, and read the per-process information directly from the instrumented process’ memory.</p>
<h2 id="exposingperthreadinformation">Exposing per-thread information</h2>
<p>With the easy part out of the way, let's get to the nitty-gritty portion: exposing per-thread information via thread-local storage (TLS). So, what exactly is TLS, and how does it work? At the most basic level, the idea is to have <strong>one instance of a variable for every thread</strong>. Semantically you can think of it like having a global Map\&lt;ThreadID, T&gt;, although that is not how it is implemented.</p>
<p>On Linux, there are two major options for thread locals: TSD and TLS.</p>
<h2 id="threadspecificdatatsd">Thread-specific data (TSD)</h2>
<p>TSD is the older and probably more commonly known variant. It works by explicitly allocating a key via pthread_key_create — usually during process startup — and passing it to all threads that require access to the thread-local variable. The threads can then pass that key to the pthread_getspecific and pthread_setspecific functions to read and update the variable for the currently running thread.</p>
<p>TSD is simple, but for our purposes it has a range of drawbacks:</p>
<ul>
<li><p>The pthread_key_t structure is opaque and doesn't have a defined layout. Similar to the Java ThreadLocals, the underlying data-structures aren't defined by the ABI documents and different libc implementations (glibc, musl) will handle them differently.</p></li>
<li><p>We cannot call a function like pthread_getspecific from BPF, so we'd have to reverse engineer and reimplement the logic. Logic may change between libc versions, and we’d have to detect the version and support all variants that may come up in the wild.</p></li>
<li><p>TSD performance is not predictable and varies depending on how many thread local variables have been allocated in the process previously. This may not be a huge concern for Java specifically since spans are typically not swapped super rapidly, but it’d likely be quite noticeable for user-mode scheduling languages where the context might need to be swapped at every await point/coroutine yield.</p></li>
</ul>
<p>None of this is strictly prohibitive, but a lot of this is annoying at the very least. Let’s see if we can do better!</p>
<h2 id="threadlocalstoragetls">Thread-local storage (TLS)</h2>
<p>Starting with C11 and C++11, both languages support thread local variables directly via the _Thread_local and thread_local storage specifiers, respectively. Declaring a variable as per-thread is now a matter of simply adding the keyword:</p>
<pre><code>thread_local void* elastic_tracecorr_tls_v1 = nullptr;
</code></pre>
<p>You might assume that the compiler simply inserts calls to the corresponding pthread function calls when variables declared with this are accessed, but this is not actually the case. The reality is surprisingly complicated, and it turns out that there are four different models of TLS that the compiler can choose to generate. For some of those models, there are further multiple dialects that can be used to implement them. The different models and dialects come with various portability versus performance trade-offs. If you are interested in the details, I suggest reading this <a href="https://maskray.me/blog/2021-02-14-all-about-thread-local-storage">blog article</a> that does a great job at explaining them.</p>
<p>The TLS model and dialect are usually chosen by the compiler based on a somewhat opaque and complicated set of architecture-specific rules. Fortunately for us, both gcc and clang allow users to pick a particular one using the -ftls-model and -mtls-dialect arguments. The variant that we ended up picking for our purposes is -ftls-model=global-dynamic and -mtls-dialect=gnu2 (and desc on aarch64).</p>
<p>Let's take a look at the assembly that is being generated when accessing a thread_local variable under these settings. Our function:</p>
<pre><code>void setThreadProfilingCorrelationBuffer(JNIEnv* jniEnv, jobject bytebuffer) {
  if (bytebuffer == nullptr) {
    elastic_tracecorr_tls_v1 = nullptr;
  } else {
    elastic_tracecorr_tls_v1 = jniEnv-&gt;GetDirectBufferAddress(bytebuffer);
  }
}
</code></pre>
<p>Is compiled to the following assembly code:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbe785dc1354e597b/6a7f046b73d9bd7e5929d784/assembly.png" alt="assembly" /></p>
<p>Both possible branches assign a value to our thread-local variable. Let’s focus at the right branch corresponding to the nullptr case to get rid of the noise from the GetDirectBufferAddress function call:</p>
<pre><code>lea   rax, elastic_tracecorr_tls_v1_tlsdesc  ;; Load some pointer into rax.
call  qword ptr [rax]                        ;; Read &amp; call function pointer at rax.
mov   qword ptr fs:[rax], 0                  ;; Assign 0 to the pointer returned by
                                             ;; the function that we just called.
</code></pre>
<p>The fs: portion of the mov instruction is the actual magic bit that makes the memory read per-thread. We’ll get to that later; let’s first look at the mysterious elastic_tracecorr_tls_v1_tlsdesc variable that the compiler emitted here. It’s an instance of the tlsdesc structure that is located somewhere in the .got.plt ELF section. The structure looks like this:</p>
<pre><code>struct tlsdesc {
  // Function pointer used to retrieve the offset
  uint64_t (*resolver)(tlsdesc*);

  // TLS offset -- more on that later.
  uint64_t tp_offset;
}
</code></pre>
<p>The resolver field is initialized with nullptr and tp_offset with a per-executable offset. The first thread-local variable in an executable will usually have offset 0, the next one sizeof(first_var), and so on. At first glance this may appear to be similar to how TSD works, with the call to pthread_getspecific to resolve the actual offset, but there is a crucial difference. When the library is loaded, the resolver field is filled in with the address of __tls_get_addr by the loader (ld.so). __tls_get_addr is a relatively heavy function that allocates a TLS offset that is globally unique between all shared libraries in the process. It then proceeds by updating the tlsdesc structure itself, inserting the global offset and replacing the resolver function with a trivial one:</p>
<pre><code>void* second_stage_resolver(tlsdesc* desc) {
  return tlsdesc-&gt;tp_offset;
}
</code></pre>
<p>In essence, this means that the first access to a tlsdesc based thread-local variable is rather expensive, but all subsequent ones are cheap. We further know that by the time that our C++ library starts publishing per-thread data, it must have gone through the initial resolving process already. Consequently, all that we need to do is to read the final offset from the process's memory and memorize it. We also refresh the offset every now and then to ensure that we really have the final offset, combating the unlikely but possible race condition that we read the offset before it was initialized. We can detect this case by comparing the resolver address against the address of the __tls_get_addr function exported by ld.so.</p>
<h2 id="determiningthetlsoffsetfromanexternalprocess">Determining the TLS offset from an external process</h2>
<p>With that out of the way, the next question that arises is how to actually find the tlsdesc in memory so that we can read the offset. Intuitively one might expect that the dynamic symbol exported on the ELF file points to that descriptor, but that is not actually the case.</p>
<pre><code>$ readelf --wide --dyn-syms elastic-jvmti-linux-x64.so | grep elastic_tracecorr_tls_v1
328: 0000000000000000     8 TLS   GLOBAL DEFAULT   19 elastic_tracecorr_tls_v1
</code></pre>
<p>The dynamic symbol instead contains an offset relative to the start of the .tls ELF section and points to the initial value that libc initializes the TLS value with when it is allocated. So how does ld.so find the tlsdesc to fill in the initial resolver? In addition to the dynamic symbol, the compiler also emits a relocation record for our symbol, and that one actually points to the descriptor structure that we are looking for.</p>
<pre><code>$ readelf --relocs --wide elastic-jvmti-linux-x64.so | grep R_X86_64_TLSDESC
00000000000426e8  0000014800000024 R_X86_64_TLSDESC       0000000000000000
elastic_tracecorr_tls_v1 + 0
</code></pre>
<p>To read the final TLS offset, we thus simply have to:</p>
<ul>
<li><p>Wait for the event notifying us about a new shared library being loaded into a process</p></li>
<li><p>Do some cheap heuristics to detect our C++ library, avoiding the more expensive analysis below from being executed for every unrelated library on the system</p></li>
<li><p>Analyze the library on disk and scan ELF relocations for our per-thread variable to extract the tlsdesc address</p></li>
<li><p>Rebase that address to match where our library was loaded in that particular process</p></li>
<li><p>Read the offset from tlsdesc+8</p></li>
</ul>
<h2 id="determiningthetlsbase">Determining the TLS base</h2>
<p>Now that we have the offset, how do we use that to actually read the data that the library puts there for us? This brings us back to the magic fs: portion of the mov instruction that we discussed earlier. In X86, most memory operands can optionally be supplied with a segment register that influences the address translation.</p>
<p>Segments are an archaic construct from the early days of 16-bit X86 where they were used to extend the address space. Essentially the architecture provides a range of segment registers that can be configured with different base addresses, thus allowing more than 16-bits worth of memory to be accessed. In times of 64-bit processors, this is hardly a concern anymore. In fact, X86-64 aka AMD64 got rid of all but two of those segment registers: fs and gs.</p>
<p>So why keep two of them? It turns out that they are quite useful for the use-case of thread-local data. Since every thread can be configured to have its own base address in these segment registers, we can use it to point to a block of data for this specific thread. That is precisely what libc implementations on Linux are doing with the fs segment. The offset that we snatched from the processes memory earlier is used as an address with the fs segment register, and the CPU automatically adds it to the per-thread base address.</p>
<p>To retrieve the base address pointed to by the fs segment register in the kernel, we need to read its destination from the kernel’s task_struct for the thread that we happened to interrupt with our profiling timer event. Getting the task struct is easy because we are blessed with the bpf_get_current_task BPF helper functions. BPF helpers are pretty much syscalls for BPF programs: we can just ask the Linux kernel to hand us the pointer.</p>
<p>Armed with the task pointer, we now have to read the thread.fsbase (X86-64) or thread.uw.tp_value (aarch64) field to get our desired base address that the user-mode process accesses via fs. This is where things get complicated one last time, at least if we wish to support older kernels without <a href="https://www.kernel.org/doc/html/latest/bpf/btf.html">BTF support</a> (we do!). The <a href="https://github.com/torvalds/linux/blob/259f7d5e2baf87fcbb4fabc46526c9c47fed1914/include/linux/sched.h#L748">task_struct is huge</a> and there are hundreds of fields that can be present or not depending on how the kernel is configured. Being a core primitive of the scheduler, it is also constantly subject to changes between different kernel versions. On modern Linux distributions, the kernel is typically nice enough to tell us the offset via BTF. On older ones, the situation is more complicated. Since hardcoding the offset is clearly not an option if we hope the code to be portable, we instead have to figure out the offset by ourselves.</p>
<p>We do this by consulting /proc/kallsyms, a file with mappings between kernel functions and their addresses, and then using BPF to dump the compiled code of a kernel function that rarely changes and uses the desired offset. We dynamically disassemble and analyze the function and extract the offset directly from the assembly. For X86-64 specifically, we dump the <a href="https://elixir.bootlin.com/linux/v5.9.16/source/arch/x86/kernel/hw_breakpoint.c#L452">aout_dump_debugregs</a> function that accesses thread-&gt;ptrace_bps, which has consistently been 16 bytes away from the fsbase field that we are interested in for all kernels that we have ever looked at.</p>
<h2 id="readingtlsdatafromkernel">Reading TLS data from kernel</h2>
<p>With all the required offsets at our hands, we can now finally do what we set out to do in the first place: use them to enrich our stack traces with the OTel trace and span IDs that our C++ library prepared for us!</p>
<pre><code>void maybe_add_otel_info(Trace* trace) {
  // Did user-mode insert a TLS offset for this process? Read it.
  TraceCorrProcInfo* proc = bpf_map_lookup_elem(&amp;tracecorr_procs, &amp;trace-&gt;pid);

  // No entry -&gt; process doesn't have the C++ library loaded.
  if (!proc) return;

  // Load the fsbase offset from our global configuration map.
  u32 key = 0;
  SystemConfig* syscfg = bpf_map_lookup_elem(&amp;system_config, &amp;key);

  // Read the fsbase offset from the kernel's task struct.
  u8* fsbase;
  u8* task = (u8*)bpf_get_current_task();
  bpf_probe_read_kernel(&amp;fsbase, sizeof(fsbase), task + syscfg-&gt;fsbase_offset);

  // Use the TLS offset to read the **pointer** to our TLS buffer.
  void* corr_buf_ptr;
  bpf_probe_read_user(
    &amp;corr_buf_ptr,
    sizeof(corr_buf_ptr),
    fsbase + proc-&gt;tls_offset
  );

  // Read the information that our library prepared for us.
  TraceCorrelationBuf corr_buf;
  bpf_probe_read_user(&amp;corr_buf, sizeof(corr_buf), corr_buf_ptr);

  // If the library reports that we are currently in a trace, store it into
  // the stack trace that will be reported to our user-land process.
  if (corr_buf.trace_present &amp;&amp; corr_buf.valid) {
    trace-&gt;otel_trace_id.as_int.hi = corr_buf.trace_id.as_int.hi;
    trace-&gt;otel_trace_id.as_int.lo = corr_buf.trace_id.as_int.lo;
    trace-&gt;otel_span_id.as_int = corr_buf.span_id.as_int;
  }
}
</code></pre>
<h2 id="sendingoutthemappings">Sending out the mappings</h2>
<p>From this point on, everything further is pretty simple. The C++ library sets up a unix datagram socket during startup and communicates the socket path to the profiler via the per-process data block. The stacktraces annotated with the OTel trace and span IDs are sent from BPF to our user-mode profiler process via perf event buffers, which in turn sends the mappings between OTel span and trace and stack trace hashes to the C++ library. Our extensions to the OTel instrumentation framework then read those mappings and insert the stack trace hashes into the OTel trace.</p>
<p>This approach has a few major upsides compared to the perhaps more obvious alternative of sending out the OTel span and trace ID with the profiler’s stacktrace records. We want the stacktrace associations to be stored in the trace indices to allow filtering and aggregating stacktraces by the plethora of fields available on OTel traces. If we were to send out the trace IDs via the profiler's gRPC connection instead, we’d have to search for and update the corresponding OTel trace records in the profiling collector to insert the stack trace hashes.</p>
<p>This is not trivial: stacktraces are sent out rather frequently (every 5 seconds, as of writing) and the corresponding OTel trace might not have been sent and stored by the time the corresponding stack traces arrive in our cluster. We’d have to build a kind of delay queue and periodically retry updating the OTel trace documents, introducing avoidable database work and complexity in the collectors. With the approach of sending stacktrace mappings to the OTel instrumented process instead, the need for server-side merging vanishes entirely.</p>
<h2 id="tracecorrelationinaction">Trace correlation in action</h2>
<p>With all the hard work out of the way, let’s take a look at what trace correlation looks like in action!</p>
<div>
    
</div>
<h2 id="futureworksupportingotherlanguages">Future work: Supporting other languages</h2>
<p>We have demonstrated that trace correlation can work nicely for Java, but we have no intention of stopping there. The general approach that we discussed previously should work for any language that can efficiently load and call into our C++ library and doesn’t do user-mode scheduling with coroutines. The problem with user-mode scheduling is that the logical thread can change at any await/yield point, requiring us to update the trace IDs in TLS. Many such coroutine environments like Rust’s Tokio provide the ability to register a callback for whenever the active task is swapped, so they can be supported easily. Other languages, however, do not provide that option.</p>
<p>One prominent example in that category is Go: goroutines are built on user-mode scheduling, but to our knowledge there’s no way to instrument the scheduler. Such languages will need solutions that don’t go via the generic TLS path. For Go specifically, we have already built a prototype that uses pprof labels that are associated with a specific Goroutine, having Go’s scheduler update them for us automatically.</p>
<h2 id="gettingstarted">Getting started</h2>
<p>We hope this blog post has given you an overview of correlating profiling signals to distributed tracing, and its benefits for end-users.</p>
<p>To get started, download the <a href="https://github.com/elastic/elastic-otel-java">Elastic distribution of the OTel agent</a>, which contains the new trace correlation library. Additionally, you will need the latest version of Universal Profiling agent, bundled with <a href="https://www.elastic.co/blog/whats-new-elastic-8-13-0">Elastic Stack version 8.13</a>.</p>
<h2 id="acknowledgment">Acknowledgment</h2>
<p>We appreciate <a href="https://github.com/trask">Trask Stalnaker</a>, maintainer of the OTel Java agent, for his feedback on our approach and for reviewing the early draft of this blog post.</p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/continuous-profiling-distributed-tracing-correlation</link>
    <guid isPermaLink="false">continuous-profiling-distributed-tracing-correlation</guid>
    <category><![CDATA[APM]]></category>
    <category><![CDATA[OpenTelemetry]]></category>
    <dc:creator><![CDATA[Joel Höner,Israel Ogbole,Jonas Kunz]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta319a1a25f2344e3/6a7f046e96b5a6604187b0b3/Under_highway_bridge.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 28 Mar 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic's contribution: Invokedynamic in the OpenTelemetry Java agent]]></title>
    <description><![CDATA[The instrumentation approach in OpenTelemetry's Java Agent comes with some limitations with respect to maintenance and testability. Elastic contributes an invokedynamic-based instrumentation approach that helps overcoming these limitations.]]></description>
    <content:encoded><![CDATA[<p>As the second largest and active Cloud Native Computing Foundation (CNCF) project, <a href="https://opentelemetry.io/">OpenTelemetry</a> is well on its way to becoming the ubiquitous, unified standard and framework for observability. OpenTelemetry owes this success to its comprehensive and feature-rich toolset that allows users to retrieve valuable observability data from their applications with low effort. The OpenTelemetry Java agent is one of the most mature and feature-rich components in OpenTelemetry’s ecosystem. It provides automatic instrumentation for JVM-based applications and comes with a broad coverage of auto-instrumentation modules for popular Java-frameworks and libraries.</p>
<p>The original instrumentation approach used in the OpenTelemetry Java agent left the maintenance and development of auto-instrumentation modules subject to some restrictions. As part of <a href="https://www.elastic.co/blog/transforming-observability-ai-assistant-otel-standardization-continuous-profiling-log-analytics">our reinforced commitment to OpenTelemetry</a>, Elastic® helps evolve and improve OpenTelemetry projects and components. <a href="https://www.elastic.co/blog/ecs-elastic-common-schema-otel-opentelemetry-announcement">Elastic’s contribution of the Elastic Common Schema</a> to OpenTelemetry was an important step for the open-source community. As another step in our commitment to OpenTelemetry, Elastic started contributing to the OpenTelemetry Java agent.</p>
<h2 id="elasticsinvokedynamicbasedinstrumentationapproach">Elastic’s invokedynamic-based instrumentation approach</h2>
<p>To overcome the above-mentioned limitations in developing and maintaining auto-instrumentation modules in the OpenTelemetry Java agent, Elastic started contributing its <a href="https://www.elastic.co/blog/embracing-invokedynamic-to-tame-class-loaders-in-java-agents"><strong>invokedynamic</strong></a><a href="https://www.elastic.co/blog/embracing-invokedynamic-to-tame-class-loaders-in-java-agents">-based instrumentation approach</a> to the OpenTelemetry Java agent in July 2023.</p>
<p>To explain the improvement, you should know that in Java, a common approach to do auto-instrumentation of applications is through utilizing Java agents that do bytecode instrumentation at runtime. <a href="https://bytebuddy.net/#/">Byte Buddy</a> is a popular and widespread utility that helps with bytecode instrumentation without the need to deal with Java’s bytecode directly. Instrumentation logic that collects observability data from the target application’s code lives in so-called <em>advice methods</em>. Byte Buddy provides different ways of hooking these advice methods into the target application’s methods:</p>
<ul>
<li><em>Advice inlining:</em> The advice method’s code is being copied into the instrumented target method.</li>
<li><em>Static advice dispatching:</em> The instrumented target method invokes static advice methods that need to be visible by the instrumented code.</li>
<li><em>Advice dispatching with</em>  <strong>invokedynamic</strong> __:_ The instrumented target method uses the JVM’s <strong>invokedynamic</strong> bytecode instruction to call advice methods that are isolated from the instrumented code.</li>
</ul>
<p>These different approaches are described in great detail in our related blog post on <a href="https://www.elastic.co/blog/embracing-invokedynamic-to-tame-class-loaders-in-java-agents">Elastic’s Java APM agent using invokedynamic</a>. In a nutshell, both approaches, <em>advice inlining</em> and <em>dispatching to static advice methods</em> come with some limitations with respect to writing and maintaining the advice code. So far, the OpenTelemetry Java agent has used <em>advice inlining</em> for its bytecode instrumentation. The resulting limitations on developing instrumentations are <a href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/v1.30.0/docs/contributing/writing-instrumentation-module.md#use-advice-classes-to-write-code-that-will-get-injected-to-the-instrumented-library-classes">documented in corresponding developer guidelines</a>. Among other things, the limitation of not being able to debug advice code is a painful restriction when developing and maintaining instrumentation code.</p>
<p>Elastic’s APM Java agent has been using the <strong>invokedynamic</strong> approach with its benefits for years — field-proven by thousands of customers. To help improve the OpenTelemetry Java agent, Elastic started contributing the <strong>invokedynamic</strong> approach with the goal to simplify and improve the development and maintainability of auto-instrumentation modules. The contribution proposal and the implementation outline is documented in more detail in <a href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/8999">this GitHub issue</a>.</p>
<p>With the new approach in place, Elastic will help migrate existing instrumentations so the OTel Java community can benefit from the <strong>invokedynamic</strong> -based instrumentation approach.</p>
<blockquote>
  <p>Elastic supports OTel natively, and has numerous capabilities to help you analyze your application with OTel. </p>
  <ul>
  <li><a href="https://www.elastic.co/blog/opentelemetry-observability">Native OpenTelemetry support in Elastic Observability</a></li>
  <li><a href="https://www.elastic.co/blog/best-practices-instrumenting-opentelemetry">Best Practices for instrumenting OpenTelemetry</a></li>
  <li><a href="https://www.elastic.co/blog/opentelemetry-observability">Independence with OpenTelemetry on Elastic</a></li>
  </ul>
  <p>Instrumenting with OpenTelemetry:</p>
  <ul>
  <li><a href="https://www.elastic.co/blog/getting-started-opentelemetry-instrumentation-sample-app">Elastiflix application</a>, a guide to instrument different languages with OpenTelemetry (this is the application the team built to highlight <em>all</em> the languages below)</li>
  <li>Python: <a href="https://www.elastic.co/blog/auto-instrumentation-of-python-applications-opentelemetry">Auto-instrumentation</a>, <a href="https://www.elastic.co/blog/manual-instrumentation-of-python-applications-opentelemetry">Manual instrumentation</a></li>
  <li>Java: <a href="https://www.elastic.co/blog/auto-instrumentation-of-java-applications-opentelemetry">Auto-instrumentation</a>, <a href="https://www.elastic.co/blog/manual-instrumentation-of-java-applications-opentelemetry">Manual instrumentation </a></li>
  <li>Node.js: <a href="https://www.elastic.co/blog/auto-instrument-nodejs-applications-opentelemetry">Auto-instrumentation</a>, <a href="https://www.elastic.co/blog/manual-instrumentation-of-nodejs-applications-opentelemetry">Manual instrumentation</a></li>
  <li>.NET: <a href="https://www.elastic.co/blog/auto-instrumentation-of-net-applications-opentelemetry">Auto-instrumentation</a>, <a href="https://www.elastic.co/blog/manual-instrumentation-of-net-applications-opentelemetry">Manual instrumentation</a><br />
  Go: <a href="https://elastic.co/blog/manual-instrumentation-of-go-applications-opentelemetry">Manual instrumentation</a></li>
  </ul>
</blockquote>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/invokedynamic-opentelemetry-java-agent</link>
    <guid isPermaLink="false">invokedynamic-opentelemetry-java-agent</guid>
    <category><![CDATA[OpenTelemetry]]></category>
    <dc:creator><![CDATA[Alexander Wert,Jack Shirazi,Jonas Kunz,Sylvain Juge]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4923c41bb46647d7/6a85c99b18249cfe1d18f79f/24-crystals.jpeg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 19 Oct 2023 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>