<?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 - logging</title>
        <link>https://www.elastic.co/observability-labs</link>
        <description>Trusted security news &amp; research from the team at Elastic.</description>
        <lastBuildDate>Mon, 11 May 2026 19:10:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Elastic Observability Labs - logging</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[Connecting the Dots: ES|QL Joins for Richer Observability Insights]]></title>
            <link>https://www.elastic.co/observability-labs/blog/elastic-esql-join-observability</link>
            <guid isPermaLink="false">elastic-esql-join-observability</guid>
            <pubDate>Thu, 29 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Now in tech preview, ES|QL LOOKUP JOIN lets you enrich logs, metrics, and traces at query time no need to denormalize at ingest. Add deployment, infra, or business context dynamically, reduce storage, and accelerate root cause analysis in Elastic Obervability.]]></description>
            <content:encoded><![CDATA[<h1>Connecting the Dots: ES|QL Joins for Richer Observability Insights</h1>
<p>You might have seen our recent announcement about the <a href="https://www.elastic.co/blog/esql-lookup-join-elasticsearch">arrival of SQL-style joins in Elasticsearch</a> with ES|QL's LOOKUP JOIN command (now in Tech Preview!). While that post covered the basics, let's take a closer look at this in the context of Observability. How can this new join capability specifically help engineers and SREs make sense of their logs, metrics, and traces and make Elasticsearch more storage efficient by not denormalizing as much data?</p>
<p><strong>Note:</strong> Before we jump into the details, it’s important to mention again that this type of functionality today relies on a special lookup index. It is not (yet) possible to JOIN any arbitrary index.</p>
<p>Observability isn't just about collecting data; it's about understanding it. Often, the raw telemetry data – a log line, a metric point, a trace span – lacks the full context needed for quick diagnosis or impact assessment. We need to correlate data, enrich it with business or infrastructure context, and ask more advanced questions.</p>
<p>Historically, achieving this in Elasticsearch involved techniques like denormalizing data at ingest time (using ingest pipelines with enrich processors, for example) or performing joins client-side. </p>
<p>By adding the necessary context (like host details or user attributes) as data flowed in, each document arrived fully ready for queries and analytics without extra processing later on. This approach worked well in many cases and still does, particularly when the reference data changes slowly or when the enriched fields are critical for nearly every search. </p>
<p>However, as environments become more dynamic and diverse, the need to frequently update reference data (or avoid storing repetitive fields in every document) highlighted some of the trade-offs. </p>
<p>With the introduction of ES|QL LOOKUP JOIN in Elasticsearch 8.18 and 9.0, you now have an additional, more flexible option for situations where real-time lookups and minimal duplication are desired. Both methods—ingest-time enrichment and on-the-fly LOOKUP JOIN—complement each other and remain valid, depending on use case needs around update frequency, query performance, and storage considerations.</p>
<h2>Why Lookup Joins for Observability</h2>
<p>Lookup joins keep things flexible. You can decide on the fly if you’d like to look up additional information to assist you in your investigation.</p>
<p>Here are some examples:</p>
<ul>
<li>
<p><strong>Deployment Information:</strong> Which version of the code is generating these errors?</p>
</li>
<li>
<p><strong>Infrastructure Mapping:</strong> Which Kubernetes cluster or cloud region is experiencing high latency? What hardware does it use?</p>
</li>
<li>
<p><strong>Business Context:</strong> Are critical customers being affected by this slowdown?</p>
</li>
<li>
<p><strong>Team Ownership:</strong> Which team owns the service throwing these exceptions?</p>
</li>
</ul>
<p>Keeping this kind of information perfectly denormalized onto <em>every single</em> log line or metric point can be challenging and inefficient. Lookup datasets – like lists of deployments, server inventories, customer tiers, or service ownership mappings – often change independently of the telemetry data itself.</p>
<p><code>LOOKUP JOIN</code> is ideal here because:</p>
<ol>
<li>
<p><strong>Lookup Indices are Writable:</strong> Update your deployment list, CMDB export, or on-call rotation in the lookup index, and your <em>next</em> ES|QL query immediately uses the fresh data. No need to re-run complex enrich policies or re-index data.</p>
</li>
<li>
<p><strong>Flexibility:</strong> You decide <em>at query time</em> which context to join. Maybe today you care about deployment versions, tomorrow about cloud regions.</p>
</li>
<li>
<p><strong>Simpler Setup:</strong> As the original post highlighted, there are no enrich policies to manage. Just create an index with <code>index.mode: lookup</code> and load your data - up to 2 billion documents per lookup index.</p>
</li>
</ol>
<h2>Observability Use Cases &amp; Examples with ES|QL</h2>
<p>Let’s now look at a few examples to see how Lookup Joins can help.</p>
<h3>Enriching Error Logs with Deployment Context</h3>
<p>Lets say you're seeing a spike in errors for your <code>checkout-service</code>. You have logs flowing into a data stream, but they only contain the service name. The documents don’t have any information about the deployment activity itself. </p>
<pre><code class="language-bash">FROM logs-*
  | WHERE log.level == &quot;error&quot;
  | WHERE service.name == &quot;opbeans-ruby&quot;
</code></pre>
<p>You need to know if a recent deployment is contributing to these errors. To do this, we can maintain a <code>deployments_info_lkp</code> index (set with <code>index.mode: lookup</code>) that maps service names to their deployment times. This index could be updated from our CI/CD pipeline automatically any time a deployment happens.</p>
<pre><code class="language-bash">PUT /deployments_info_lkp
{
  &quot;settings&quot;: {
    &quot;index.mode&quot;: &quot;lookup&quot;
  },
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;service&quot;: {
        &quot;properties&quot;: {
          &quot;name&quot;: {
            &quot;type&quot;: &quot;keyword&quot;
          },
          &quot;deployment_time&quot;: {
            &quot;type&quot;: &quot;date&quot;
          },
          &quot;version&quot;: {
            &quot;type&quot;: &quot;keyword&quot;
          }
        }
      }
    }
  }
}
# Bulk index the deployment documents
POST /_bulk
{ &quot;index&quot; : { &quot;_index&quot; : &quot;deployments_info_lkp&quot; } }
{ &quot;service.name&quot;: &quot;opbeans-ruby&quot;, &quot;service.version&quot;: &quot;1.0&quot;, &quot;deployment_time&quot;: &quot;2025-05-22T06:00:00Z&quot; }
{ &quot;index&quot; : { &quot;_index&quot; : &quot;deployments_info_lkp&quot; } }
{ &quot;service.name&quot;: &quot;opbeans-go&quot;, &quot;service.version&quot;: &quot;1.1.0&quot;, &quot;deployment_time&quot;: &quot;2025-05-22T06:00:00Z&quot; }
</code></pre>
<p>Using this information you can now write a query that joins these two sources.</p>
<p><em>ES|QL Query:</em></p>
<pre><code class="language-bash">FROM logs-* 
  | WHERE log.level == &quot;error&quot;
  | WHERE service.name == &quot;opbeans-ruby&quot;
  | LOOKUP JOIN deployments_info_lkp ON service.name 
</code></pre>
<p>This alone is a good step towards troubleshooting the problem. You now have the deployment_time column available for each of your error documents. The last remaining step now is to use this for further filtering. </p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/elastic-esql-join-observability/discover.png" alt="Discover" /></p>
<p>Any of the data we managed to join from the lookup index can be handled as any other data we’d usually have available in the ES|QL query. This means that we can filter on it, and check if we had a recent deployment.</p>
<pre><code class="language-bash">FROM logs-*
  | WHERE log.level == &quot;error&quot;
  | WHERE service.name == &quot;opbeans-ruby&quot;
  | LOOKUP JOIN deployments_info_lkp ON service.name 
  | KEEP message, service.name, service.version, deployment_time 
  | WHERE deployment_time &gt; NOW() - 2h
</code></pre>
<p><img src="https://www.elastic.co/observability-labs/assets/images/elastic-esql-join-observability/discover2.png" alt="Discover2" /></p>
<h3>Saving disk space using JOIN</h3>
<p>Denormalizing data by including contextual information like host OS or cloud provider details directly in every log event is convenient for querying but can increase storage consumption, especially with high-volume data streams. Instead of storing this often-redundant information repeatedly, we can leverage joins to retrieve it on demand, potentially saving valuable disk space. While compression often handles repetitive data well, removing these fields entirely can still yield noticeable storage savings.</p>
<p>In this example we’ll use a dataset of 1,000,000 Kubernetes container logs using the default mapping of the Kubernetes integration, with <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/logs-data-stream">logsdb index mode</a> enabled. The starting size for this index is 35.5mb. </p>
<pre><code class="language-bash">GET _cat/indices/k8s-logs-default?h=index,pri.store.size
### 
k8s-logs-default       35.5mb
</code></pre>
<p>Using the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-disk-usage">disk usage API</a>, we observed that fields like host.os and cloud.* contribute roughly 5% to the total index size on disk (35.5mb). These fields can be useful in some cases, but information like the os.name is rarely queried. </p>
<pre><code class="language-bash">// Example host.os structure
&quot;os&quot;: {
  &quot;codename&quot;: &quot;Plow&quot;, &quot;family&quot;: &quot;redhat&quot;, &quot;kernel&quot;: &quot;6.6.56+&quot;,
  &quot;name&quot;: &quot;Red Hat Enterprise Linux&quot;, &quot;platform&quot;: &quot;rhel&quot;, &quot;type&quot;: &quot;linux&quot;, &quot;version&quot;: &quot;9.5 (Plow)&quot;
}

// Example cloud structure
&quot;cloud&quot;: {
  &quot;account&quot;: { &quot;id&quot;: &quot;elastic-observability&quot; },
  &quot;availability_zone&quot;: &quot;us-central1-c&quot;,
  &quot;instance&quot;: { &quot;id&quot;: &quot;5799032384800802653&quot;, &quot;name&quot;: &quot;gke-edge-oblt-edge-oblt-pool-46262cd0-w905&quot; },
  &quot;machine&quot;: { &quot;type&quot;: &quot;e2-standard-4&quot; },
  &quot;project&quot;: { &quot;id&quot;: &quot;elastic-observability&quot; },
  &quot;provider&quot;: &quot;gcp&quot;, &quot;region&quot;: &quot;us-central1&quot;, &quot;service&quot;: { &quot;name&quot;: &quot;GCE&quot; }
}
</code></pre>
<p>Instead of storing this information with every document, let's instead drop this information in an ingest pipeline.</p>
<pre><code class="language-bash">PUT _ingest/pipeline/drop-host-os-cloud
{
  &quot;processors&quot;: [
      { &quot;remove&quot;: { &quot;field&quot;: &quot;host.os&quot; } },
      { &quot;set&quot;: { &quot;field&quot;: &quot;tmp1&quot;, &quot;value&quot;: &quot;{{cloud.instance.id}}&quot; } }, // Temporarily store the ID
</code></pre>
<pre><code>      { &quot;remove&quot;: { &quot;field&quot;: &quot;cloud&quot; } },                             // Remove the entire cloud object
      { &quot;set&quot;: { &quot;field&quot;: &quot;cloud.instance.id&quot;, &quot;value&quot;: &quot;{{tmp1}}&quot; } }, // Restore just the cloud instance ID
      { &quot;remove&quot;: { &quot;field&quot;: &quot;tmp1&quot;, &quot;ignore_missing&quot;: true } }         // Clean up temporary field
    ]
}
</code></pre>
<p>Reindexing (and <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-forcemerge">force merging to one segment</a>) now shows the following size, resulting in approximately 5% less space. </p>
<pre><code class="language-bash">GET _cat/indices/k8s-logs-*?h=index,pri.store.size
### 
k8s-logs-default             33.7mb
k8s-logs-drop-cloud-os       35.5mb
</code></pre>
<p>Now, to regain access to the removed host.os and cloud.* information during analysis without storing it in every log document, we can create a lookup index. This index will store the full host and cloud metadata, keyed by the cloud.instance.id that we preserved in our logs. This instance_metadata_lkp index will be significantly smaller than the space saved across millions or billions of log lines, as it only needs one document per unique instance.</p>
<pre><code class="language-bash"># Create the lookup index for instance metadata
PUT /instance_metadata_lkp
{
  &quot;settings&quot;: {
    &quot;index.mode&quot;: &quot;lookup&quot;
  },
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
</code></pre>
<pre><code class="language-bash">      &quot;cloud.instance.id&quot;: {  # The join key we kept in the logs
        &quot;type&quot;: &quot;keyword&quot;
      },
      &quot;host.os&quot;: {           # The full host.os object we removed
        &quot;type&quot;: &quot;object&quot;,
        &quot;enabled&quot;: false      # Often don't need to search sub-fields here
      },
      &quot;cloud&quot;: {             # The full cloud object we removed (mostly)
         &quot;type&quot;: &quot;object&quot;,
         &quot;enabled&quot;: false     # Often don't need to search sub-fields here
      }
    }
  }
}

# Bulk index sample instance metadata (keyed by cloud.instance.id)
# This data might come from your cloud provider API or CMDB
POST /_bulk
{ &quot;index&quot; : { &quot;_index&quot; : &quot;instance_metadata_lkp&quot;, &quot;_id&quot;: &quot;5799032384800802653&quot; } }
{ &quot;cloud.instance.id&quot;: &quot;5799032384800802653&quot;, &quot;host.os&quot;: { &quot;codename&quot;: &quot;Plow&quot;, &quot;family&quot;: &quot;redhat&quot;, &quot;kernel&quot;: &quot;6.6.56+&quot;, &quot;name&quot;: &quot;Red Hat Enterprise Linux&quot;, &quot;platform&quot;: &quot;rhel&quot;, &quot;type&quot;: &quot;linux&quot;, &quot;version&quot;: &quot;9.5 (Plow)&quot; }, &quot;cloud&quot;: { &quot;account&quot;: { &quot;id&quot;: &quot;elastic-observability&quot; }, &quot;availability_zone&quot;: &quot;us-central1-c&quot;, &quot;instance&quot;: { &quot;id&quot;: &quot;5799032384800802653&quot;, &quot;name&quot;: &quot;gke-edge-oblt-edge-oblt-pool-46262cd0-w905&quot; }, &quot;machine&quot;: { &quot;type&quot;: &quot;e2-standard-4&quot; }, &quot;project&quot;: { &quot;id&quot;: &quot;elastic-observability&quot; }, &quot;provider&quot;: &quot;gcp&quot;, &quot;region&quot;: &quot;us-central1&quot;, &quot;service&quot;: { &quot;name&quot;: &quot;GCE&quot; } } }
</code></pre>
<p>With this setup, when you need the full host or cloud context for your logs, you can simply use LOOKUP JOIN in your ES|QL query and continue filtering on the data from the lookup index</p>
<pre><code class="language-bash">FROM logs-* 
  | LOOKUP JOIN instance_metadata_lkp ON cloud.instance.id 
  | WHERE cloud.region == &quot;us-central1&quot;
</code></pre>
<p>This approach allows us to query the full context when needed (e.g., filtering logs by host.os.name or cloud.region) while significantly reducing the storage footprint of the high-volume log indices by avoiding redundant data denormalization.</p>
<p>It should be noted that low cardinality metadata fields generally compress well and a large part of the storage savings in this case come from the “text” mapping of the host.os.name and cloud.instance.name field. Make sure to use the disk usage API to evaluate if this approach would be worth it in your specific use case. </p>
<h2>Getting Started with Lookups for Observability</h2>
<p>Creating the necessary lookup indices is straightforward. As detailed in our <a href="http://link-to-original-blog-post">initial blog post</a>, you can use Kibana's Index Management UI, the Create Index API, or the File Upload utility – the key is setting <code>&quot;index.mode&quot;: &quot;lookup&quot;</code> in the index settings.</p>
<p>For Observability, consider automating the population of these lookup indices:</p>
<ul>
<li>
<p>Export data periodically from your CMDB, CRM, or HR systems.</p>
</li>
<li>
<p>Have your CI/CD pipeline update the <code>deployments_lkp</code> index upon successful deployment.</p>
</li>
<li>
<p>Use tools like Logstash with an <code>elasticsearch</code> output configured to write to your lookup index.</p>
</li>
</ul>
<h2>A Note on Performance and Alternatives</h2>
<p>While incredibly powerful, joins aren't free. Each <code>LOOKUP JOIN</code> adds processing overhead to your query. For contextual data that is <em>very</em> static (e.g., the cloud region a host <em>permanently</em> resides in) and needed in <em>almost every</em> query against that data, the traditional approach of enriching at ingest time might still be slightly more performant for those specific queries, trading upfront processing and storage for query speed.</p>
<p>However, for the dynamic, flexible, and targeted enrichment scenarios common in Observability – like mapping to ever-changing deployments, user segments, or team structures – <code>LOOKUP JOIN</code> offers a compelling, efficient, and easier-to-manage solution.</p>
<h2>Conclusion</h2>
<p>ES|QL's <code>LOOKUP JOIN</code> is making it easy to correlate and enrich your logs, metrics, and traces with up-to-date external information <em>at query time</em>; you can move faster from detecting problems to understanding their scope, impact, and root cause.</p>
<p>This feature is currently in Technical Preview in Elasticsearch 8.18 and Serverless, available now on Elastic Cloud. We encourage you to try it out with your own Observability data and share your feedback using the &quot;Submit feedback&quot; button in the ES|QL editor in Discover. We're excited to see how you use it to connect the dots in your systems!</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/elastic-esql-join-observability/esql-join.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Elastic's collaboration with OpenTelemetry on improving the filelog receiver]]></title>
            <link>https://www.elastic.co/observability-labs/blog/elastics-collaboration-opentelemetry-filelog-receiver</link>
            <guid isPermaLink="false">elastics-collaboration-opentelemetry-filelog-receiver</guid>
            <pubDate>Mon, 17 Jun 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic is committed to help OpenTelemetry advance it's logging capabilities. Learn about our collaboration with the OpenTelemetry community on improving the capabilities and quality aspects of the OpenTelemetry Collector's filelog receiver.]]></description>
            <content:encoded><![CDATA[<p>As the newest generally available signal in OpenTelemetry (OTel), logging support currently lags behind tracing and metrics in terms of feature scope and maturity.
At Elastic, we bring years of extensive experience with logging use cases and the challenges they present.
Committed to advancing OpenTelemetry's logging capabilities, we have focused on enhancing its logging functionalities.</p>
<p>Over the past few months, we have dealt with the capabilities of the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/v0.102.0/receiver/filelogreceiver/README.md">filelog receiver</a>
in the <a href="https://opentelemetry.io/docs/collector/">OpenTelemetry Collector</a>, leveraging our expertise as the <a href="https://www.elastic.co/beats/filebeat">Filebeat's</a> maintainers to help refine and expand its potential.
Our goal is to contribute meaningfully to the evolution of OpenTelemetry's logging features, ensuring they meet the high standards required for robust observability.</p>
<p>Specifically, we focused on verifying that the receiver is well covered for cases and aspects that have been a pain for us in the past with Filebeat
— such as fail-over handling, self-telemetry, test coverage, documentation and usability.
Based on our exploration, we started insightful conversations with the OTel project's maintainers, sharing our thoughts and any suggestions that could be useful from our experience.
Moreover, we've started putting up PRs to add documentation, make enhancements, improve tests, fix bugs, and even implement completely new features.</p>
<p>In this blog post we'll provide a sneak preview of the work that we've done so far in collaboration with the OpenTelemetry community and what's coming next as we continue to explore ways to improve the OpenTelemetry Collector for log collection.</p>
<h2>Enhancing the filelog receiver's telemetry</h2>
<p>Observability tools are software components like any other and, thus, need to be monitored as any other software to be able to debug problems and tune relevant settings.
In particular, users of the filelog receiver will want to know how it's performing.
It's important that the filelog receiver emits sufficient telemetry data for common troubleshooting and optimization use cases.
This includes sufficient logging and observable metrics providing insights into the filelog receiver's internal state.</p>
<p>While the filelog receiver already provided a good set of self-telemetry data, we identified some areas of improvement.
In particular, we contributed functionality to emit self-telemetry <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/33237">logs on crucial events</a> like when log files are discovered, moved or truncated.
Another contribution includes <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/31544">observable metrics about filelog’s receiver internal state</a> about how many files are opened and being harvested.
You can find more information on the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/31256">respective tracking issue</a>.</p>
<h2>Improving the Kubernetes container logs parsing</h2>
<p>The filelog receiver has been able to parse Kubernetes container logs for some time now.
However, properly parsing logs from Kubernetes Pods required a fair bit of configuration to deal with different runtime formats and to extract important meta information, such as <code>k8s.pod.name</code>, <code>k8s.container.name</code>, etc.
With this in mind we proposed to abstract these complex set of configuration into a simpler implementation specific container parser and contributed this new feature to the filelog receiver.
With that new feature, setting up logs collection for Kubernetes is by magnitudes easier - with only eight lines of configuration vs. ~ 80 lines of configuration before.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/elastics-collaboration-opentelemetry-filelog-receiver/container-parser-config-example.png" alt="1 - Usability improvement for parsing Kubernetes container logs" /></p>
<p>You can learn more about the details of the new <a href="https://opentelemetry.io/blog/2024/otel-collector-container-log-parser">container logs parser in the corresponding OpenTelemetry blog post</a>.</p>
<h3>Evaluating test coverage</h3>
<p>Logs collection from files can run into different unexpected scenarios such as restarts, overload and error scenarios.
To ensure reliable and consistent collection of logs, it's important to ensure tests cover these kind of scenarios.
Based on our experience with testing Filebeat, we evaluated the existing filelog receiver tests with respect to those scenarios.
While most of the use cases and scenarios were well-tested already, we identified a few scenarios to improve tests for to ensure reliable logs collection.<br />
At the creation time of this blog posts we were working on contributing additional tests to address the identified test coverage gaps.
You can learn more about it in <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/32001">this GitHub issue</a>.</p>
<h3>Persistence evaluation</h3>
<p>Another important aspect for log collection that we often hear from Elastic's log users are the failover handling capabilities and the delivery guarantees for logs.
Some logging use cases, for example audit logging, have strict delivery guarantee requirements.
Hence, it's important that the filelog receiver provides functionality to reliably handle situations, such as temporary unavailability of the logging backend or unexpected restarts of the OTel Collector.</p>
<p>Overall, the filelog receiver already has corresponding functionality to deal with such situations.
However, user documentation on how to setup reliable logs collection with tangible examples was an area with potential for improvement.</p>
<p>In this regard, beyond verifying the persistence and offset tracking capabilities we worked on improving respective documentation
<a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/31886">1</a> <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/30914">2</a>
and also are collaborating on a <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/31074">community reported issue</a> to ensure delivery guarantees for logs.</p>
<h3>Helping users help themselves</h3>
<p>Elastic has a long and varied history of supporting customers who use our products for log ingestion.
Drawing from this experience, we've proposed a couple of documentation improvements to the OpenTelemetry Collector to help logging users get out of some tricky situations.</p>
<p><strong>Documenting the structure of the tracking file</strong></p>
<p>For every log file the filelog receiver ingests, it needs to track how far into the file it has already read, so it knows where to start reading from when new contents are added to the file.
By default, the filelog receiver doesn't persist this tracking information to disk, but it can be configured to do so.
We felt it would be useful to <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/32180">document the structure of this tracking file</a>. When ingestion stops unexpectedly,
peeking into this tracking file can often provide clues as to where the problem may lie.</p>
<p><strong>Challenges with symlink target changes</strong></p>
<p>The filelog receiver periodically refreshes its memory of the files it's supposed to be ingesting.
The interval at which these refreshes happen is controlled by the <code>poll_interval</code> setting.
In certain setups log files being ingested by the filelog receiver are symlinks pointing to actual files.
Moreover, these symlinks can be updated to point to newer files over time.
If the symlink target changes twice before the filelog receiver has had a chance to refresh its memory, it will miss the first change and therefore not ingest the corresponding target file.
We've <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/32217">documented this edge case</a>, suggesting the users with such setups should make sure they set <code>poll_interval</code> to a sufficiently low value.</p>
<h3>Planning ahead for the receiver's GA </h3>
<p>Last but not least, we have raised the topic of making the filelog receiver a generally available (GA) component.
For users it's important to be able to rely on the stability of used functionality, hence, not being required to deal with the risk of breaking changes through minor version updates.
In this regard, for the filelog receiver we have kicked off a first plan with the maintainers to mark any issue that is a blocker for stability with a <code>required_for_ga</code>
<a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Arelease%3Arequired-for-ga+label%3Areceiver/filelog">label</a>.
Once the OpenTelemetry collector goes to version <code>v1.0.0</code> we will be able to also work towards the specific receiver’s GA.</p>
<h2>Conclusion</h2>
<p>Overall, OTel's filelog receiver component is in a good shape and provides important functionality for most log collection use cases.
Where there are still minor gaps or need for improvement with the filelog receiver, we are gladly to contribute our expertise and experience from Filebeat use cases.
The above is just the beginning of our effort to help advancing the OpenTelemetry Collector, and specifically for log collection, get closer to a stable version.
Moreover, we are happy to help the filelog receiver maintainers with general maintenance of the component, hence, dealing with community issues and PRs, jointly working on the component's roadmap, etc.</p>
<p>We'd like to thank the OTel Collector group and, in particular, <a href="https://github.com/djaglowski">Daniel Jaglowski</a> for the great and constructive collaboration on the filelog receiver, so far!</p>
<p>Stay tuned to <a href="https://www.elastic.co/observability/opentelemetry">learn more about our future contributions and involvement in OpenTelemetry</a>.</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/elastics-collaboration-opentelemetry-filelog-receiver/otel-filelog-receiver.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Elasticsearch over the years — how LogsDB cuts index size by up to 75% at no throughput cost]]></title>
            <link>https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-storage-evolution</link>
            <guid isPermaLink="false">elasticsearch-logsdb-storage-evolution</guid>
            <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[By default, Elasticsearch is optimized for retrieval, not storage. LogsDB changes that. Here's the layered architecture behind a 77% index size reduction.]]></description>
            <content:encoded><![CDATA[<p>Elasticsearch was built as a search engine. That heritage has a cost for log storage: every event fans out to multiple on-disk structures, each optimized for retrieval rather than compression. LogsDB changes both. On our nightly benchmark, Enterprise mode produces a 37.5 GB index from the same data that takes 161.9 GB without LogsDB — a 77% reduction from a single setting.</p>
&lt;div align=&quot;center&quot;&gt;
![Standard vs LogsDB storage breakdown](/assets/images/elasticsearch-logsdb-storage-evolution/storage-breakdown-v3-bold@2x.png)
&lt;/div&gt;
<h2>The write overhead</h2>
<p>Lucene, the library underneath, keeps multiple structures for every indexed document:</p>
<ul>
<li>The <strong>inverted index</strong> maps terms to documents. This is what makes text search fast.</li>
<li><strong><code>_source</code></strong> stores the original JSON blob, returned when you fetch a document.</li>
<li><strong>Doc values</strong> store field values in columns for sorting and aggregation.</li>
<li><strong>Points / BKD trees</strong> index numeric and date fields for range queries.</li>
</ul>
<p>The inverted index earns its keep: it's what lets you search a billion log lines by keyword in milliseconds, and there's no cheaper way to build that capability. <code>_source</code> exists to give you back exactly what you indexed: search results and <code>GET</code> requests return this blob directly. The problem is that it stores the full event even though the same field values are already available through doc values and the other structures.</p>
<p>Take a log event with fields like <code>host.name</code>, <code>@timestamp</code>, <code>http.response.status_code</code>, and <code>duration_ms</code>. The entire event is serialized as JSON in <code>_source</code>. The same field values are also written into doc values columns, indexed into the inverted index, and stored in BKD trees for range queries. Same data, multiple structures, each with its own on-disk footprint.</p>
<p>For a search engine where you need fast retrieval across all dimensions, that overhead is a reasonable tradeoff. For logs, where you rarely need the raw JSON and almost never do relevance-ranked search, much of it is pure waste.</p>
&lt;div align=&quot;center&quot;&gt;
![One incoming log event fans out to four on-disk structures](/assets/images/elasticsearch-logsdb-storage-evolution/dual-storage-bold@2x.png)
_One write, four on-disk structures: `_source` (the raw JSON blob), the inverted index, doc values columns, and BKD / points trees for numeric range queries. The same field values end up in multiple places._
&lt;/div&gt;
<h2>Why columnar storage matters for compression</h2>
<p>Doc values are the key to everything LogsDB does. Unlike <code>_source</code>, which stores entire documents as blobs, doc values store each field as a separate column across all documents in a Lucene segment.</p>
<p>Picture a segment with a million log events. The <code>_source</code> representation is a million JSON blobs, one per event, each containing all fields jumbled together. The doc values representation is a set of columns: one column of a million timestamps, one column of a million host names, one column of a million status codes, and so on.</p>
&lt;div align=&quot;center&quot;&gt;
![Row-oriented vs column-oriented storage](/assets/images/elasticsearch-logsdb-storage-evolution/doc-values-columns-bold@2x.png)
_Row-oriented `_source` keeps all fields for each document in one blob — doc0 through doc5 each carry `host.name`, `@timestamp`, `status`, `duration_ms`, and more jumbled together. Column-oriented doc values restructure the same data so all `host.name` values sit in one column, all timestamps in another, all status codes in another. Compression codecs can then run on each contiguous column independently._
&lt;/div&gt;
<p>That columnar layout is what makes per-column compression possible. When all values of <code>http.response.status_code</code> sit in a contiguous column, Lucene can apply codecs that exploit patterns in the sequence.</p>
<p>Delta encoding stores differences between adjacent values instead of full values. GCD encoding finds a common factor and divides everything down. Run-length encoding collapses repeats. Lucene picks the codec per segment and re-evaluates when segments merge.</p>
&lt;div align=&quot;center&quot;&gt;
![Numeric codec pipeline: RAW → DELTA → GCD → BIT-PACK](/assets/images/elasticsearch-logsdb-storage-evolution/numeric-codec-pipeline-bold@2x.png)
_Four sorted `@timestamps` from the same host, compressed in four stages. RAW: four 32-bit integers, 128 bits total. DELTA: store differences instead of full values — base stays, deltas +100, +200, +300 take 59 bits. GCD: divide out the common factor of 100, leaving 1, 2, 3 at 39 bits. BIT-PACK: pack those three small integers into contiguous bit storage, 9 bits freed._
&lt;/div&gt;
<p>But here's the catch: these codecs only work well when adjacent documents have correlated values. Consider the <code>@timestamp</code> column.</p>
<p>If logs arrive from dozens of hosts interleaved randomly, the timestamps in the column jump around. The delta between adjacent values might be +3 seconds, then -47 seconds, then +120 seconds. Delta encoding can't do much with that.</p>
<p>Now consider what happens if you sort by <code>host.name</code> and <code>@timestamp</code> before writing to the segment. All logs from host-A land in a contiguous run, followed by all logs from host-B, and so on. Within each host's run, the timestamps are monotonically increasing and the deltas are predictable.</p>
<p>Four timestamps from the same host might look like 1706745600, +100s, +200s, +300s. Delta encoding shrinks those to a base value plus three small integers.</p>
<p>GCD encoding finds that 100, 200, 300 are all divisible by 100 and stores 1, 2, 3 instead. Bit-packing then fits those three values into a handful of bits. The same pattern applies to fields like <code>host.name</code>, <code>service.name</code>, or <code>http.response.status_code</code>: within a sorted run, long stretches of identical values collapse to near nothing under run-length encoding.</p>
&lt;div align=&quot;center&quot;&gt;
![Index sorting: arrival order → sorted by host.name → after RLE](/assets/images/elasticsearch-logsdb-storage-evolution/index-sorting-bold@2x.png)
_Five hosts — api-01, api-02, db-01, web-01, web-02 — scattered randomly in arrival order (left). Sorting by `host.name` groups them into five contiguous blocks of eight (center). Run-length encoding collapses each block to a single (value, count) pair — 5 pairs stored instead of 40, the remaining slots freed (right)._
&lt;/div&gt;
<p>Elasticsearch never sorted by default. Documents landed in arrival order, compressed with DEFLATE. We left a lot on the table.</p>
<h2>How we got here: 2012–2026</h2>
<p>Not all of the individual techniques in LogsDB were designed for logs. They were built over twelve years to solve different problems, and LogsDB is what happens when you stack them.</p>
<p><strong>The foundation (2012–2017).</strong> Lucene 4.0 introduced doc values in 2012. By Elasticsearch 5.0 in 2016, they were on by default for all keyword and numeric fields. Lucene 7.0 added sparse doc values, so fields that only appear in some documents don't waste space on every document in the segment. That fixed a significant force-merge bloat problem (up to 10× on sparse fields) and set up the storage model everything else depends on.</p>
&lt;div align=&quot;center&quot;&gt;
![Dense vs sparse doc values encoding](/assets/images/elasticsearch-logsdb-storage-evolution/sparse-doc-values-bold@2x.png)
_Dense encoding reserves an 8-byte slot per document regardless of presence. Sparse encoding stores only documents that have a value at 12 bytes each (value + doc ID). For `error_code` with 2 of 16 docs populated (12% fill), sparse is 81% smaller: 24 B vs 128 B. For `request_path` at 88% fill, sparse is larger: 168 B vs 128 B. Lucene picks per field; sparse wins below ~67% fill._
&lt;/div&gt;
<p><strong>Incremental wins (2020–2021).</strong> Two smaller changes targeted observability workloads. Dictionary-based stored fields compression deduplicated repetitive string metadata for about a 10% win.</p>
<p>The <code>match_only_text</code> field type dropped term frequencies and positions from the inverted index. Term frequencies are what BM25 uses to score documents by relevance — how often a term appears in a document relative to the rest of the corpus. For log search that signal is meaningless: you don't care whether &quot;timeout&quot; appeared twice or seven times in a log line, you just want to find it. Positions are similar: they're stored so Elasticsearch can do exact phrase matching, but the position data is expensive and phrase queries on logs are rare enough that the tradeoff is worth it. When you do run a phrase query on a <code>match_only_text</code> field, it still works — it just falls back to a slower path that rescores candidates rather than using stored positions directly.</p>
&lt;div align=&quot;center&quot;&gt;
![text vs match_only_text inverted index storage](/assets/images/elasticsearch-logsdb-storage-evolution/match-only-text-bold@2x.png)
_`text` stores each term with its frequency and every position it appears at. `match_only_text` keeps only the doc IDs — enough to find the document, nothing more. The `timeout` term appears twice in this message (positions 1 and 4), which is exactly the kind of data that gets dropped._
&lt;/div&gt;
<p>Dropping frequencies and positions cuts the inverted index for a text field by roughly 40%. The overall index impact in 2021 was only ~10%, which sounds like a poor return on a 40% field-level reduction. The reason is where storage was going at the time: <code>_source</code> was stored in full for every document as a raw JSON blob, doc values were uncompressed and unsorted, and nothing was using ZSTD. The <code>message</code> field's inverted index was a small slice of a much larger, poorly-compressed whole. As the next five years of work addressed those other structures, the same 40% field-level savings became a meaningful fraction of a much smaller total.</p>
<p>Neither change was decisive on its own, but they established that log-specific storage optimization was worth pursuing.</p>
<p><strong>The TSDB turning point (April 2023).</strong> This is where the story really starts. We shipped synthetic <code>_source</code> and index sorting for time series metrics in Elasticsearch 8.7.</p>
<p>Synthetic source changes the write-and-read contract. At write time, we skip storing the raw JSON blob entirely. At read time, when a query needs to return the original document, we reconstruct it by reading each field's value out of doc values and stored fields and assembling them back into JSON. The result is functionally equivalent to the original <code>_source</code> (with minor differences like field ordering), but we never stored the blob.</p>
<p>Index sorting groups documents by dimension fields and timestamp before writing to disk. Together, synthetic source and index sorting cut metrics storage by up to 70%.</p>
<p>That result told us something important: the same architecture could work for logs.</p>
&lt;div align=&quot;center&quot;&gt;
![Standard _source vs synthetic _source](/assets/images/elasticsearch-logsdb-storage-evolution/synthetic-source-bold@2x.png)
_Without LogsDB, Elasticsearch writes every log event twice: once as a raw `_source` blob on disk, once into doc values columns. LogsDB skips the blob entirely. At read time, a `GET &lt;index&gt;/_doc/1` request gathers field values from doc values and assembles the document on the fly._
&lt;/div&gt;
<p><strong>The TSDB codec (2024).</strong> In 8.13 and 8.14, we built a custom doc values codec with run-length encoding optimized for sorted consecutive values, PFOR-delta encoding, and cyclic ordinal encoding for multi-valued dimensions. The numbers were striking: <code>kubernetes.pod.name</code> doc values dropped from 110 MB to 7.25 MB in one benchmark. We extended coverage to all numeric and keyword types including <code>ip</code>, <code>scaled_float</code>, and <code>unsigned_long</code>.</p>
<p><strong>LogsDB Tech Preview (August 2024).</strong> In <a href="https://github.com/elastic/elasticsearch/pull/108896">8.15</a>, we combined everything into <code>index.mode: logsdb</code>: host-first sorting, synthetic <code>_source</code>, ZSTD compression, and the TSDB numeric codecs. One decision mattered more than expected: sort order. Sorting by <code>host.name</code> first, then <code>@timestamp</code>, delivers up to ~40% storage reduction. Sorting by timestamp first gives ≤10%. The host-first ordering co-locates documents that share field values, which is exactly what the numeric codecs need.</p>
<p><strong>ZSTD and GA (November–December 2024).</strong> In <a href="https://github.com/elastic/elasticsearch/pull/112665">8.16</a>, we switched <code>best_compression</code> from DEFLATE to ZSTD permanently (level 3, blocks up to 2,048 documents or 240 kB, native bindings via Panama FFI on JDK 21+). ZSTD gave us ~12% smaller stored fields and ~14% higher indexing throughput at the same time, which almost never happens. LogsDB went GA in 8.17.</p>
<p>At GA, we claimed up to 65% storage reduction.</p>
<p><strong>Routing and recovery (April 2025).</strong> In 8.18, <a href="https://github.com/elastic/elasticsearch/pull/116687"><code>route_on_sort_fields</code></a> started routing documents to shards by sort field values instead of <code>_id</code>. Without this optimization, Elasticsearch hashes the <code>_id</code> to pick a shard, so logs from the same host scatter across all shards. With routing on sort fields, logs with similar <code>host.name</code> values land on the same shard. This co-locates similar documents at the shard level, not just within segments, adding ~20% storage reduction at a 1–4% ingest penalty. Routing on sort fields requires auto-generated <code>_id</code>.</p>
&lt;div align=&quot;center&quot;&gt;
![Shard routing: standard, routed, routed + sorted](/assets/images/elasticsearch-logsdb-storage-evolution/shard-routing-bold@2x.png)
_Data stream `.ds-logs-nginx-default-00001` with six hosts across three shards. STANDARD (hashed by `_id`): all host colors scattered randomly. ROUTED (`route_on_sort_fields`): same-host logs land on the same shard, but remain in arrival order within it. ROUTED + SORTED (host-first sort): each shard contains contiguous blocks of a single host — the combination that lets numeric codecs and RLE reach their full potential._
&lt;/div&gt;
<p>We also <a href="https://github.com/elastic/elasticsearch/pull/119110">switched peer recovery to synthetic source reconstruction</a>, eliminating the duplicate <code>_recovery_source</code> blob. In <a href="https://github.com/elastic/elasticsearch/pull/121049">9.0</a>, <code>logs-*-*</code> indices default to LogsDB.</p>
&lt;div align=&quot;center&quot;&gt;
![Index size written: _recovery_source eliminated](/assets/images/elasticsearch-logsdb-storage-evolution/recovery-source-bold@2x.png)
_Nightly synthetic source benchmark, December 2024. Index size written drops 39% — from ~279 GB to ~171 GB — the day peer recovery switches from copying the raw `_recovery_source` blob to reconstructing documents from doc values._
&lt;/div&gt;
<p><strong>Merge and recovery overhaul: 9.1 (July 2025).</strong> We fully eliminated the recovery source. Peer recovery uses batched synthetic reconstruction, cutting write I/O by ~50% and boosting median indexing throughput ~19% over the 8.17 baseline. We replaced up to four separate doc values merge passes with a single pass, cutting background merge CPU by up to 40%. And we swapped <code>_seq_no</code>'s BKD tree for Lucene doc value skippers, halving <code>_seq_no</code> storage.</p>
<p><strong>pattern_text and Failure Store: 9.2–9.3 (October 2025–February 2026).</strong> In <a href="https://github.com/elastic/elasticsearch/pull/124323">9.2</a>, we shipped <code>pattern_text</code> as a Tech Preview: a new field type that decomposes log messages into static templates and dynamic variable parts. A log line like <code>Session opened for user alice from 10.0.1.42 via TLS</code> gets split into the template <code>Session opened for user {} from {} via TLS</code> (stored once, as a template ID) and the variables <code>alice</code>, <code>10.0.1.42</code> (stored per document). For logs with high template repetition, this cuts message field storage by up to 50%. A companion <code>template_id</code> sub-field lets you sort by template, and the LogsDB setting <code>index.logsdb.default_sort_on_message_template</code> enables this automatically. <code>pattern_text</code> <a href="https://github.com/elastic/elasticsearch/pull/135370">went GA in 9.3</a>.</p>
&lt;div align=&quot;center&quot;&gt;
![TEXT vs PATTERN_TEXT field type](/assets/images/elasticsearch-logsdb-storage-evolution/pattern-text-bold@2x.png)
_TEXT stores each log message as a full string per document — eight copies of near-identical blobs. PATTERN_TEXT decomposes them: the shared template `Session opened for user {} from {} via TLS` is stored once with ID T0, and only the variable columns (`user`, `ip`) are stored per document — alice/10.0.1.42, bob/10.0.1.87, carol/10.0.2.11, and so on._
&lt;/div&gt;
<p><code>pattern_text</code> does come with an indexing CPU cost: decomposing each message into template and variables takes more work at write time than storing a raw string. Whether that tradeoff makes sense depends on your dataset and your priorities.</p>
<p>If your log messages follow highly repetitive patterns (structured application logs, Kubernetes events, access logs), the storage wins are large and the CPU overhead is bounded. If your messages are free-form or low-repetition, the compression gains shrink while the CPU cost stays roughly the same.</p>
<p>For data you keep for months or years, the cumulative storage reduction usually makes it worthwhile. For high-cardinality, rapidly changing messages where storage isn't the constraint, it may not be.</p>
<p>9.3 also brought compression for binary doc values, making <code>wildcard</code> field types significantly more storage-efficient. Internally, wildcard fields store an inverted index of trigrams in a binary doc values column; that column is now compressed with Zstandard instead of being stored raw. In one benchmark, a URL field dropped from 2.92 GB to 1.12 GB, more than 60% compression. If you use <code>wildcard</code> fields heavily, the gain is automatic with no mapping changes needed.</p>
<p>Also in 9.3, skip lists for <code>@timestamp</code> and <code>host.name</code> became available as an opt-in for LogsDB. Skip lists let Elasticsearch jump ahead in a doc values column without reading every entry, which speeds up time-range queries on large segments. Other index modes have skip lists disabled by default; in LogsDB you can enable them selectively for the fields you range-query most.</p>
<p>Also in 9.3, the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/failure-store">Failure Store</a> <a href="https://github.com/elastic/elasticsearch/pull/131261">became enabled by default</a> for <code>logs-*-*</code> data streams. Failed documents (mapping conflicts, ingest pipeline errors) now land in dedicated <code>::failures</code> indices instead of being rejected, which means LogsDB's strict synthetic source requirements are less likely to cause silent data loss during migration.</p>
<h2>Performance, not just storage</h2>
<p>LogsDB started as a storage optimization, and the early releases came with a throughput cost — sorting, synthetic source reconstruction, and ZSTD all add work at write time. Over two years of releases, we clawed that back. Indexing throughput is now on par with what users had before enabling LogsDB. You get the storage reduction without giving up the ingest rate you were used to.</p>
&lt;div align=&quot;center&quot;&gt;
![LogsDB throughput and storage on disk over time](/assets/images/elasticsearch-logsdb-storage-evolution/performance-over-time-bold@2x.png)
_Throughput (teal) has climbed from ~25k to ~35k docs/s since the Tech Preview. Storage on disk (blue) has dropped from ~65 GB to ~36 GB on the same benchmark dataset. Both curves move in the right direction, driven by the same layered releases: ZSTD in 8.16, routing optimization in 8.18, the merge and recovery overhaul in 9.1. Live numbers at [elasticsearch-benchmarks.elastic.co](https://elasticsearch-benchmarks.elastic.co/#tracks/logsdb/nightly/default/90d)._
&lt;/div&gt;
<p>The two trends compound each other. Less storage means fewer segments to merge, which frees CPU for indexing. Synthetic source reconstruction is cheaper to compute than it is to store and replicate the raw blob. Each release that shrank the index also reduced background I/O, which fed back into throughput.</p>
<p>The practical result: if you were running standard Elasticsearch for log ingestion two years ago, the throughput you had then is roughly what LogsDB delivers now — with a 50–75% smaller index alongside it.</p>
<h2>How to enable it</h2>
<p>As of 9.0, <code>logs-*-*</code> data streams default to LogsDB automatically. If your data streams match that pattern, you're already using it.</p>
<blockquote>
<p><strong>Want a hands-on walkthrough?</strong> <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-index-mode-storage-savings"><em>Cut Elasticsearch log storage costs by 76% with LogsDB</em></a> walks through creating two indices, reindexing, and measuring the difference with the <code>_stats</code> API — including version-specific enable instructions for 8.x clusters.</p>
</blockquote>
<p>For other index patterns, set it in your template:</p>
<pre><code class="language-json">PUT _index_template/logs-template
{
  &quot;index_patterns&quot;: [&quot;logs-*&quot;],
  &quot;template&quot;: {
    &quot;settings&quot;: {
      &quot;index.mode&quot;: &quot;logsdb&quot;
    }
  }
}
</code></pre>
<p>Synthetic <code>_source</code> turns on automatically with <code>index.mode: logsdb</code>.</p>
<p>For the routing optimization (8.18+), add one more setting:</p>
<pre><code class="language-json">PUT _index_template/logs-template
{
  &quot;index_patterns&quot;: [&quot;logs-*&quot;],
  &quot;template&quot;: {
    &quot;settings&quot;: {
      &quot;index.mode&quot;: &quot;logsdb&quot;,
      &quot;index.logsdb.route_on_sort_fields&quot;: true
    }
  }
}
</code></pre>
<p>This routes shards by sort field values instead of <code>_id</code>, adding ~20% storage reduction at a 1–4% ingestion penalty. It requires at least two sort fields beyond <code>@timestamp</code> and auto-generated <code>_id</code>.</p>
<p>Switching an existing index to LogsDB requires a reindex. So does rolling back. There's no in-place conversion, so try it on new data streams first.</p>
<p>Storage improves further as segments merge — freshly written data compresses well, but merged segments compress even better.</p>
<h2>What's next</h2>
<p>Elasticsearch still carries some structural overhead from its search engine roots. <code>_id</code> and <code>_seq_no</code> are two examples: both consume meaningful disk space (on small documents they can account for more than half the index size), but neither is essential for log analytics workloads.</p>
<p>We've already taken the first step for TSDB: <a href="https://github.com/elastic/elasticsearch/pull/144026">PR #144026</a> eliminated stored <code>_id</code> bytes from TSDB indices by reconstructing the field on the fly from doc values, the same approach synthetic <code>_source</code> uses. We're exploring the same direction for LogsDB.</p>
<p><strong>9.4 and beyond.</strong> The architecture still has room to improve, and we're on it.</p>
<p>For the full reference, see the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/logs-data-stream.html">logs data stream documentation</a>.</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/elasticsearch-logsdb-storage-evolution/elasticsearch-logsdb-storage-evolution.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Migrate Logstash Pipelines from Azure Event Hubs to OTel Collector Kafka Receiver]]></title>
            <link>https://www.elastic.co/observability-labs/blog/migrate-logstash-pipelines-from-azure-event-hubs-to-otel-collector-kafka-receiver</link>
            <guid isPermaLink="false">migrate-logstash-pipelines-from-azure-event-hubs-to-otel-collector-kafka-receiver</guid>
            <pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Step-by-step guide to migrating Logstash pipelines from the Azure Event Hubs plugin to the OpenTelemetry Collector Kafka receiver.]]></description>
            <content:encoded><![CDATA[<h2>Introduction</h2>
<p>This article is a companion guide to the <a href="https://www.elastic.co/observability-labs/blog/migrate-logstash-pipelines-from-azure-event-hubs-to-kafka-plugin">Logstash Azure Event Hubs to Kafka input plugin migration</a>, covering an alternative path: replacing <code>logstash-input-azure_event_hubs</code> with the OpenTelemetry Collector <code>kafka</code> receiver to consume from the Azure Event Hubs Kafka endpoint. For the reasons to migrate, authentication considerations, and key behavior changes such as offset handling, refer to the original article.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/migrate-logstash-pipelines-from-azure-event-hubs-to-otel-collector-kafka-receiver/amqp-vs-kafka_OTel.png" alt="AMQP vs Kafka protocol path comparison in Otel Collector connected to Azure Event Hubs" /></p>
<blockquote>
<p><strong>Reference</strong>: For detailed OTel Kafka receiver configuration options or parameter default values, see the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/kafkareceiver">Kafka Receiver README</a>.</p>
</blockquote>
<h2>Converting your configuration</h2>
<h3>TLS configuration</h3>
<p>Azure Event Hubs requires TLS for all Kafka connections on port 9093. The <code>tls: {}</code> block enables TLS with default settings (system CA certificates, no client certificate), which is sufficient for Azure Event Hubs. Omitting this block will cause the connection to fail because the broker expects a TLS handshake.</p>
<h3>Encoding</h3>
<p>The <code>encoding</code> field controls how the receiver interprets each Kafka message payload. For events consumed from Azure Event Hubs, the most common options are:</p>
<ul>
<li><code>text</code>: decodes the payload as text and inserts it as the body of a log record. Uses UTF-8 by default; use <code>text_&lt;ENCODING&gt;</code> (e.g., <code>text_shift_jis</code>) for other character sets.</li>
<li><code>raw</code>: inserts the payload bytes as-is into the log record body.</li>
<li><code>json</code>: decodes the payload as JSON and inserts it as the log record body.</li>
<li><code>azure_resource_logs</code>: converts Azure Resource Logs format to OpenTelemetry format.</li>
</ul>
<p>Additional encodings such as <code>otlp_proto</code>, <code>otlp_json</code>, and trace-specific formats (<code>jaeger_proto</code>, <code>zipkin_json</code>, etc.) are also available. See the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/kafkareceiver">Kafka Receiver README</a> for the full list.</p>
<h3>Basic configuration</h3>
<p>Minimal configuration to consume logs from one Event Hub with SASL/PLAIN.</p>
<pre><code class="language-yaml">receivers:
  kafka:
    brokers:
      - &quot;&lt;NAMESPACE&gt;.servicebus.windows.net:9093&quot;
    group_id: &quot;&lt;CONSUMER_GROUP_NAME&gt;&quot;
    auth:
      sasl:
        username: &quot;$ConnectionString&quot;
        password: &quot;Endpoint=sb://&lt;NAMESPACE&gt;.servicebus.windows.net/;SharedAccessKeyName=&lt;ACCESS_KEY_NAME&gt;;SharedAccessKey=&lt;ACCESS_KEY&gt;&quot;
        mechanism: &quot;PLAIN&quot;
    tls: {}
    logs:
      topics:
        - &quot;&lt;EVENT_HUB_NAME&gt;&quot;
      encoding: text
</code></pre>
<h3>Advanced configuration</h3>
<p>Example with multiple Event Hubs.</p>
<pre><code class="language-yaml">receivers:
  kafka/eh1:
    brokers:
      - &quot;&lt;NAMESPACE&gt;.servicebus.windows.net:9093&quot;
    group_id: &quot;&lt;CONSUMER_GROUP_1&gt;&quot;
    auth:
      sasl:
        username: &quot;$ConnectionString&quot;
        password: &quot;Endpoint=sb://&lt;NAMESPACE&gt;.servicebus.windows.net/;SharedAccessKeyName=&lt;KEY_1&gt;;SharedAccessKey=&lt;ACCESS_KEY_1&gt;&quot;
        mechanism: &quot;PLAIN&quot;
    tls: {}
    logs:
      topics:
        - &quot;&lt;EVENT_HUB_1&gt;&quot;
      encoding: text

  kafka/eh2:
    brokers:
      - &quot;&lt;NAMESPACE&gt;.servicebus.windows.net:9093&quot;
    group_id: &quot;&lt;CONSUMER_GROUP_2&gt;&quot;
    auth:
      sasl:
        username: &quot;$ConnectionString&quot;
        password: &quot;Endpoint=sb://&lt;NAMESPACE&gt;.servicebus.windows.net/;SharedAccessKeyName=&lt;KEY_2&gt;;SharedAccessKey=&lt;ACCESS_KEY_2&gt;&quot;
        mechanism: &quot;PLAIN&quot;
    tls: {}
    logs:
      topics:
        - &quot;&lt;EVENT_HUB_2&gt;&quot;
      encoding: text
</code></pre>
<h2>Configuration parameters mapping</h2>
<p>The following section maps each <a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-azure_event_hubs.html"><code>logstash-input-azure_event_hubs</code></a> parameter to its OpenTelemetry Collector <code>kafka</code> receiver equivalent.</p>
<ol>
<li>
<p><a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-azure_event_hubs.html#plugins-inputs-azure_event_hubs-checkpoint_interval"><code>checkpoint_interval</code></a>: Direct mapping to <code>autocommit.interval</code>.</p>
<p><strong>Units</strong>: Azure <code>checkpoint_interval</code> is in <strong>seconds</strong>. OTel <code>autocommit.interval</code> requires a duration string (e.g., <code>10s</code>, <code>500ms</code>).</p>
<p>Azure config:</p>
<pre><code class="language-ruby">input {
    azure_event_hubs {
        # ... other params ...
        checkpoint_interval =&gt; 10 # Default 5
    }
}
</code></pre>
<p>OTel receiver equivalent:</p>
<pre><code class="language-yaml">receivers:
  kafka:
    # ... other params ...
    autocommit:
      interval: 10s # Default 1s
</code></pre>
</li>
<li>
<p><a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-azure_event_hubs.html#plugins-inputs-azure_event_hubs-initial_position"><code>initial_position</code></a>: Maps to <code>initial_offset</code>.</p>
<p>Azure config:</p>
<pre><code class="language-ruby">input {
    azure_event_hubs {
        initial_position =&gt; &quot;end&quot;
    }
}
</code></pre>
<p>OTel receiver equivalent:</p>
<pre><code class="language-yaml">receivers:
  kafka:
    initial_offset: latest
</code></pre>
<p>Value mapping:</p>
<table>
<thead>
<tr>
<th>Azure value</th>
<th>OTel value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>beginning</code></td>
<td><code>earliest</code></td>
</tr>
<tr>
<td><code>end</code></td>
<td><code>latest</code> (default)</td>
</tr>
<tr>
<td><code>look_back</code></td>
<td>Not directly supported</td>
</tr>
</tbody>
</table>
<p><strong>Note:</strong> Since the Kafka receiver can't read the old Blob Storage checkpoints, it treats the migration as a first-time connection. To avoid reprocessing data the legacy plugin already handled, set <code>initial_offset: latest</code> for the initial deployment.</p>
</li>
<li>
<p><a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-azure_event_hubs.html#plugins-inputs-azure_event_hubs-max_batch_size"><code>max_batch_size</code></a>: No direct 1:1 mapping.</p>
<p>In OTel, the maximum batch of events processed cannot be directly controlled by the receiver. The receiver only controls how much data is read per fetch request using <code>min_fetch_size</code>, <code>max_fetch_size</code>, and <code>max_fetch_wait</code>.</p>
<p>The actual event batching happens at the processing layer via the <a href="https://github.com/open-telemetry/opentelemetry-collector/blob/main/processor/batchprocessor/README.md"><code>batch processor</code></a>, which groups telemetry at the configured pipeline stage.</p>
<p><strong>Units</strong>: <code>min_fetch_size</code> and <code>max_fetch_size</code> are in <strong>bytes</strong>. <code>max_fetch_wait</code> uses duration strings (e.g., <code>250ms</code>). <code>send_batch_size</code> is the <strong>number of records</strong>. <code>timeout</code> uses duration strings (e.g., <code>5s</code>).</p>
<p>Azure config:</p>
<pre><code class="language-ruby">input {
    azure_event_hubs {
        max_batch_size =&gt; 125
    }
}
</code></pre>
<p>OTel receiver example:</p>
<pre><code class="language-yaml">receivers:
  kafka:
    max_fetch_size: 2097152  # bytes (2 MiB)
    max_fetch_wait: 250ms

processors:
  batch:
    send_batch_size: 125  # number of log records
</code></pre>
</li>
<li>
<p><a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-azure_event_hubs.html#plugins-inputs-azure_event_hubs-threads"><code>threads</code></a>: No direct mapping.</p>
<p>Event Hubs distribute work by partition. A single Collector Kafka client can read from multiple partitions in parallel because the underlying Kafka client (<a href="https://pkg.go.dev/github.com/twmb/franz-go">franz-go</a>) uses internal goroutines to fetch and process partition data concurrently. This concurrency is handled internally and is not configurable via a user-facing <code>threads</code> setting.</p>
</li>
<li>
<p><a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-azure_event_hubs.html#plugins-inputs-azure_event_hubs-decorate_events"><code>decorate_events</code></a>: Not supported by Kafka receiver.</p>
</li>
</ol>
<h2>Performance comparison</h2>
<p>These results use the same test environment described in the <a href="https://www.elastic.co/observability-labs/blog/migrate-logstash-pipelines-from-azure-event-hubs-to-kafka-plugin">companion article</a>: same Event Hub namespace, same number of partitions, and same batch/thread configuration. The absolute numbers are environment-specific, but the relative difference is what matters.</p>
<table>
<thead>
<tr>
<th><strong>Component</strong></th>
<th><strong>Payload</strong></th>
<th><strong>Throughput (events/s)</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Logstash <code>azure_event_hubs</code> plugin</td>
<td>100B</td>
<td>~5700</td>
</tr>
<tr>
<td>OTel Collector <code>kafka</code> receiver</td>
<td>100B</td>
<td>~10900</td>
</tr>
<tr>
<td>Logstash <code>azure_event_hubs</code> plugin</td>
<td>1KB</td>
<td>~1500</td>
</tr>
<tr>
<td>OTel Collector <code>kafka</code> receiver</td>
<td>1KB</td>
<td>~1900</td>
</tr>
<tr>
<td>Logstash <code>azure_event_hubs</code> plugin</td>
<td>10KB</td>
<td>~170</td>
</tr>
<tr>
<td>OTel Collector <code>kafka</code> receiver</td>
<td>10KB</td>
<td>~190</td>
</tr>
</tbody>
</table>
<p>Across all payload sizes, the OTel Collector <code>kafka</code> receiver outperforms the Logstash <code>azure_event_hubs</code> plugin, with the largest gain at small payloads (~1.9x at 100B) where protocol overhead dominates, narrowing at larger sizes (~1.3x at 1KB, ~1.1x at 10KB). It does not reach the throughput of the Logstash <code>kafka</code> plugin from the <a href="https://www.elastic.co/observability-labs/blog/migrate-logstash-pipelines-from-azure-event-hubs-to-kafka-plugin">companion article</a>, but it improves on the legacy plugin across all tested payload sizes. Combined with the removal of the Blob Storage and GPv2 dependencies, the OTel Collector path removes two pieces of infrastructure that need to be provisioned, secured, and monitored.</p>
<h2>Conclusions</h2>
<p>Both migration paths eliminate the Blob Storage checkpoint dependency and improve throughput over the legacy <code>azure_event_hubs</code> plugin. The Logstash <code>kafka</code> plugin is the lower-friction option: the configuration change is minimal, the offset model carries over, and it delivers the highest throughput of the options tested. The OTel Collector <code>kafka</code> receiver is the better fit if you want to remove Logstash from the pipeline entirely and align with OpenTelemetry. It trades a lower peak throughput and no <code>decorate_events</code> equivalent for a vendor-neutral ingestion layer that can run alongside other OTel Collector pipelines in the same Collector.</p>
<h2>Next steps</h2>
<p>With the GPv1 retirement deadline (October 2026) approaching, starting this migration sooner reduces the time spent managing storage infrastructure that is no longer needed.</p>
<p>If any issues arise during migration:</p>
<ul>
<li>
<p><strong>Usage questions or help with configuration</strong>: Post on the <a href="https://github.com/open-telemetry/opentelemetry-collector/discussions">OpenTelemetry Collector GitHub Discussions</a> or the <a href="https://discuss.elastic.co/c/observability/">Elastic Discuss forum</a>.</p>
</li>
<li>
<p><strong>Bugs or unexpected behavior in the Kafka receiver</strong>: Open an issue in the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/issues">opentelemetry-collector-contrib</a> repository.</p>
</li>
</ul>
<h2>Related resources</h2>
<ul>
<li><a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/kafkareceiver">Kafka receiver documentation</a>: Full reference for all OTel Collector <code>kafka</code> receiver configuration parameters.</li>
<li><a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-azure_event_hubs.html">Azure Event Hubs input plugin documentation</a>: Full reference for the legacy plugin being replaced.</li>
<li><a href="https://www.elastic.co/observability-labs/blog/migrate-logstash-pipelines-from-azure-event-hubs-to-kafka-plugin">Logstash Azure Event Hubs to Kafka input plugin migration</a>: Companion guide covering the alternative migration path to the <code>logstash-input-kafka</code> plugin.</li>
<li><a href="https://learn.microsoft.com/en-us/azure/event-hubs/azure-event-hubs-kafka-overview">Azure Event Hubs for Apache Kafka overview</a>: Microsoft's documentation on the built-in Kafka endpoint in Event Hubs.</li>
<li><a href="https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-quotas#basic-vs-standard-vs-premium-vs-dedicated-tiers">Event Hubs quotas and tier comparison</a>: Tier requirements for Kafka protocol support.</li>
</ul>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/migrate-logstash-pipelines-from-azure-event-hubs-to-otel-collector-kafka-receiver/elastic-blog-otel-kafka.jpeg" length="0" type="image/jpeg"/>
        </item>
        <item>
            <title><![CDATA[Windows Event Log Monitoring with OpenTelemetry & Elastic Streams]]></title>
            <link>https://www.elastic.co/observability-labs/blog/windows-event-monitoring-with-opentelemetry-and-elastic-streams</link>
            <guid isPermaLink="false">windows-event-monitoring-with-opentelemetry-and-elastic-streams</guid>
            <pubDate>Thu, 05 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how to enhance Windows Event Log monitoring with OpenTelemetry for standardized ingestion and Elastic Streams for smart partitioning and analysis.]]></description>
            <content:encoded><![CDATA[<p>For system administrators and SREs, Windows Event Logs are both a goldmine and a graveyard. They contain the critical data needed to diagnose the root cause of a server crash or a security breach, but they are often buried under gigabytes of noise. Traditionally, extracting value from these logs required brittle regex parsers, manual rule creation, and a significant amount of human intuition.</p>
<p>However, the landscape of log management is shifting. By combining the industry-standard ingestion of OpenTelemetry (OTel) with the AI-driven capabilities of Elastic Streams, we can change how we monitor Windows infrastructure. This approach isn't just moving data. We are also using Large Language Models (LLMs) to understand it.</p>
<h2>The Challenge with Traditional Windows Logging</h2>
<p>Windows generates a massive variety of logs: System, Security, Application, Setup, and Forwarded Events. Within those categories, you have thousands of Event IDs. Historically, getting this data into an observability platform involved installing proprietary agents and configuring complex pipelines to strip out the XML headers and format the messages.</p>
<p>Once the data was ingested, we can try to figure out what &quot;bad&quot; looked like. You had to know in advance that Event ID 7031 indicated a service crash, and then write a specific alert for it. If you missed a specific Event ID or if the format changed, your monitoring went dark.</p>
<h2>Step 1: Ingestion via OpenTelemetry</h2>
<p>The first step in modernizing this workflow is adopting OpenTelemetry. The OTel collector has matured significantly and now offers robust support for Windows environments. By installing the collector directly on Windows servers, you can configure receivers to tap into the event log subsystems.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/windows-event-monitoring-with-opentelemetry-and-elastic-streams/otel-config.png" alt="OTel collector configuration for Windows Event Logs" /></p>
<p>The beauty of this approach is standardization. You aren't locked into a vendor-specific shipping agent. The OTel collector acts as a universal router, grabbing the logs and sending them to your observability backend in this case, the Elastic logs index designed to handle high-throughput streams.</p>
<p>The key thing to pay attention to in this configuration is how we add this transform statement:</p>
<pre><code class="language-yaml">transform/logs-streams:
  log_statements:
    - context: resource
      statements:
        - set(attributes[&quot;elasticsearch.index&quot;], &quot;logs&quot;)
</code></pre>
<p>This works with the vanilla opentelemetry collector and when the data arrives in Elastic, it tells Elastic to use the new wired streams feature which enables all the downstream AI features we discuss in later steps.</p>
<p>Checkout my example configuration <a href="https://github.com/davidgeorgehope/otel-collector-windows/blob/main/config.yaml">here</a></p>
<h2>Step 2: AI-Driven Partitioning</h2>
<p>Once the data arrives, the next challenge is organization. Dumping all Windows logs into a single <code>logs-*</code> index is a recipe for slow queries and confusion. In the past, we split indices based on hardcoded fields. Now, we can use AI to &quot;fingerprint&quot; the data.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/windows-event-monitoring-with-opentelemetry-and-elastic-streams/ai-partitioning.png" alt="AI-driven partitioning of Windows logs" /></p>
<p>This process involves analyzing the incoming stream to identify patterns. The system looks at the structure and content of the logs to determine their origin. For example, it can distinguish between a <code>Windows Security Audit</code> log and a <code>Service Control Manager</code> log purely based on the data shape.</p>
<p>The result is automatic partitioning. The system creates separate, optimized &quot;buckets&quot; or streams for each data type. You get a clean separation of concerns, Security logs go to one stream, File Manager logs to another, without having to write a single conditional routing rule. This partitioning is crucial for performance and for the next phase of the process: analysis.</p>
<h2>Step 3: Significant Events and LLM Analysis</h2>
<p>Once your data is partitioned (e.g., into a dedicated <code>Service Control Manager</code> stream), you can apply GenAI models to analyze the semantic meaning of that stream.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/windows-event-monitoring-with-opentelemetry-and-elastic-streams/llm-analysis.png" alt="LLM analysis of log streams" /></p>
<p>In a traditional setup, the system sees text strings. In an AI-driven setup, the system understands context. When an LLM analyzes the <code>Service Control Manager</code> stream, it identifies what that system is responsible for. It knows that this specific component manages the starting and stopping of system services.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/windows-event-monitoring-with-opentelemetry-and-elastic-streams/significant-events-suggestions.png" alt="Significant events suggestions from AI" /></p>
<p>Because the model understands the <em>purpose</em> of the log stream, it can generate suggestions for what constitutes a &quot;Significant Event.&quot; It doesn't need you to tell it to look for crashes; it knows that for a Service Manager, a crash is a critical failure.</p>
<h3>From Passive Storage to Proactive Suggestions</h3>
<p>The workflow effectively automates the creation of detection rules. The LLM scans the logs and generates a list of potential problems relevant to that specific dataset, such as:</p>
<ul>
<li><strong>Service Crashes:</strong> High severity anomalies where background processes terminate unexpectedly.</li>
<li><strong>Startup/Boot Failures:</strong> Critical errors preventing the OS from reaching a stable state.</li>
<li><strong>Permission Denials:</strong> Security-relevant events regarding service interactions.</li>
</ul>
<p><img src="https://www.elastic.co/observability-labs/assets/images/windows-event-monitoring-with-opentelemetry-and-elastic-streams/significant-events-list.png" alt="List of significant events detected" /></p>
<p>It bubbles these up as suggested observations. You can review a list of potential issues, see the severity the AI has assigned to them (e.g., Critical, Warning), and with a single click, generate the query required to find those logs.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/windows-event-monitoring-with-opentelemetry-and-elastic-streams/query-generation.png" alt="Auto-generated query for significant events" /></p>
<h2>Conclusion</h2>
<p>The combination of OpenTelemetry for standardized ingestion and AI-driven Streams for analysis turns the chaotic flood of Windows logs into a structured, actionable intelligence source. We are moving away from the era of &quot;log everything, look at nothing&quot; to an era where our tools understand our infrastructure as well as we do.</p>
<p>The barrier to effective monitoring is no longer technical complexity. Whether you are tracking security audits or debugging boot loops, leveraging LLMs to partition and analyze your streams is the new standard for observability.</p>
<p><a href="https://cloud.elastic.co/serverless-registration?onboarding_token=observability">Try Streams today</a></p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/windows-event-monitoring-with-opentelemetry-and-elastic-streams/ai-partitioning.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>