<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Elastic Observability Labs - Distributed Tracing</title>
        <link>https://www.elastic.co/observability-labs</link>
        <description>Trusted security news &amp; research from the team at Elastic.</description>
        <lastBuildDate>Thu, 20 Aug 2026 14:35:26 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Elastic Observability Labs - Distributed Tracing</title>
            <url>https://www.elastic.co/observability-labs/assets/observability-labs-thumbnail.png</url>
            <link>https://www.elastic.co/observability-labs</link>
        </image>
        <copyright>© 2026. Elasticsearch B.V. All Rights Reserved</copyright>
        <item>
            <title><![CDATA[Bridging the Gap: End-to-End Observability from Cloud Native to Mainframe]]></title>
            <link>https://www.elastic.co/observability-labs/blog/end-to-end-o11y-from-cloud-native-to-mainframe</link>
            <guid isPermaLink="false">end-to-end-o11y-from-cloud-native-to-mainframe</guid>
            <pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Achieving end-to-end observability in hybrid enterprise environments, where modern cloud-native applications interact with critical, yet often opaque, IBM mainframe systems is a challenge. By utilizing IBM Z Observability Connect, which enables OTel output, with Elastic Observability is a solution, transforming your mainframe black box into a fully observable component in your deployment]]></description>
            <content:encoded><![CDATA[<h2>Introduction:</h2>
<p>OpenTelemetry is emerging as the standard for modern observability. As a highly active project within the Cloud Native Computing Foundation (CNCF)—second only to Kubernetes—it has become the monitoring solution of choice for cloud-native applications. OpenTelemetry provides a unified method for collecting traces, metrics, and logs across Kubernetes, microservices, and infrastructure.</p>
<p>However, for many enterprises—especially in banking, insurance, healthcare, and government—the reality is more complex than just “cloud native.” Although most organizations have deployed mobile apps and adopted microservices architectures, much of their critical core processing still relies on IBM mainframe applications. These systems process credit card swipes, financial transactions, patient records, and premium calculations.</p>
<p>This creates a dilemma: while the modern distributed systems of the hybrid environment are well-observed, the critical backend remains a black box.</p>
<h2>The “Broken Trace”</h2>
<p>A common challenge we see with customers involves a request that originates from a modern mobile application. The request hits microservices running on Kubernetes, initiates a service call to the mainframe, and suddenly, visibility stops.</p>
<p>When latency spikes or a transaction fails, Site Reliability Engineers (SREs) are left guessing. Is it the network? The API gateway? Or underlying mainframe applications like CICS? Without a unified, end-to-end view of the services involved—from the frontend Node.js microservices to the backend CICS service—mean time to resolution (MTTR) becomes “mean time to innocence,” with teams simply proving it wasn't their microservice rather than fixing root causes.</p>
<p>We need a unified view where a trace flows seamlessly from a cloud-native frontend (like React) all the way into mainframe transactions.</p>
<h2>IBM Z Observability Connect</h2>
<p>With the recent release of <a href="https://www.ibm.com/docs/en/zapmc/7.1.0?topic=z-observability-connect-overview">Z Observability Connect</a>, IBM has introduced OpenTelemetry-native instrumentation into mainframe applications. This creates a bridge between modern cloud-native services and mainframe transactions.</p>
<p>This means the mainframe is no longer a special case; it acts just like any other microservice in a mesh. It functions as an OpenTelemetry data producer, emitting traces, metrics, and logs to OpenTelemetry-compliant backends like Elastic.</p>
<h2>The Architecture</h2>
<p>The architecture is straightforward:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/end-to-end-o11y-from-cloud-native-to-mainframe/architecture.png" alt="architecture" /></p>
<ul>
<li><strong>The Collector</strong>: <a href="https://docs.google.com/document/d/1-0gDjeM6s63AaQio1j0Cb2Pfodkr6KfSw1-q847Gzes/edit?tab=t.0#heading=h.xa5hqxwq5lps">IBM Z Observability Connect</a> runs on z/OS. It collects logs, metrics, or traces and converts them into the OTLP (OpenTelemetry Protocol) format.</li>
<li><strong>The Processor</strong>: The <a href="https://www.elastic.co/docs/reference/opentelemetry/motlp">Elastic Cloud Managed OTLP Endpoint</a> acts as a gateway collector, providing fully hosted, scalable, and reliable native OTLP ingestion.</li>
<li><strong>The Consumer</strong>: <a href="https://www.elastic.co/docs/solutions/observability/apm">Elastic APM</a> enables OpenTelemetry-native application performance monitoring, making it easy to pinpoint and fix performance problems quickly.</li>
</ul>
<h2>Putting it all together in Kubernetes</h2>
<p>We deploy an OpenTelemetry Collector within our Kubernetes cluster. This collector acts as a specialized gateway. It is configured to receive OTLP traffic directly from IBM Z Observability Connect on the mainframe and forward it securely to our observability backend, Elastic APM, by using the <code>otlp/elastic</code> exporter.</p>
<p>Here is the configuration for the OpenTelemetry Collector. Note the <code>exporters</code> section, which handles the authentication and batched transmission to Elastic:</p>
<pre><code>exporters:
  # Exporter to print the first 5 logs/metrics and then every 1000th
  debug:
    verbosity: detailed
    sampling_initial: 5
    sampling_thereafter: 1000

  # Exporter to send logs and metrics to Elasticsearch Managed OTLP Input
  otlp/elastic:
    endpoint: ${env:ELASTIC_OTLP_ENDPOINT}
    headers:
      Authorization: ApiKey ${env:ELASTIC_API_KEY}
    sending_queue:
      enabled: true
      sizer: bytes
      queue_size: 50000000 # 50MB uncompressed
      block_on_overflow: true
    batch:
      flush_timeout: 1s
      min_size: 1_000_000 # 1MB uncompressed
      max_size: 4_000_000 # 4MB uncompressed

service:
  extensions: [pprof, zpages, health_check]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/elastic, debug]
</code></pre>
<p><em>Note: We strongly recommend using environment variables for your endpoints and API keys to keep your manifest secure.</em></p>
<h2>Why the OTel specification matters</h2>
<p><a href="https://www.elastic.co/docs/reference/opentelemetry/motlp">Elastic’s managed OTLP endpoint</a> and observability solution is built with native OTel support and adheres to the OTel specification and semantic conventions. Once we wired everything up and the data started to flow, we noticed that some of the traces in Elastic APM were not being represented correctly.</p>
<p>Most observability solutions derive the so-called RED metrics (rate, error, and duration) for the most important spans in a trace—i.e., incoming and outgoing spans of each individual service. This allows for an efficient indication of a service’s performance without the need to comb through all of the tracing data to show something as simple as the latency of a service’s endpoint or the error rate on outgoing requests.</p>
<p>For an efficient calculation of such derived metrics for incoming spans on a service, the <a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/0182-otlp-remote-parent.md">OTel community</a> introduced the <code>SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK</code> and <code>SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK</code> flags on the span entities within the OTLP protocol. These flags provide an unambiguous indication of whether an individual span is an entry span and, thus, allow observability backends to efficiently calculate metrics for entry-level spans.</p>
<p>If these flags are set incorrectly for an entry span, the span cannot be recognized as an entry span, and metrics are not derived properly—leading to a broken experience. This is what we initially experienced with the ingested OTel data from the IBM mainframe instrumentation.</p>
<p>In a proprietary world, this might have been a dead end or a months-long troubleshooting exercise. However, since OpenTelemetry is an open standard, we were able to debug the issue rapidly and share our findings with IBM engineers, who quickly developed a fix.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/end-to-end-o11y-from-cloud-native-to-mainframe/service_map.png" alt="service_map" /></p>
<h2>Streamline observability</h2>
<p>We now have end-to-end visibility that spans from modern mobile or web applications deep into the IBM mainframe. This unlocks significant value:</p>
<ul>
<li><strong>Unified Service Maps</strong>: You can visually see the dependency between the cloud-native cart service and the backend inventory system on z/OS.</li>
<li><strong>Single Pane of Glass</strong>: SREs no longer need to switch between modern observability tools and separate mainframe monitoring tools to view service health.</li>
<li><strong>Operational Efficiency</strong>: By eliminating the “blind spot” in the trace, you reduce the time spent on coordinating between cloud and mainframe teams, making issue resolution faster.</li>
</ul>
<h2>Conclusion</h2>
<p>If you are running hybrid workloads, it is time to stop treating your mainframe as a black box. With IBM Z Observability Connect, the Elastic Managed OTLP Endpoint, and Elastic APM, your entire stack can finally speak a single language: OpenTelemetry.</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/end-to-end-o11y-from-cloud-native-to-mainframe/end-to-end-o11y.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Correlate logs, metrics, and traces in one ES|QL query]]></title>
            <link>https://www.elastic.co/observability-labs/blog/esql-subqueries-correlate-logs-metrics-traces</link>
            <guid isPermaLink="false">esql-subqueries-correlate-logs-metrics-traces</guid>
            <pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Walk through four investigations, from CPU saturation to pod memory pressure, each answered by a single query across signal types.]]></description>
            <content:encoded><![CDATA[<p>ES|QL can now filter one observability signal by the live result of a query against another.
<a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-in-subquery"><code>WHERE field IN (subquery)</code></a> is in technical preview in Elastic Stack 9.5 and on Elastic Cloud Serverless, and because these subqueries nest, one query can reach across logs, metrics, and traces at the same time.</p>
<p>The gain is in where the intermediate set lives.
When you ask what the saturated hosts logged, the list of saturated hosts is computed and consumed inside Elasticsearch.
Six host names or 500 trace IDs never land in a clipboard or in an AI agent's context window, and the set is recomputed from current data every time the query runs.</p>
<p>That changes the unit of investigation.
You go from &quot;one slow request stalled on a lock&quot; to &quot;most of the slowest requests did,&quot; and only the second answer tells you which team to page.
It matters more the further apart your signals are: in many observability stacks, logs, metrics, and traces live in three separate systems, each with its own query language, its own time picker, and its own idea of what a host is, so the same question has to be asked two or three times and the answers joined by hand.</p>
<p>In this post we walk through four investigations, each of which is self-contained.
For every one of them, we set out the scenario that started it, the query that answers it, the table it returns, and a note on the difficulties you run into when you try to get the same answer any other way.</p>
<table>
<thead>
<tr>
<th>Pattern</th>
<th>Starts from</th>
<th>Question it answers</th>
</tr>
</thead>
<tbody>
<tr>
<td>Metrics to logs</td>
<td>CPU saturation</td>
<td>Which error patterns show up only on the saturated hosts?</td>
</tr>
<tr>
<td>Logs to metrics</td>
<td>Error logs</td>
<td>Do the erroring hosts look any different from the healthy ones?</td>
</tr>
<tr>
<td>Traces to logs</td>
<td>Slow spans</td>
<td>What did every service log during those specific requests?</td>
</tr>
<tr>
<td>All three signals</td>
<td>Pod memory pressure</td>
<td>Which log lines sit behind the requests that failed under that pressure?</td>
</tr>
</tbody>
</table>
<p>The data was collected with <a href="https://www.elastic.co/docs/reference/opentelemetry">OpenTelemetry</a> and lands in the <code>logs-*.otel-*</code>, <code>traces-*.otel-*</code>, and <code>metrics-*.otel-*</code> data streams, where fields keep their <a href="https://opentelemetry.io/docs/specs/semconv/">semantic convention</a> names rather than being rewritten into another schema.
The correlation pattern works just as well on Elastic Agent integrations, though the queries need translating rather than just renaming: ECS carries log severity as the text field <code>log.level</code> instead of a numeric <code>severity_number</code>, the System integration reports CPU as separate <code>system.cpu.*.pct</code> fields instead of one metric with a state dimension, and APM records durations in microseconds.
All of it lands in the same cluster either way, which is the part the subquery depends on.</p>
<p>In <a href="https://www.elastic.co/docs/explore-analyze/discover">Discover</a>, the time picker already applies the range, so the examples below omit an explicit <code>@timestamp</code> filter.
Outside Discover, add a filter by time yourself, either with literal timestamps in the query or with <code>?_tstart</code> and <code>?_tend</code> in the query and values in the <code>params</code> array of your <code>_query</code> request.
Every result below comes from a one hour window over a synthetic fleet of 300 hosts.</p>
<h2>Metrics to logs: what are the saturated hosts complaining about?</h2>
<p>An infrastructure alert tells you a handful of hosts in a fleet of a few hundred sat above 90% CPU over the last hour.
That tells you which hosts are hot and nothing about why.
The question worth answering is whether those hosts share a failure mode, or whether they are busy for unrelated reasons and the alert is a coincidence.</p>
<pre><code class="language-esql">FROM logs-*.otel-*
| WHERE severity_number &gt;= 17
  AND resource.attributes.host.name IN (
      TS metrics-hostmetrics.otel-*
      | WHERE attributes.state == &quot;idle&quot;
      | STATS idle = AVG(AVG_OVER_TIME(metrics.system.cpu.utilization))
          BY resource.attributes.host.name
      | WHERE idle &lt; 0.1
      | KEEP resource.attributes.host.name
    )
| STATS errors = COUNT(*), hosts = COUNT_DISTINCT(resource.attributes.host.name)
    BY pattern = CATEGORIZE(body.text), service = resource.attributes.service.name
| SORT errors DESC
</code></pre>
<p>The two halves of the query map onto the two halves of the question.
The subquery works out which hosts were saturated, averaging CPU utilization per host and keeping the ones that averaged under 10% idle, which is another way of saying above 90% busy for the window.
The outer query then works out what those hosts were complaining about, pulling their error logs and using <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/grouping-functions/categorize"><code>CATEGORIZE</code></a> to collapse thousands of individual lines into a handful of error classes.</p>
<p>Two choices in there are worth pausing on.</p>
<p>The subquery uses <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code></a> rather than <code>FROM</code> because a host does not report one CPU number.
It reports a separate time series per CPU state, and per logical core as well if your collector is configured to break them out, so the reduction has to happen in two stages.
<a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions/avg_over_time"><code>AVG_OVER_TIME</code></a> collapses each series to a single value first, and the outer <code>AVG</code> then combines those into one figure per host.</p>
<p>Naming that inner function matters more than it looks.
Write <code>AVG(metrics.system.cpu.utilization)</code> on its own and <code>TS</code> supplies <code>LAST_OVER_TIME</code> for you, averaging each series' final sample rather than its average over the window.
In this dataset that one substitution moves a host from 7% idle to 10% idle, which is the difference between appearing in the results and not.
<a href="https://www.elastic.co/observability-labs/blog/esql-ts-command-querying-metrics">Querying metrics with the TS command</a> goes into the two aggregation phases in more depth.</p>
<p>The log filter tests <code>severity_number</code> (17 is the ERROR floor on the OpenTelemetry scale) rather than the severity text, because the numeric scale is fixed by the spec while the text is whatever the emitting library decided to write.
That is not a hypothetical distinction here: the error logs in this cluster carry four different labels, including <code>SEVERE</code> from a Java service.
Matching on the text alone returns 2,754 of checkout's errors and misses the billing service entirely, while the numeric filter returns all 5,910 of them and keeps billing too.</p>
<p>When you run the query in Discover, the result is a short table of error classes, scoped to the saturated hosts:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/esql-subqueries-correlate-logs-metrics-traces/metrics-to-logs-result.jpg" alt="Discover ES|QL results showing error log patterns grouped by CATEGORIZE for hosts above 90% CPU" /></p>
<p>The subquery returned six saturated hosts, and on all six the same service is timing out against an upstream dependency and draining its connection pool.
The <code>hosts</code> column is what lets you set the other two rows aside without opening anything: the certificate errors reach only two of the six, and the gateway declines amount to five lines in an hour.
Neither tracks the cohort the way the checkout patterns do.</p>
<p>Having all three signals in one store already removed the exports from this investigation.
The subquery removes the step after that, and closing that last gap matters more than it sounds.
By the time you have read six host names off a chart and typed them into a log search, the set has moved: a host that crossed the threshold a minute ago is missing from your list, and one that has since recovered is still in it.
Here the host list is derived from current data on every run, so re-running the query during an incident gives you the current cohort.</p>
<p>The stale list is only half of it.
A correlation done by hand exists only in the head of the person who did it, so nobody else can check it, save it, or run it again tomorrow.</p>
<h2>Logs to metrics: do the erroring hosts look different from the healthy ones?</h2>
<p>Filtering metrics by a log-derived host set answers the opposite question.
The payments service is throwing errors on some hosts and not others, and you want to know whether resource pressure explains the split before you start reading deploy history.</p>
<p>That is a comparison, so the query needs both cohorts.
<a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fork"><code>FORK</code></a> runs two branches over the same input, and <code>IN</code> and <code>NOT IN</code> against the same log-derived host set divide the fleet between them.</p>
<pre><code class="language-esql">TS metrics-hostmetrics.otel-*
| WHERE attributes.state == &quot;idle&quot;
| STATS idle = AVG(AVG_OVER_TIME(metrics.system.cpu.utilization))
    BY host = resource.attributes.host.name
| EVAL busy = 1 - idle
| FORK
    ( WHERE host IN (
        FROM logs-*.otel-*
        | WHERE severity_number &gt;= 17
          AND resource.attributes.service.name == &quot;payments&quot;
          AND resource.attributes.host.name IS NOT NULL
        | STATS errors = COUNT(*) BY resource.attributes.host.name
        | KEEP resource.attributes.host.name )
      | EVAL cohort = &quot;logging errors&quot; )
    ( WHERE host NOT IN (
        FROM logs-*.otel-*
        | WHERE severity_number &gt;= 17
          AND resource.attributes.service.name == &quot;payments&quot;
          AND resource.attributes.host.name IS NOT NULL
        | STATS errors = COUNT(*) BY resource.attributes.host.name
        | KEEP resource.attributes.host.name )
      | EVAL cohort = &quot;no errors&quot; )
| STATS hosts = COUNT(*), mean_busy = AVG(busy), busiest_host = MAX(busy)
    BY cohort
</code></pre>
<p>The query reads top to bottom as three stages.
The metrics query runs first and reduces the whole fleet to one busy figure per host.
<code>FORK</code> then splits that fleet in two using the same log query in both branches, separating the hosts that appear in it from the hosts that do not.
The final <code>STATS</code> summarizes each group, so both cohorts come back as two rows of one table, measured the same way over the same window.</p>
<p>This query is longer than the others, and three parts of it are less obvious than they look.</p>
<p>The natural way to label the two cohorts would be <code>EVAL cohort = CASE(host IN (...), &quot;erroring&quot;, &quot;healthy&quot;)</code>, and ES|QL rejects it.
In 9.5 an <code>IN</code> subquery has to be a top-level predicate in a <code>WHERE</code> condition rather than an argument to a scalar function, which is why the split happens at the command level with <code>FORK</code>.</p>
<p>The <code>STATS ... BY</code> inside each subquery looks redundant, since <code>KEEP</code> alone would return the same host names.
It is not: without it, the subquery returns one row per matching log document instead of one row per host, and those rows are all held in memory for the outer query to filter against.
Aggregating first turns millions of rows into a few hundred host names.</p>
<p>The <code>IS NOT NULL</code> filter guards the sharpest edge here, and this dataset is a live example rather than a hypothetical.
<code>NOT IN</code> follows SQL null semantics, so a single null in the subquery result makes the predicate match nothing at all.
Five of the payments error logs in this cluster came through a sidecar that dropped the host name.
Remove that one line from both branches and the query still succeeds, but it returns a single row: the nulls quietly delete the entire 286-host &quot;no errors&quot; cohort, and what is left looks like a perfectly plausible answer to a different question.</p>
<p>Run the query in Discover and you get two rows, one per cohort:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/esql-subqueries-correlate-logs-metrics-traces/cohort-comparison.jpg" alt="Discover ES|QL results comparing mean and peak CPU for hosts logging errors against hosts with no errors" /></p>
<p>CPU does not explain the split, and the numbers say so twice.
The 14 erroring hosts run slightly cooler on average than the 286 quiet ones, and the busiest machine among them averaged 50% over the hour while the quiet cohort contains a host that averaged 95%.
Whatever is failing on those 14, they had headroom the entire time, and deploy history is a better place to spend the next ten minutes.</p>
<p>A negative result like this is worth as much as a positive one, and it is usually the one people skip.
Getting it the long way means running the metrics query twice against two hand-built host lists and lining the numbers up afterwards, which is enough friction that the check often just does not happen.
Both cohorts here come from the same log query in the same execution, over identical time windows, so there is nothing to reconcile and no reason not to check.</p>
<h2>Traces to logs: what did every service log during the slow requests?</h2>
<p>A <a href="https://www.elastic.co/observability-labs/blog/slo-burn-rate-analysis-trace-investigation">service level objective (SLO) burn alert</a> fires on checkout latency.
Tracing gives you the slow requests and their spans, and the next question is what the services involved were writing to their logs while those specific requests were in flight.</p>
<p>The trace ID is the join key, and there are far too many of them to move by hand.</p>
<pre><code class="language-esql">FROM logs-*.otel-*
| WHERE trace_id IN (
      FROM traces-*.otel-*
      | WHERE kind == &quot;Server&quot;
        AND resource.attributes.service.name == &quot;checkout&quot;
        AND name == &quot;POST /api/orders&quot;
        AND duration &gt; 2000000000
      | SORT duration DESC
      | LIMIT 500
      | KEEP trace_id
    )
| STATS lines = COUNT(*), traces = COUNT_DISTINCT(trace_id)
    BY pattern = CATEGORIZE(body.text),
       service = resource.attributes.service.name,
       severity_text
| SORT traces DESC
</code></pre>
<p>The subquery answers &quot;which requests were slow.&quot;
It looks at the inbound request span for the checkout endpoint rather than the client and internal spans beneath it, then keeps the 500 slowest requests over two seconds.
Durations are recorded in nanoseconds, which is why the threshold has so many zeros.</p>
<p>The outer query answers &quot;what got logged while they were running,&quot; gathering every log line that shares one of those trace IDs and grouping them into patterns.</p>
<p>Counting distinct traces per pattern is what makes the output readable.
A log pattern that appears 30,000 times across four traces is one chatty request, while a pattern that shows up in 470 of the 500 slowest traces is a property of being slow.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/esql-subqueries-correlate-logs-metrics-traces/slow-trace-log-patterns.jpg" alt="Discover ES|QL results ranking log patterns by how many of the 500 slowest checkout requests they appear in" /></p>
<p>From the results above, the top row is there by construction and can be set aside: checkout writes one <code>order submitted</code> line per order, so it appears in all 500 traces and says nothing about why these particular 500 were slow.</p>
<p>From the results above, the third row is the answer, and it points at a service nobody was looking at.
A lock wait timeout in inventory, two hops downstream from where the alert fired, shows up in 470 of the 500 slowest requests, and the 947 lines behind those 470 traces mean a good share of them retried more than once.
The payment gateway declines are real failures, and at 19 traces out of 500 they are not what is burning the SLO.</p>
<p>Done by hand, this means opening slow traces one at a time and reading the correlated logs for each, which is tedious at ten traces and nobody's idea of a plan at 500.
When the spans and the logs are held in different systems, every trace you check is a copied ID and a context switch, and the sample size you can afford drops to about three.
Three traces is enough to form a theory and not enough to test one.
Treating the slow requests as a population is what turns &quot;this trace had a lock wait&quot; into &quot;470 of the 500 slowest requests had a lock wait,&quot; and that difference decides whether you page the inventory team.</p>
<h2>All three signals: from pod memory pressure to the log lines behind the failures</h2>
<p><code>IN</code> subqueries nest, so the pattern extends to as many signal types as the question needs.</p>
<p>A node pool starts reporting memory pressure after a rollout.
You want the log lines from the requests that actually failed on the pods under pressure, which means going from metrics to traces to logs without stopping in between.</p>
<pre><code class="language-esql">FROM logs-*.otel-*
| WHERE trace_id IN (
      FROM traces-*.otel-*
      | WHERE kind == &quot;Server&quot;
        AND status.code == &quot;Error&quot;
        AND resource.attributes.k8s.pod.uid IN (
            TS metrics-kubeletstats.otel-*
            | STATS peak = MAX(MAX_OVER_TIME(metrics.k8s.pod.memory_limit_utilization))
                BY resource.attributes.k8s.pod.uid
            | WHERE peak &gt; 0.95
            | KEEP resource.attributes.k8s.pod.uid
          )
      | STATS failures = COUNT(*) BY trace_id
      | SORT failures DESC
      | LIMIT 1000
      | KEEP trace_id
    )
| STATS lines = COUNT(*), traces = COUNT_DISTINCT(trace_id)
    BY pattern = CATEGORIZE(body.text), service = resource.attributes.service.name
| SORT traces DESC
</code></pre>
<p>Reading the query inside out, you can see each layer answering one part of the question.
The innermost subquery identifies the pods whose memory peaked above 95% of their limit, using <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions/max_over_time"><code>MAX_OVER_TIME</code></a> to take each pod's peak rather than its average.
The middle one narrows to the requests that actually failed on those pods and reduces them to at most 1,000 trace IDs.
The outer query then collects the logs for those traces from every service that took part, including services running on pods that were entirely healthy, and that last part turns out to be where the answer is.</p>
<p>The pods are matched on their UID rather than their name, because names repeat across namespaces and restarts.
The query assumes Kubernetes metadata reaches your spans, which is what the collector's <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/k8sattributesprocessor"><code>k8sattributes</code> processor</a> is for; if it does not, the host or container ID works the same way.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/esql-subqueries-correlate-logs-metrics-traces/three-signal-result.jpg" alt="Discover ES|QL results tracing Kubernetes pod memory pressure through failed spans to the log patterns behind them" /></p>
<p>From the results above, the first row sits at exactly 1,000 because that is the subquery's <code>LIMIT</code>, so it describes the size of the sample rather than the size of the incident.
Every trace in that sample carries the cart deadline line, which is the symptom you already knew about when you started.</p>
<p>Look at the second row instead.
<code>product-catalog</code> is rejecting oversized payloads across 946 of the same 1,000 traces, and it never appeared in the pod subquery at all: its pods peaked at 75% of their memory limit, well under the 95% threshold.
The rollout started sending larger payloads, which would account for both cart's memory climb and the failures.
Checkout's retry budget gives out in about half of them, which is how the failure became visible to users.</p>
<p>Filtering logs directly by the pressured pods would have shown you the cart line and hidden the product-catalog one, which is to say it would have confirmed the symptom and buried the cause.
Doing it without subqueries means three queries and two hand-built lists, and the second list is a thousand trace IDs.
That is usually the point at which people stop after the first hop and go with the cart theory.
The reason the second hop is cheap here is that all three signals sit in the same store behind the same query language, so widening from pods to traces to every service in the trace is a clause, not a project.</p>
<h2>Why do ES|QL subqueries matter for AI agents?</h2>
<p>Keeping the intermediate set inside the cluster is convenient for a person and close to essential for an agent querying on your behalf.</p>
<p>Split across two tool calls, the intermediate result has to travel.
A list of 500 trace IDs comes back in a tool response and occupies the model's context, and the agent then has to rewrite every one of them into the next query.
That costs tokens on every hop, and it is where truncation and transcription errors come from.
With a subquery, the intermediate set stays inside Elasticsearch and the agent only ever sees the final table.</p>
<p>The problem compounds when the signals are spread across systems.
An agent then needs credentials, a client, and a working knowledge of the query language for each one, plus the judgment to join results that use different names for the same host.
One store and one query language reduce that to a single skill the agent has to be good at.</p>
<p>One ES|QL string is also a complete description of the correlation, which makes the investigation reproducible: an agent can put the query in its summary, and a human can paste it into Discover and get the same logic evaluated against current data.
A two-call sequence with a hardcoded host list in the middle gives you neither.
The one thing to watch is that an agent calling the <code>_query</code> API has to filter <code>@timestamp</code> itself, since nothing is binding a time picker.</p>
<h2>Four things to know before writing ES|QL IN subqueries</h2>
<p><code>IN</code> subqueries are in technical preview in Elastic Stack 9.5 and on Elastic Cloud Serverless, while <code>TS</code> and <code>FORK</code> have been generally available since 9.4.
<code>CATEGORIZE</code> has been generally available since 9.1 and requires a <a href="https://www.elastic.co/subscriptions">Platinum license</a>; every query above works without it if you group by an existing field instead.</p>
<p>Four things are worth knowing before you write your own:</p>
<ul>
<li>In 9.5 the subquery returns exactly one column, which is what the trailing <code>KEEP</code> does in each example.</li>
<li>Aggregate the subquery down to distinct values with <code>STATS ... BY</code> before returning them.
Its result is materialized for the outer query to filter against, so handing back a few hundred host names instead of a few million rows is both faster and safer.</li>
<li>Filter nulls out of any <code>NOT IN</code> subquery, because SQL null semantics mean one null makes the predicate match nothing.</li>
<li>Subqueries are non-correlated.
They run independently and cannot reference columns from the outer query, so this is a set filter rather than a row-by-row join.
Reach for <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/lookup-join"><code>LOOKUP JOIN</code></a> when you need per-row enrichment, which we covered in <a href="https://www.elastic.co/observability-labs/blog/elastic-esql-join-observability">ES|QL joins for richer observability</a>.</li>
</ul>
<h2>Try ES|QL signal correlation on your own data</h2>
<p>The pattern under all four examples is the same.
You start with a set you can describe in one signal and a question you can only answer in another, and the subquery carries that set across the boundary for you.</p>
<p>The syntax is the smaller part of what makes that work.
It works because logs, metrics, and traces sit in <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-columnar-metrics-engine-30x-faster-prometheus">one store behind one query engine</a>, under field names they kept on the way in, so crossing from one signal to another is a clause in a query rather than an integration to build and maintain.
Where that is not true, the same four investigations turn into a sequence of exports, translations, and manual joins, and that costs more than slower answers.
It quietly shrinks the number of questions anyone is willing to ask, and the negative results are the first to go.</p>
<p>Each pattern here replaces two or three queries with one, and no host list or trace ID list has to move between them.
Fewer steps mean fewer places to be wrong, and a correlation you can save as a single string and hand to someone else.</p>
<p>To try it:</p>
<ol>
<li>Open an <a href="https://cloud.elastic.co/serverless-registration?onboarding_token=observability">Observability project on Elastic Cloud Serverless</a>, or upgrade to Elastic Stack 9.5.</li>
<li>Send data with the <a href="https://www.elastic.co/docs/reference/opentelemetry">Elastic Distributions of OpenTelemetry</a>, or point an existing collector at Elasticsearch.</li>
<li>In <strong>Discover</strong>, switch to ES|QL and start from the metrics to logs query above, swapping in your own data streams and thresholds.</li>
<li>Read the <a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-in-subquery"><code>IN</code> subquery reference</a> for the full set of commands you can use inside a subquery.</li>
</ol>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/esql-subqueries-correlate-logs-metrics-traces/header.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[How Elastic cut OpenTelemetry tail sampling memory by 65% with disk-backed trace storage]]></title>
            <link>https://www.elastic.co/observability-labs/blog/tail-sampling-memory-opentelemetry</link>
            <guid isPermaLink="false">tail-sampling-memory-opentelemetry</guid>
            <pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic contributed two features upstream to the OTel Collector's tail sampling processor. The span-ingest strategy lets sampling decisions happen earlier, and Pebble tail storage moves trace buffering to disk. It costs more CPU, but operators can raise decision_wait and num_traces without OOM kills.]]></description>
            <content:encoded><![CDATA[<p>Elastic contributed two upstream improvements to the OpenTelemetry Collector's tail sampling processor (<a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor"><code>tailsamplingprocessor</code></a>) that cut memory usage by up to 65%.
<code>sampling_strategy: span-ingest</code> lets sampling decisions happen at ingest time, releasing traces before <code>decision_wait</code> elapses.
<a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/tailstorage/pebbletailstorageextension"><code>pebbletailstorageextension</code></a> moves trace buffering to a Pebble LSM database on disk, so storage scales with disk capacity instead of RAM. That means operators can increase <code>decision_wait</code> and <code>num_traces</code> without OOM kills. The cost is roughly 2x CPU.</p>
<h2>What is Tail Sampling?</h2>
<p>Distributed tracing is useful for debugging, but at production scale it comes with processing overhead and storage costs, at which point sampling becomes a natural way to maintain the value of tracing while keeping costs under control. Tail-based sampling, or tail sampling, is a technique that makes a sampling decision conditionally at a later stage, so that high-value traces like errors or slow transactions are more likely to be sampled. The opposite is head sampling, which makes the decision at the start of a trace, before any such information is available.</p>
<h2>How does the tail sampling processor work?</h2>
<p>The tail sampling processor buffers 100% of incoming traces (or spans, used interchangeably), then forwards the sampled subset after applying the sampling policies.
Buffering is a major source of memory usage, and it scales proportionally to the volume of spans, a well known pain point in the community.</p>
<p>Memory usage is bounded by configuration parameters like <code>decision_wait</code> and <code>num_traces</code>.
Setting <code>decision_wait</code> to 1 minute means a sampling decision is made for a trace after 1 minute, during which all spans for that trace are expected to have arrived.
If a trace is slower than 1 minute, the decision is made with some spans missing.</p>
<p>As a side note, scaling out the tail sampling setup involves using the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/loadbalancingexporter"><code>loadbalancingexporter</code></a> to satisfy the requirement that all spans for a trace must be routed to the same collector.
This introduces some operational complexity and potentially data loss during collector restarts.
But this post focuses on the memory usage of a single tail sampling processor instance, regardless of horizontal scaling.</p>
<h2>Why does tail sampling cause memory pressure?</h2>
<p>These parameters introduce a tradeoff between data loss and memory usage, and they require assumptions about the shape of traces: how slow they can be, how many spans they contain, how large each span is. These assumptions can become stale as instrumentation evolves.</p>
<p>How much data loss is acceptable to limit memory usage, and can the tradeoff be improved? The following two contributions aim to give operators more flexibility.</p>
<h2>How span-ingest reduces tail sampling memory by releasing spans early</h2>
<p><code>sampling_strategy</code> is a new configuration option added to the tail sampling processor in <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/releases/tag/v0.149.0"><code>v0.149.0</code></a>.</p>
<p><code>sampling_strategy</code> defaults to <code>trace-complete</code>, which matches the original behavior: sampling policies are only evaluated when <code>decision_wait</code> has elapsed, at which point the trace is considered complete.
(There is a similar config, <code>decision_wait_after_root_received</code>, for optimization, but it is excluded from this discussion for simplicity.)
This means all spans are buffered in memory for roughly <code>decision_wait</code> before being released, regardless of whether a decision could have been made earlier.
For example, health check spans that should always be dropped are still held in memory until policy evaluation time.</p>
<p>Alternatively, <code>sampling_strategy</code> can be set to <code>span-ingest</code>, where spans are evaluated individually at ingest time.
This allows terminal decisions, specifically <code>drop</code> or <code>sampled</code>, to be made earlier, freeing memory by dropping or exporting all spans buffered so far for that trace before <code>decision_wait</code> elapses.
In the health check example, a policy can be configured to drop the entire trace as soon as the root span belongs to a health check.
It is worth noting that an <code>unsampled</code> decision, unlike an explicit <code>drop</code>, is not terminal, as it can be overruled by a <code>sampled</code> or <code>drop</code> decision from another span in the same trace, so <code>unsampled</code> traces cannot be released early.</p>
<p>Switching from <code>trace-complete</code> to <code>span-ingest</code> will require policy adjustments, as policies can no longer assume all spans are available at evaluation time.
Moreover, not all policy types are supported with the <code>span-ingest</code> strategy.</p>
<h2>Disk-backed tail sampling storage with Pebble</h2>
<p>Even with <code>span-ingest</code>, all spans are still buffered in memory.
As <code>decision_wait</code> is increased to accommodate slow traces and <code>num_traces</code> is increased to limit data loss, the collector will eventually hit its memory limit and get OOM killed, resulting in further data loss.</p>
<p>What if traces were buffered on disk instead, where there is an order of magnitude more capacity?
The main drawback is performance: disk throughput and latency, even with SSDs, are at least an order of magnitude slower than memory, so disk writes need to be efficient.
For this reason, <a href="https://github.com/cockroachdb/pebble"><code>Pebble</code></a>, an LSM database, was chosen as the storage backend for its fast write performance.
Read performance is less of a concern, as reads only happen for the sampled subset of traces when <code>sampling_strategy</code> is set to <code>span-ingest</code>.</p>
<p>The implementation introduces a <code>TailStorage</code> interface for trace storage operations, and a new <code>tail_storage</code> option in <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/releases/tag/v0.150.0"><code>v0.150.0</code></a> (behind feature gate <code>processor.tailsamplingprocessor.tailstorageextension</code>) to configure the storage backend.
The default in-memory behavior is unchanged, but it is now possible to swap in a different storage backend, like the new <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/tailstorage/pebbletailstorageextension"><code>pebbletailstorageextension</code></a> contributed to Collector Contrib.</p>
<h2>Tail sampling memory benchmarks: trace-complete vs span-ingest with Pebble</h2>
<h3>Benchmark setup: OpenTelemetry Demo with fan-out collectors</h3>
<p>The following benchmarks were produced by running <a href="https://github.com/open-telemetry/opentelemetry-demo"><code>OpenTelemetry Demo</code></a> with increased load against a pipe collector, which receives all spans and fans them out to two identical collectors under observation (<code>CUO-A</code> and <code>CUO-B</code>), differing only in their tail sampling configuration.
Measurements include pipe collector throughput, spans received, spans sent (sampled), CPU usage, and memory usage.</p>
<h3>Benchmark setup diagram</h3>
<pre><code class="language-text">                      demo ns
     +----------------------------------------+
     |  opentelemetry-demo                     |
     |    loadgenerator (locust)               |
     |    services: frontend, cart, ...        |
     |    demo-collector                       |
     +----------------------------------------+
                          |  OTLP/gRPC
                          v
                     chamber ns
     +----------------------------------------+
     |             pipe-collector             |
     |         receive once, fan out          |
     |      exporters: [otlp/a, otlp/b]       |
     +----------------------------------------+
              | OTLP                  | OTLP
              v                       v
     +----------------+      +----------------+
     |     CUO-A      |      |     CUO-B      |
     | tail_sampling  |      | tail_sampling  |
     |   (config A)   |      |   (config B)   |
     +----------------+      +----------------+
</code></pre>
<h3>Tail sampling processor configurations</h3>
<h4>CUO-A</h4>
<pre><code class="language-yaml">config:
  processors:
    tail_sampling:
      sampling_strategy: trace-complete
      decision_wait: 5m
      num_traces: 5000000
      block_on_overflow: true
      decision_cache:
        sampled_cache_size: 10000
        non_sampled_cache_size: 200000
      policies:
        - name: root_1pct
          type: and
          and:
            and_sub_policy:
              - name: root_span_only
                type: ottl_condition
                ottl_condition:
                  error_mode: ignore
                  span:
                    - &quot;IsRootSpan()&quot;
              - name: root_probabilistic
                type: probabilistic
                probabilistic:
                  sampling_percentage: 1.0
</code></pre>
<h4>CUO-B</h4>
<p><code>CUO-B</code> uses the same tail sampling processor configuration as <code>CUO-A</code>, except it sets <code>sampling_strategy: span-ingest</code> and <code>tail_storage: pebble_tail_storage/main</code>, along with its corresponding <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/tailstorage/pebbletailstorageextension"><code>pebbletailstorageextension</code></a> configuration.</p>
<pre><code class="language-yaml">extensions:
  pebble_tail_storage/main:
    directory: /var/lib/otelcol/pebble
</code></pre>
<h3>Memory, CPU and throughput results</h3>
<p>The following tables compare trace-complete (CUO-A) against span-ingest with Pebble disk storage (CUO-B) across memory, CPU and throughput.
The process RSS, Go heap allocation, and per-process CPU measurements come from OpenTelemetry Collector internal process and runtime metrics, while container working set and container CPU come from Kubernetes cgroup metrics scraped by kubelet/cAdvisor.</p>
<h4>Memory (peak over the window)</h4>
<table>
<thead>
<tr>
<th>Metric</th>
<th align="right">cuo-a</th>
<th align="right">cuo-b</th>
<th align="right">Δ (B vs A)</th>
</tr>
</thead>
<tbody>
<tr>
<td>process RSS</td>
<td align="right">916.4 MiB</td>
<td align="right">442.7 MiB</td>
<td align="right">-51.7%</td>
</tr>
<tr>
<td>Go heap alloc</td>
<td align="right">699.3 MiB</td>
<td align="right">241.7 MiB</td>
<td align="right">-65.4%</td>
</tr>
<tr>
<td>container working set</td>
<td align="right">763.0 MiB</td>
<td align="right">282.9 MiB</td>
<td align="right">-62.9%</td>
</tr>
</tbody>
</table>
<h4>CPU (total over the window)</h4>
<table>
<thead>
<tr>
<th>Metric</th>
<th align="right">cuo-a</th>
<th align="right">cuo-b</th>
<th align="right">Δ (B vs A)</th>
</tr>
</thead>
<tbody>
<tr>
<td>per-process CPU</td>
<td align="right">11.9 core-s</td>
<td align="right">22.7 core-s</td>
<td align="right">+90.7%</td>
</tr>
<tr>
<td>container CPU</td>
<td align="right">11.9 core-s</td>
<td align="right">22.7 core-s</td>
<td align="right">+90.1%</td>
</tr>
</tbody>
</table>
<h4>Throughput (total over the window)</h4>
<table>
<thead>
<tr>
<th>Metric</th>
<th align="right">cuo-a</th>
<th align="right">cuo-b</th>
<th align="right">Δ (B vs A)</th>
</tr>
</thead>
<tbody>
<tr>
<td>spans received</td>
<td align="right">257,804</td>
<td align="right">257,804</td>
<td align="right">0.0%</td>
</tr>
<tr>
<td>spans sent</td>
<td align="right">2,477</td>
<td align="right">2,477</td>
<td align="right">0.0%</td>
</tr>
</tbody>
</table>
<h4>Tail sampling</h4>
<table>
<thead>
<tr>
<th>Metric</th>
<th align="right">cuo-a</th>
<th align="right">cuo-b</th>
<th align="right">Δ (B vs A)</th>
</tr>
</thead>
<tbody>
<tr>
<td>traces in memory peak</td>
<td align="right">29,284</td>
<td align="right">29,245</td>
<td align="right">-0.1%</td>
</tr>
<tr>
<td>traces sampled by root_1pct policy</td>
<td align="right">496</td>
<td align="right">496</td>
<td align="right">0.0%</td>
</tr>
</tbody>
</table>
<ul>
<li><code>cuo-a</code> = <code>trace-complete</code>, <code>cuo-b</code> = <code>span-ingest-pebble</code></li>
<li>Window: 15m 39s (<code>t+0:00</code> start, <code>t+10:06</code> drain start, <code>t+15:39</code> drain end)</li>
</ul>
<p>The results show a significant memory reduction when using <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/tailstorage/pebbletailstorageextension"><code>pebbletailstorageextension</code></a> with <code>span-ingest</code>, at the cost of increased CPU usage from event serialization and database overhead.</p>
<h2>What's next for OpenTelemetry tail sampling</h2>
<p>Both <code>sampling_strategy</code> and <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/tailstorage/pebbletailstorageextension"><code>pebbletailstorageextension</code></a> are still in their early stages at the time of writing.
Feedback and contributions are welcome in the OpenTelemetry Collector Contrib repo.
Stay tuned for more improvements.</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/tail-sampling-memory-opentelemetry/header.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Trace your Azure Function application with Elastic Observability]]></title>
            <link>https://www.elastic.co/observability-labs/blog/trace-azure-function-application-observability</link>
            <guid isPermaLink="false">trace-azure-function-application-observability</guid>
            <pubDate>Tue, 16 May 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Serverless applications deployed on Azure Functions are growing in usage. This blog shows how to deploy a serverless application on Azure functions with Elastic Agent and use Elastic's APM capability to manage and troubleshoot issues.]]></description>
            <content:encoded><![CDATA[<p>Adoption of Azure Functions in cloud-native applications on Microsoft Azure has been increasing exponentially over the last few years. Serverless functions, such as the Azure Functions, provide a high level of abstraction from the underlying infrastructure and orchestration, given these tasks are managed by the cloud provider. Software development teams can then focus on the implementation of business and application logic. Some additional benefits include billing for serverless functions based on the actual compute and memory resources consumed, along with automatic on-demand scaling.</p>
<p>While the benefits of using serverless functions are manifold, it is also necessary to make them observable in the wider end-to-end microservices architecture context.</p>
<h2>Elastic Observability (APM) for Azure Functions: The architecture</h2>
<p><a href="https://www.elastic.co/blog/whats-new-elastic-observability-8-7-0">Elastic Observability 8.7</a> introduced distributed tracing for Microsoft Azure Functions — available for the Elastic APM Agents for .NET, Node.js, and Python. Auto-instrumentation of HTTP requests is supported out-of-the-box, enabling the detection of performance bottlenecks and sources of errors.</p>
<p>The key components of the solution for observing Azure Functions are:</p>
<ol>
<li>The Elastic APM Agent for the relevant language</li>
<li>Elastic Observability</li>
</ol>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/blog-elastic-azure-function.png" alt="azure function" /></p>
<p>The APM server validates and processes incoming events from individual APM Agents and transforms them into Elasticsearch documents. The APM Agent provides auto-instrumentation capabilities for the application being observed. The Node.js APM Agent can trace function invocations in an Azure Functions app.</p>
<h2>Setting up Elastic APM for Azure Functions</h2>
<p>To demonstrate the setup and usage of Elastic APM, we will use a <a href="https://github.com/elastic/azure-functions-apm-nodejs-sample-app">sample Node.js application</a>.</p>
<h3>Application overview</h3>
<p>The Node.js application has two <a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook">HTTP-triggered</a> functions named &quot;<a href="https://github.com/elastic/azure-functions-apm-nodejs-sample-app/blob/main/Hello/index.js">Hello</a>&quot; and &quot;<a href="https://github.com/elastic/azure-functions-apm-nodejs-sample-app/blob/main/Goodbye/index.js">Goodbye</a>.&quot; Once deployed, they can be called as follows, and tracing data will be sent to the configured Elastic Observability deployment.</p>
<pre><code class="language-bash">curl -i https://&lt;APP_NAME&gt;.azurewebsites.net/api/hello
curl -i https://&lt;APP_NAME&gt;.azurewebsites.net/api/goodbye
</code></pre>
<h3>Setup</h3>
<p><strong>Step 0. Prerequisites</strong></p>
<p>To run the sample application, you will need:</p>
<ul>
<li>
<p>An installation of <a href="https://nodejs.org/">Node.js</a> (v14 or later)</p>
</li>
<li>
<p>Access to an Azure subscription with an appropriate role to create resources</p>
</li>
<li>
<p>The <a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli">Azure CLI (az)</a> logged into an Azure subscription</p>
<ol>
<li>Use az login to login</li>
<li>See the output of az account show</li>
</ol>
</li>
<li>
<p>The <a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4%2Cwindows%2Ccsharp%2Cportal%2Cbash#install-the-azure-functions-core-tools">Azure Functions Core Tools (func)</a> (func --version should show a 4.x version)</p>
</li>
<li>
<p>An Elastic Observability deployment to which monitoring data will be sent</p>
<ol>
<li>The simplest way to get started with Elastic APM Microsoft Azure is through Elastic Cloud. <a href="https://www.elastic.co/guide/en/elastic-stack-deploy/current/azure-marketplace-getting-started.html">Get started with Elastic Cloud on Azure Marketplace</a> or <a href="https://www.elastic.co/cloud/elasticsearch-service/signup">sign up for a trial on Elastic Cloud</a>.</li>
</ol>
</li>
<li>
<p>The APM server URL (serverUrl) and secret token (secretToken) from your Elastic stack deployment for configuration below</p>
<ol>
<li><a href="https://www.elastic.co/guide/en/apm/guide/8.7/install-and-run.html">How to get the serverUrl and secretToken documentation</a></li>
</ol>
</li>
</ul>
<p><strong>Step 1. Clone the sample application repo and install dependencies</strong></p>
<pre><code class="language-bash">git clone https://github.com/elastic/azure-functions-apm-nodejs-sample-app.git
cd azure-functions-apm-nodejs-sample-app
npm install
</code></pre>
<p><strong>Step 2. Deploy the Azure Function App</strong><br />
Caution icon! Deploying a function app to Azure can incur <a href="https://azure.microsoft.com/en-us/pricing/details/functions/">costs</a>. The following setup uses the free tier of Azure Functions. Step 5 covers the clean-up of resources.</p>
<p><strong>Step 2.1</strong><br />
To avoid name collisions with others that have independently run this demo, we need a short unique identifier for some resource names that need to be globally unique. We'll call it the DEMO_ID. You can run the following to generate one and save it to DEMO_ID and the &quot;demo-id&quot; file.</p>
<pre><code class="language-bash">if [[ ! -f demo-id ]]; then node -e 'console.log(crypto.randomBytes(3).toString(&quot;hex&quot;))' &gt;demo-id; fi
export DEMO_ID=$(cat demo-id)
echo $DEMO_ID
</code></pre>
<p><strong>Step 2.2</strong><br />
Before you can deploy to Azure, you will need to create some Azure resources: a Resource Group, Storage Account, and the Function App. For this demo, you can use the following commands. (See <a href="https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-node#create-supporting-azure-resources-for-your-function">this Azure docs section</a> for more details.)</p>
<pre><code class="language-bash">REGION=westus2   # Or use another region listed in 'az account list-locations'.
az group create --name &quot;AzureFnElasticApmNodeSample-rg&quot; --location &quot;$REGION&quot;
az storage account create --name &quot;eapmdemostor${DEMO_ID}&quot; --location &quot;$REGION&quot; \
    --resource-group &quot;AzureFnElasticApmNodeSample-rg&quot; --sku Standard_LRS
az functionapp create --name &quot;azure-functions-apm-nodejs-sample-app-${DEMO_ID}&quot; \
    --resource-group &quot;AzureFnElasticApmNodeSample-rg&quot; \
    --consumption-plan-location &quot;$REGION&quot; --runtime node --runtime-version 18 \
    --functions-version 4 --storage-account &quot;eapmdemostor${DEMO_ID}&quot;
</code></pre>
<p><strong>Step 2.3</strong><br />
Next, configure your Function App with the APM server URL and secret token for your Elastic deployment. This can be done in the <a href="https://portal.azure.com/">Azure Portal</a> or with the az CLI.</p>
<p>In the Azure portal, browse to your Function App, then its Application Settings (<a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal#settings">Azure user guide</a>). You'll need to add two settings:</p>
<p>First set your APM URL and token.</p>
<pre><code class="language-bash">export ELASTIC_APM_SERVER_URL=&quot;&lt;your serverUrl&gt;&quot;
export ELASTIC_APM_SECRET_TOKEN=&quot;&lt;your secretToken&gt;&quot;
</code></pre>
<p>Or you can use the az functionapp config appsettings set ... CLI command as follows:</p>
<pre><code class="language-bash">az functionapp config appsettings set \
  -g &quot;AzureFnElasticApmNodeSample-rg&quot; -n &quot;azure-functions-apm-nodejs-sample-app-${DEMO_ID}&quot; \
  --settings &quot;ELASTIC_APM_SERVER_URL=${ELASTIC_APM_SERVER_URL}&quot;
az functionapp config appsettings set \
  -g &quot;AzureFnElasticApmNodeSample-rg&quot; -n &quot;azure-functions-apm-nodejs-sample-app-${DEMO_ID}&quot; \
  --settings &quot;ELASTIC_APM_SECRET_TOKEN=${ELASTIC_APM_SECRET_TOKEN}&quot;
</code></pre>
<p>The ELASTIC_APM_SERVER_URL and ELASTIC_APM_SECRET_TOKEN are set in Azure function’s settings for the app and used by the Elastic APM Agent. This is initiated by the initapm.js file, which starts the Elastic APM agent with:</p>
<pre><code class="language-javascript">require(&quot;elastic-apm-node&quot;).start();
</code></pre>
<p>When you log in to Azure and look at the function’s configuration, you will see them set:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/blog-elastic-azure-functions-application-settings.png" alt="azure functions application settings" /></p>
<p><strong>Step 2.4</strong><br />
Now you can publish your app. (Re-run this command every time you make a code change.)</p>
<pre><code class="language-bash">func azure functionapp publish &quot;azure-functions-apm-nodejs-sample-app-${DEMO_ID}&quot;
</code></pre>
<p>You should log in to Azure to see the function running.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/blog-elastic-azure-function-app.png" alt="azure function app" /></p>
<p><strong>Step 3. Try it out</strong></p>
<pre><code class="language-bash">% curl https://azure-functions-apm-nodejs-sample-app-${DEMO_ID}.azurewebsites.net/api/Hello
{&quot;message&quot;:&quot;Hello.&quot;}
% curl https://azure-functions-apm-nodejs-sample-app-${DEMO_ID}.azurewebsites.net/api/Goodbye
{&quot;message&quot;:&quot;Goodbye.&quot;}
</code></pre>
<p>In a few moments, the APM app in your Elastic deployment will show tracing data for your Azure Function app.</p>
<p><strong>Step 4. Apply some load to your app</strong><br />
To get some more interesting data, you can run the following to generate some load on your deployed function app:</p>
<pre><code class="language-bash">npm run loadgen
</code></pre>
<p>This uses the <a href="https://github.com/mcollina/autocannon">autocannon</a> node package to generate some light load (2 concurrent users, each calling at 5 requests/s for 60s) on the &quot;Goodbye&quot; function.</p>
<p><strong>Step 5. Clean up resources</strong><br />
If you deployed to Azure, you should make sure to delete any resources so you don't incur any costs.</p>
<pre><code class="language-bash">az group delete --name &quot;AzureFnElasticApmNodeSample-rg&quot;
</code></pre>
<h2>Analyzing Azure Function APM data in Elastic</h2>
<p>Once you have successfully set up the sample application and started generating load, you should see APM data appearing in the Elastic Observability APM Services capability.</p>
<h2>Service map</h2>
<p>With the default setup, you will see two services in the APM Service map.</p>
<p>The main function: azure-functions-apm-nodejs-sample-app</p>
<p>And the end point where your function is accessible: azure-functions-apm-nodejs-sample-app-ec7d4c.azurewebsites.net</p>
<p>You will see that there is a connection between the two as your application is taking requests and answering through the endpoint.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/blog-elastic-observability-services.png" alt="observability services" /></p>
<p>From the <a href="https://www.elastic.co/observability/application-performance-monitoring">APM Service</a> map you can further investigate the function, analyze traces, look at logs, and more.</p>
<h3>Service details</h3>
<p>When we dive into the details, we can see several items.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/blog-elastic-observability-azure-functions-apm.png" alt="observability azure functions apm" /></p>
<ul>
<li>Latency for the recent load we ran against the application</li>
<li>Transactions (Goodbye and Hello)</li>
<li>Average throughput</li>
<li>And more</li>
</ul>
<h3>Transaction details</h3>
<p>We can see transaction details.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/blog-elastic-observability-get-api-goodbye.png" alt="observability get api goodbye" /></p>
<p>An individual trace shows us that the &quot;Goodbye&quot; function <a href="https://github.com/elastic/azure-functions-apm-nodejs-sample-app/blob/main/Goodbye/index.js#L6-L10">calls the &quot;Hello&quot; function</a> in the same function app before returning:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/blog-elastic-latency-distribution-trace-sample.png" alt="latency distribution trace sample" /></p>
<h3>Machine learning based latency correlation</h3>
<p>As we’ve mentioned in other blogs, we can also correlate issues such as higher than normal latency. Since we see a spike at 1s, we run the embedded latency correlation, which uses machine learning to help analyze the potential impacting component by analyzing logs, metrics, and traces.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/blog-elastic-latency-distribution-correlations.png" alt="latency distribution correlations" /></p>
<p>The correlation indicated there is a potential cause (25%) due to the host sending the load (my machine).</p>
<h3>Cold start detection</h3>
<p>Also, we can see the impact a <a href="https://azure.microsoft.com/en-ca/blog/understanding-serverless-cold-start/">cold start</a> can have on the latency of a request:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/blog-elastic-trace-sample.png" alt="trace sample" /></p>
<h2>Summary</h2>
<p>Elastic Observability provides real-time monitoring of Azure Functions in your production environment for a broad range of use cases. Curated dashboards assist DevOps teams in performing root cause analysis for performance bottlenecks and errors. SRE teams can quickly view upstream and downstream dependencies, as well as perform analyses in the context of distributed microservices architecture.</p>
<h2>Learn more</h2>
<p>To learn how to add the Elastic APM Agent to an existing Node.js Azure Function app, read <a href="https://www.elastic.co/guide/en/apm/agent/nodejs/master/azure-functions.html">Monitoring Node.js Azure Functions</a>. Additional resources include:</p>
<ul>
<li><a href="https://www.elastic.co/blog/getting-started-with-the-azure-integration-enhancement">How to deploy and manage Elastic Observability on Microsoft Azure</a></li>
<li><a href="https://www.elastic.co/guide/en/apm/guide/current/apm-quick-start.html">Elastic APM Quickstart</a></li>
</ul>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/trace-azure-function-application-observability/09-road.jpeg" length="0" type="image/jpeg"/>
        </item>
        <item>
            <title><![CDATA[Trace-based testing with Elastic APM and Tracetest]]></title>
            <link>https://www.elastic.co/observability-labs/blog/trace-based-testing-apm-tracetest</link>
            <guid isPermaLink="false">trace-based-testing-apm-tracetest</guid>
            <pubDate>Wed, 15 Feb 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Want to run trace-based tests with Elastic APM? We're happy to announce that Tracetest now integrates with Elastic Observability APM. Check out this hands-on example of how Tracetest works with Elastic Observability APM and OpenTelemetry.]]></description>
            <content:encoded><![CDATA[<p><em>This post was originally published on the</em> <a href="https://tracetest.io/blog/tracetest-integration-elastic-trace-based-testing-application-performance-monitoring"><em>Tracetest blog</em></a><em>.</em></p>
<p>Want to run trace-based tests with Elastic APM? Today is your lucky day. We're happy to announce that Tracetest now integrates with Elastic Observability APM.</p>
<p>Check out this <a href="https://github.com/kubeshop/tracetest/tree/main/examples/tracetest-elasticapm-with-elastic-agent">hands-on example</a> of how Tracetest works with Elastic Observability APM and OpenTelemetry!</p>
<p><a href="https://tracetest.io/">Tracetest</a> is a <a href="https://www.cncf.io/">CNCF</a> project aiming to provide a solution for deep integration and system testing by leveraging the rich data in distributed system traces. In this blog, we intend to provide an introduction to Tracetest and its capabilities, including how it can be integrated with <a href="https://www.elastic.co/observability/application-performance-monitoring">Elastic Application Performance Monitoring</a> and <a href="https://opentelemetry.io/">OpenTelemetry</a> to enhance the testing process.</p>
<h2>Your good friend distributed tracing</h2>
<p>Distributed tracing is a way to understand how a distributed system works by tracking the flow of requests through the system. It can be used for a variety of purposes, such as identifying and fixing performance issues, figuring out what went wrong when an error occurs, and making sure that the system is running smoothly. Here are a few examples of how distributed tracing can be used:</p>
<ul>
<li><strong>Monitoring performance:</strong> Distributed tracing can help you keep an eye on how your distributed system is performing by showing you what's happening in real time. This can help you spot and fix problems like bottlenecks or slow response times that can make the system less reliable.</li>
<li><strong>Finding the source of problems:</strong> When something goes wrong, distributed tracing can help you figure out what happened by showing you the sequence of events that led up to the problem. This can help you pinpoint the specific service or component that's causing the issue and fix it.</li>
<li><strong>Debugging:</strong> Distributed tracing can help you find and fix bugs by giving you detailed information about what's happening in the system. This can help you understand why certain requests are behaving in unexpected ways and how to fix them.</li>
<li><strong>Security:</strong> Distributed tracing can help you keep an eye on security by showing you who is making requests to the system, where they are coming from, and what services are being accessed.</li>
<li><strong>Optimization:</strong> Distributed tracing can help you optimize the performance of the system by providing insight into how requests are flowing through it, which can help you identify areas that can be made more efficient and reduce the number of requests that need to be handled.</li>
</ul>
<h2>Distributed tracing — Now also for testing</h2>
<p>Observability, previously only used in operations, is now being applied in other areas of development, such as testing. This shift has led to the emergence of <a href="https://www.infoq.com/articles/observability-driven-development/">&quot;Observability-driven development&quot;</a> and &quot;trace-based testing&quot; as new methods for using distributed tracing to test distributed applications.</p>
<p>Instead of just checking that certain parts of the code are working, trace-driven testing follows the path that a request takes as it goes through the system. This way, you can make sure that the entire system is working properly and that the right output is produced for a given input. By using distributed tracing, developers can record what happens during the test and then use that information to check that everything is working as it should.</p>
<p>This method of testing can help to find problems that may be hard to detect with other types of testing and can better validate that the new code is working as expected. Additionally, distributed tracing provides information about what is happening during the test, such as how long it takes for a request to be processed and which services are being used, which can help developers understand how the code behaves in a real-world scenario.</p>
<h2>Enters Tracetest</h2>
<p><a href="https://tracetest.io/">Tracetest</a> is a CNCF project that can run tests by verifying new traces against previously created assertions against other traces captured from the real systems. Here's how you can use Tracetest:</p>
<ul>
<li>Capture the baseline good known trace. This will be the golden standard that you will use to write your tests and assertions. Trace-driven development is a better way to test how different parts of the system work together because it allows developers to test the entire process from start to finish, making sure that everything is working as it should and giving a more complete view of how the system is functioning instead of trying to create disjointed assertions validating the request execution.</li>
<li>Now you can start validating your code changes against good known behavior captured previously.</li>
<li>Tracetest can validate the resulting traces from the test and see if the system is working as it should. This can help you find problems that traditional testing methods might not catch.</li>
<li>Create reports: Tracetest can also create reports that summarize the results of the test so that you can share the information with your team.</li>
<li>Help you validate in production that the new requests follow the known path and run the predefined assertions against them.</li>
</ul>
<p>The APM tool in Kibana, which is a familiar UI for many developers, can provide extra information when used with Tracetest. The APM tool can show you how the system is performing during the test and help you find issues using the familiar user interface. For example, the APM tool can show you how requests are moving through the system, how long requests take to be processed, and which parts of the system are being used. This information can help you identify and fix problems during testing.</p>
<p>Furthermore, the APM tool can be set to show you all the data in real-time, which allows you to monitor the system's behavior during the test or even in production and helps you make sense of what Tracetest is showing.</p>
<h2>How Tracetest works with Elastic APM to test the application</h2>
<p>The components work together to provide a complete solution for testing distributed systems. The telemetry captured by the OpenTelemetry agent is sent to the Elastic APM Server, which processes and formats the data for indexing in Elasticsearch. The data can then be queried and analyzed using Kibana APM UI, and Tracetest can be used to conduct deep integration and system tests by utilizing the rich data contained in the distributed system trace.</p>
<p>For more details on Elastic's support for OpenTelelemetry, check out <a href="https://www.elastic.co/blog/opentelemetry-observability">Independence with OpenTelemetry on Elastic</a>.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-based-testing-apm-tracetest/blog-elastic-distributed-system-trace.png" alt="" /></p>
<ol>
<li>Tracetest initiates the test by sending a request to the application under test.</li>
<li>The application processes the request, and the built-in OpenTelemetry agent captures the telemetry data of the request. This data includes information such as request and response payloads, request and response headers, and any errors that occurred during the request processing. The agent then sends the captured telemetry data to the Elastic APM Server.</li>
<li>Elastic APM server consumes OpenTelemetry or Elastic APM spans and sends the data to be stored and indexed in Elasticsearch.</li>
<li>Tracetest polls Elasticsearch to retrieve the captured trace data. It makes use of Elasticsearch query to fetch the trace data. Tracetest compares the received trace data with the expected trace data and runs the assertions. This step is used to check whether the data received from the application matches the expected data and to check for any errors or issues that may have occurred during the request processing. Based on the results of the comparison, Tracetest will report any errors or issues found and will provide detailed information about the root cause of the problem. If the test passes, Tracetest will report that the test passed, and the test execution process will be completed.</li>
<li>The trace data is visible and can be analyzed in Kibana APM UI as well.</li>
</ol>
<h2>Running your first Tracetest environment with Elastic APM and Docker compose</h2>
<p>In your existing observability setup, you have the <a href="https://opentelemetry.io/docs/instrumentation/js/getting-started/nodejs/">OpenTelemetry Nodejs agent</a> configured in your code and <a href="https://www.elastic.co/blog/opentelemetry-observability">sending OpenTelemetry traces to the Elastic APM server that then stores</a> them in Elasticsearch. Adding Tracetest to the infrastructure lets you write detailed trace-based tests based on the existing tracing infrastructure. Tracetest runs tests against endpoints and uses trace data to run assertions.</p>
<p>The example that we are going to run is from the Tracetest GitHub repository. It contains a docker-compose setup, which is a convenient way to run multiple services together in a defined environment. The example includes a sample application that has been instrumented with an OpenTelemetry agent. The example also includes the Tracetest server with its Postgres database, which is responsible for invoking the test, polling Elasticsearch to retrieve the captured trace data, comparing the received trace data with the expected trace data, and running the assertions. Finally, the example includes Elasticsearch, Kibana, and the Elastic APM server from the Elastic Stack.</p>
<p>To quickly access the example, you can run the following:</p>
<pre><code class="language-bash">git clone https://github.com/kubeshop/tracetest.git
cd tracetest/examples/tracetest-elasticapm-with-otel
docker-compose up -d
</code></pre>
<p>Once you have Tracetest set up, open <a href="http://localhost:11633">http://localhost:11633</a> in your browser to check out the Web UI.</p>
<p>Navigate to the Settings menu and ensure the connection to Elasticsearch is working by pressing Test Connection:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-based-testing-apm-tracetest/blog-elastic-tracetest-configure-data-store.png" alt="" /></p>
<p>To create a test, click the Create dropdown and choose Create New Test. Select the HTTP Request and give it a name and description.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-based-testing-apm-tracetest/blog-elastic-create-new-test.png" alt="" /></p>
<p>For this simple example, GET the Node.js app, which runs at <a href="http://app:8080">http://app:8080</a>.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-based-testing-apm-tracetest/blog-elastic-trace-request-details.png" alt="" /></p>
<p>With the test created, you can click the Trace tab to see the distributed trace. It’s simple, but you can start to see how it delivers immediate visibility into every transaction your HTTP request generates.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-based-testing-apm-tracetest/blog-elastic-tracetest-trigger.png" alt="" /></p>
<p>From here, you can continue by adding assertions.</p>
<p>To make an assertion based on the GET / span of our trace, select that span in the graph view and click <strong>Current span</strong> in the Test Spec modal. Or, copy this span selector directly, using the <a href="https://docs.tracetest.io/concepts/selectors/">Tracetest Selector Language</a>:</p>
<pre><code class="language-javascript">span[tracetest.span.type=&quot;http&quot; name=&quot;GET /&quot; http.target=&quot;/&quot; http.method=&quot;GET&quot;]
</code></pre>
<p>Below, add the attr:http.status_code attribute and the expected value, which is 200. You can add more complex assertions as well, like testing whether the span executes in less than 500ms. Add a new assertion for attr:http.status_code, choose &lt;, and add 500ms as the expected value.</p>
<p>You can check against other properties, return statuses, timing, and much more, but we’ll keep it simple for now.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-based-testing-apm-tracetest/blog-elastic-tracetest-edit-test-spec.png" alt="" /></p>
<p>Then click <strong>Save Test Spec</strong> , followed by <strong>Publish</strong> , and you’ve created your first assertion.If you open the APM app in Kibana at <a href="https://localhost:5601">https://localhost:5601</a> (find the username and password from the examples/tracetest-elasticapm- <strong>with</strong> -otel/.env file), you will be able to navigate to the transaction generated by the test representing the overall application call with three underlying spans:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/trace-based-testing-apm-tracetest/blog-elastic-latency-distribution.png" alt="" /></p>
<h2>Summary</h2>
<p>Elastic APM and Tracetest are tools that can help make testing distributed applications easier by providing a more comprehensive view of the system's behavior and allowing developers to identify and diagnose performance issues more efficiently. Tracetest allows you to test the entire process from start to finish, making sure that everything is working as it should, by following the path that a request takes.</p>
<p>Elastic APM provides detailed information about the performance of a system, including how requests are flowing through the system, how long requests take to be processed, and which services are being called. Together, these tools can help developers to identify and fix issues more quickly, improve collaboration and communication among the team, and ultimately improve the overall quality of the system.</p>
<blockquote>
<ul>
<li>Elastic APM documentation: <a href="https://www.elastic.co/guide/en/apm/guide/current/index.html">https://www.elastic.co/guide/en/apm/guide/current/index.html</a></li>
<li>Tracetest documentation: <a href="https://tracetest.io/docs/">https://tracetest.io/docs/</a> </li>
<li>Tracetest Github page: <a href="https://github.com/kubeshop/tracetest">https://github.com/kubeshop/tracetest</a> </li>
<li>Elastic blog: <a href="https://www.elastic.co/blog/category/technical-topics">https://www.elastic.co/blog/category/technical-topics</a> </li>
<li>Elastic APM community forum: <a href="https://discuss.elastic.co/c/apm">https://discuss.elastic.co/c/apm</a> </li>
<li>Tracetest support: <a href="https://discord.com/channels/884464549347074049/963470167327772703">Discord channel</a></li>
</ul>
</blockquote>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/trace-based-testing-apm-tracetest/telescope-search-1680x980.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>