<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Data Management - Elastic Observability Labs]]></title>
    <description><![CDATA[Trusted security news & research from the team at Elastic.]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Data Management - Elastic Observability Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad972c1c27dbefc6/6a88d9782904ea5e8511d473/observability-labs-thumbnail.png</url>
      <link>https://www.elastic.co/observability-labs/blog/category/data-management</link>
    </image>
    <link>https://www.elastic.co/observability-labs/blog/category/data-management</link>
    <atom:link href="https://www.elastic.co/observability-labs/rss/category/data-management.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Sat, 24 Oct 2026 09:05:09 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Three regions, three Kibanas, one view: Cross-cluster search comes to Elastic Synthetics]]></title>
    <description><![CDATA[Monitors from every connected cluster appear in one Kibana, with journey waterfalls, screenshots, errors and certificates. Nothing is replicated, and each cluster still owns its monitors.]]></description>
    <content:encoded><![CDATA[<p>Until now, three regional deployments meant three browser tabs, separate sets of credentials, and different mental models to reconcile every time someone asked how checkout was doing worldwide.<a href="https://www.elastic.co/docs/explore-analyze/cross-cluster-search"> Cross-cluster search (CCS)</a> for synthetic monitoring ships in Elastic Stack 9.5. Monitors from every connected cluster appear in one Kibana, with status, journey waterfalls, screenshots, errors, and certificates. Nothing is replicated; the central cluster expands the index expression at query time and asks each remote directly, so configuration stays where it was created and each cluster still owns its monitors.</p><p><em>This feature is available in stateful deployments only and isn’t supported on Elastic Cloud Serverless.</em></p><h2>What cross-cluster search for synthetic monitoring adds to Elasticsearch CCS</h2><p>Before 9.5, Synthetics and CCS weren't integrated. Connecting clusters at the Elasticsearch layer gave you raw ping data in Discover but not in the Synthetics UI.Elastic's site reliability engineering (SRE) team <a href="https://www.elastic.co/blog/use-cross-cluster-search-elastic-cloud-observability">wrote about this pattern</a><em>,</em> using CCS to build a single pane of glass across 150+ clusters, but it applied to raw data, not the Synthetics app.</p><p>Monitor definitions are Kibana saved objects, and those objects don't travel across clusters. Wiring CCS at the Elasticsearch layer didn't make remote monitors appear in the Synthetics app. You'd get raw data in Discover, but none of the Synthetics UI experience: no status cards, journey waterfalls, step screenshots, or error drilldowns.</p><p>That's still true for raw CCS on saved objects. What shipped in 9.5 is a native integration built on top of remote cluster search. The app searches remote <code>synthetics-*</code> pings. It marks what's remote and sends you back to the origin Kibana whenever you need to change something.</p><h2>What remote monitors look like in the Synthetics UI</h2><p>Once remote clusters are configured, their monitors appear alongside your local ones throughout the Synthetics UI.</p><h3>Find remote monitors on the Overview page</h3><p>The <strong>Overview</strong> page shows remote monitors inline with local ones, tagged with a <strong>Remote</strong> badge next to the monitor type badge. Two additions help with navigation: a <strong>Remote cluster </strong>filter alongside the existing <strong>Type</strong> / <strong>Location</strong> / <strong>Tags</strong> / <strong>Frequency filters</strong>, and a <strong>Group b</strong>y → <strong>Remote cluster </strong>option. When a remote cluster filter is active, local monitors are hidden and status counts reflect only the filtered set, so you can focus on one deployment at a time.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc0d15a9409ecf14e/6ad88461e0c9dc4a23fd1db9/image1.png" alt="Elastic Synthetics Overview page in Kibana showing remote monitors with Remote badges and a Remote cluster filter" /><h3>Open a remote monitor detail page</h3><p><strong>Monitor detail </strong>pages work the same way for remote monitors as for local ones, including overview, history, errors, journey runs, step details, screenshots, network waterfalls, and duration charts.</p><p>A <strong>Remote monitor</strong> callout shows the cluster name and the originating Kibana URL, and a <strong>View on remote cluster</strong> button takes you directly to the monitor on its home instance.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd0e2ae13d11eec3d/6ad884789e74353d8074561d/image2.png" alt="Remote monitor callout on a Synthetics monitor detail page with the View on remote cluster button" /><h3>See remote TLS certificates on the Certificates page</h3><p>Transport Layer Security (TLS) certificates observed on remote clusters appear on the <strong>Certificates</strong> page, with a tooltip showing which cluster they came from.</p><h2>Can you edit remote monitors from the central Kibana?</h2><p>No, not from the central Kibana. Remote monitors are read-only where you view them because their configuration lives as a saved object in the Kibana where they were defined.</p><p><strong>Action</strong></p><p><strong>Behavior on the central Kibana</strong></p><p>View status, history, errors, journey runs, screenshots, network waterfalls</p><p>Available</p><p>Enable / Disable</p><p>Not available</p><p>Run test manually</p><p>Not available</p><p>Create SLO</p><p>Not available</p><p>Add to dashboard</p><p>Not available</p><p>Edit</p><p>Opens the origin Kibana, when that cluster’s public URL is configured</p><p>Clone</p><p>Opens the origin Kibana, when that cluster’s public URL is configured</p><p>View on remote cluster</p><p>Opens the monitor on the Kibana where it lives</p><p> To manage a remote monitor, click the <strong>View on remote cluster</strong> button on its detail page. It takes you to the monitor on the Kibana where it lives.</p><p>Keeping configuration on the origin cluster is intentional. A single source of truth for monitor definitions avoids the mess of cross-cluster writes.</p><p>One current limitation to note: alerting doesn't cross cluster boundaries yet. A remote monitor that's down shows as down in the federated UI, but it won't fire alerts in the central Kibana. Keep those rules on the cluster that owns the monitor. Federated alerting is on the roadmap.</p><h2>How to set up cross-cluster search for synthetic monitoring</h2><p>There are a few things to put in place.</p><h3>1. Connect the remote clusters in Elasticsearch</h3><p>In <a href="https://www.elastic.co/docs/deploy-manage/remote-clusters"><strong>Stack Management</strong> → <strong>Remote Clusters</strong></a>, add the Elasticsearch clusters that hold Synthetics data. Synthetics doesn't create these connections; it uses the ones already configured at the Elasticsearch level. The <a href="https://www.elastic.co/blog/search-and-replicate-data-between-your-elastic-cloud-and-on-prem-deployments">Search and replicate data between Elastic Cloud and on-prem</a> post covers the Elasticsearch-level setup in detail.</p><h3>2. Set server.publicBaseUrl on every participating Kibana</h3><p>The <strong>View on remote cluster</strong> deep link comes from a <code>kibanaUrl</code> field on ping documents, stamped from <code>server.publicBaseUrl</code>. If that setting is missing on a remote Kibana, you still see the monitor but there's no URL to click. Since kibanaUrl is written at ping time, you need to wait for new check results after setting this config before the link appears.</p><h3>3. Choose which remote clusters Synthetics includes</h3><p>Go to <a href="https://www.elastic.co/docs/solutions/observability/synthetics/configure-settings"><strong>Synthetics</strong> → <strong>Settings</strong> → <strong>Remote Clusters</strong></a>. You need the <strong>All </strong>privilege for <strong>Synthetics and Uptime</strong>. Users with <strong>Read</strong> see the form as read-only.</p><p>Toggle on <strong>Use all remote clusters</strong>, or pick specific ones. Each option shows connected versus disconnected status.</p><h3>4. Grant access to Kibana spaces</h3><p>This is one shared configuration per deployment, not a separate copy per space. Use the <strong>Spaces</strong> field to grant access to specific spaces, or select <strong>All spaces</strong>. On first save, settings are anchored to the space you're in.</p><p><em>Note that clearing the </em><em><strong>Spaces </strong></em><em>field on a subsequent save doesn’t remove access from existing spaces. You must explicitly select the spaces you want to keep.</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt21482e1dec686373/6ad884b2744ada25845686ec/image5.png" alt="Synthetics Settings Remote Clusters tab in Kibana with the Use all remote clusters toggle and Spaces access field" /><p>Open the <strong>Monitors</strong> overview, and remote monitors appear alongside your local ones.</p><h2>How the index expression resolves across clusters</h2><p>Nothing is replicated into the central cluster. CCS works by expanding the index expression at query time, which means that the central cluster never stores remote data; it just knows where to ask.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4fdcb5493e7692e7/6ad884cad26d92ccbf9f70a3/image3.png" alt="Cross-cluster search architecture for Elastic Synthetics: UI layer, server route wrapper, local and remote clusters" /><p>On each request, the server figures out which index expressions to query.</p><ul><li><p>If you select <strong>Use all remote clusters</strong>, it builds <code>synthetics-*,*:synthetics-*</code> with no connectivity check needed.</p></li><li><p>If you pick specific clusters, it calls Elasticsearch's <code>cluster.remoteInfo()</code> to check which are currently reachable and only includes those, so a disconnected remote doesn't hang your overview page.</p></li><li><p>The resolved indices are cached per space.</p></li></ul><p>The result is a combined index pattern like the following:</p>synthetics-*,cluster1:synthetics-*,cluster2:synthetics-*<p>The <strong>Overview status</strong> page and the <strong>Certificates</strong> page pick up CCS automatically, since they fan out across all configured clusters. Detail views scope each query to the right cluster. Pings, journeys, screenshots, and network events all resolve against a single remote rather than spreading across everything.</p><h2>What cross-cluster search for Synthetics does not do</h2><p>CCS for Synthetics is a visibility layer, not a management layer. You get a unified read view across clusters; each cluster stays the owner of its own monitors. If you need to create, edit, or disable monitors centrally, you still do that on the Kibana where each monitor lives.</p><p><strong>What</strong></p><p><strong>Where it lives</strong></p><p>Monitor configuration (saved objects)</p><p>Origin cluster only</p><p>Private locations</p><p>Origin cluster only</p><p>Global parameters</p><p>Origin cluster only</p><p>API keys</p><p>Origin cluster only</p><p>Alerting rules</p><p>Origin cluster only</p><p>Ping results</p><p>Federated, visible in the central Kibana</p><p>Journey steps and screenshots</p><p>Federated, visible in the central Kibana</p><p>TLS certificate status</p><p>Federated, visible in the central Kibana</p><p>CCS at the Elasticsearch level is also a prerequisite rather than the whole story. Synthetics needs its own <strong>Remote Clusters</strong> configuration to show remote monitors properly. Dashboards and Discover can still query <code>synthetics-*</code> via CCS for custom visualizations.</p><h2>Start centralized monitoring across your clusters</h2><p>Federated Synthetics ships in 9.5 for stateful deployments. If you run monitors across multiple clusters, go to <strong>Synthetics</strong> → <strong>Settings</strong> → <strong>Remote Clusters</strong> and connect your remotes.</p><p>If a remote monitor appears but the deep link is missing, set <code>server.publicBaseUrl</code> on the origin Kibana and wait for new check results. The <code>kibanaUrl</code> field is stamped on documents written after that config is in place.</p><p>We welcome your feedback in the <a href="https://discuss.elastic.co">Elastic discuss forum</a>.</p><p><strong>Documentation:</strong></p><ul><li><p><a href="https://www.elastic.co/docs/solutions/observability/synthetics/configure-settings">Configure Synthetics settings</a></p></li><li><p><a href="https://www.elastic.co/docs/solutions/observability/synthetics/analyze-data">Analyze data from synthetic monitors</a></p></li></ul><p><strong>Related reading:</strong></p><ul><li><p><a href="https://www.elastic.co/blog/use-cross-cluster-search-elastic-cloud-observability">Use Cross Cluster Search with Elastic Cloud to improve observability:</a> Elastic's SRE team on building a single pane of glass across 150+ clusters</p></li><li><p><a href="https://www.elastic.co/blog/search-and-replicate-data-between-your-elastic-cloud-and-on-prem-deployments">Search and replicate data between Elastic Cloud and on-prem:</a> CCS/cross-cluster replication (CCR) general availability (GA) announcement</p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/cross-cluster-search-synthetic-monitoring</link>
    <guid isPermaLink="false">cross-cluster-search-synthetic-monitoring</guid>
    <category><![CDATA[Data Management]]></category>
    <category><![CDATA[APM]]></category>
    <category><![CDATA[What's New]]></category>
    <dc:creator><![CDATA[Panagiota Mitsopoulou]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4a6e07790da1cb0/6ad883deff956c2d46ad8547/image4.png" length="0" type="image/png"/>
    <pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[One ES|QL query, 365 days of audit logs: Same search on hot data and searchable snapshots]]></title>
    <description><![CDATA[Collect audit logs with the EDOT Collector, then confirm the 365-day retention policy actually applied before an auditor asks you to.]]></description>
    <content:encoded><![CDATA[<p>The same Elasticsearch Query Language (ES|QL) query that reconstructs a user’s activity on live data runs unchanged against a mounted searchable snapshot, which is what makes a 365-day audit logging policy workable. You investigate a year-old event the way you investigate yesterday’s.</p><p>In this article, we follow six audit events from a company’s admin console through the Elastic Distribution of OpenTelemetry (EDOT) Collector into a dedicated data stream. One of them is a user named <code>alex</code>, who fails two login tries but gets in on the third and then attempts a role change. We reconstruct that timeline with one ES|QL query and read back the retention policy to confirm that it applied. Then we rerun the same query on a snapshot-backed index. The evidence comes back in a form that you can hand to an auditor and explain.</p><h2>Prerequisites</h2><ul><li><p>Elasticsearch and Kibana 9.5+</p></li><li><p>Docker, to run the <code>elastic-otel-collector:9.5</code> container</p></li><li><p>An API key created by a superuser</p></li></ul><p>You can find the companion notebook <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/observability-labs/audit-logging-elastic-stack/audit-logging-elastic-stack.ipynb">here</a>. It reproduces the whole lab from this article, including the collector configuration and the searchable snapshot, and the steps in between, and it cleans up at the end.</p><h2>What audit logs tell you that application logs don’t</h2><p>Application logs usually explain how software behaved, such as a timeout or an exception, or even a failed connection. Audit logs establish accountability by recording an actor, action, target, outcome, and time. They can help debugging, too, but their value is in reconstructing activity and demonstrating how controls operated.</p><h3>A Kubernetes RoleBinding change captured in full</h3><p>In Kubernetes, a Role defines permissions and a RoleBinding grants them to an identity within a namespace. Changes to both are auditable when the <a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/">audit policy</a> includes those resources. Here’s a real RoleBinding creation that was captured in an isolated Kubernetes 1.35.0 lab. It was collected through OpenTelemetry (OTel) and queried in Elasticsearch</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd6368a6e9f21787f/6acdf401cb361920b7dbcbba/image1.png" alt="Kubernetes audit log diagram: kubernetes-admin creates a RoleBinding granting deploy-bot the pod-reader role" /><p>The successful request grants the <code>deploy-bot</code> service account the existing <code>pod-reader</code> role in <code>payments</code>. The <a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding">RoleBinding's subjects and role reference</a> come from its request body, captured by this lab's targeted <code>Request</code> audit level. <code>Metadata</code> logging alone would omit that body.</p><h3>Where audit logs come from</h3><p>Common sources cover different parts of the story:</p><p><strong>Source</strong></p><p><strong>Questions it can answer</strong></p><p>Kubernetes API audit logs</p><p>Who requested a resource change, and how did the API server respond?</p><p>Cloud provider trails</p><p>Which principal performed an API operation on which resource?</p><p>Authentication systems</p><p>Who signed in or failed authentication? Who changed privileges?</p><p>Database audit logs</p><p>Who accessed a sensitive object or changed database permissions?</p><p>Collection starts with enabling the right events at the source. Cloud data-access events may need separate configuration; database slow-query logs aren’t a complete access audit. For Kubernetes setup, use the existing <a href="https://www.elastic.co/observability-labs/blog/bringing-your-cloud-managed-kubernetes-audit-logs-into-elasticsearch">managed Kubernetes audit-log tutorial</a>.</p><h2>Four requirements for an audit logging pipeline</h2><p>Four requirements shape an audit pipeline:</p><p><strong>Requirement</strong></p><p><strong>Design decision</strong></p><p>Long retention</p><p>Keep audit data in streams with an explicit retention policy, separate from short-lived operational logs.</p><p>Immutability</p><p>Define who shouldn’t be able to alter or delete records, and for how long. A read-only index doesn’t prevent privileged administration.</p><p>Tamper-evidence</p><p>Preserve originals and independently protected verification material so changes or missing records can be detected.</p><p>Access control</p><p>Separate collection, investigation, and retention administration; record changes to those controls.</p><p>For <a href="https://www.aicpa-cima.com/resources/landing/system-and-organization-controls-soc-suite-of-services">System and Organization Controls 2 (SOC 2)</a> or <a href="https://www.iso.org/standard/27001">ISO/IEC 27001</a>, logs support the evidence behind your controls; storing them doesn’t establish compliance. Agree which activities to record, how long to retain each copy, who can retrieve it, and how exceptions or legal holds work. Neither framework gives this lab a universal 365-day retention requirement.</p><p>Search and preservation need complementary controls. For example, <a href="https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-log-file-validation-intro.html">CloudTrail integrity validation</a> uses signed digest files that must actually be validated. Where write-once retention is required, design a source archive using appropriate controls, such as <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html">S3 Object Lock</a>. An indexed JSON string isn’t a substitute for that archive or its validation material.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt00cef31f4742cb6b/6acdf45d7cefb1fa34ce3582/image3.png" alt="Audit log pipeline: JSON events to EDOT Collector filelog, Elasticsearch data stream, ES|QL in Kibana" /><h2>How to collect audit logs with the EDOT Collector</h2><p>The retention walk-through uses six fictitious application-audit JSON events: four for the user <code>alex</code> and two for a second user, <code>priya</code>, so the actor filter has something to exclude. The <a href="https://www.elastic.co/docs/reference/edot-collector">EDOT Collector</a> runs as a container and ships to Elasticsearch and Kibana 9.5.3 on Elastic Cloud Hosted (ECH). The configuration is based on Elastic's documented <a href="https://www.elastic.co/docs/reference/edot-collector/config/configure-logs-collection">file log collection and JSON parsing procedure</a>: a <code>filelog</code> receiver with a <code>json_parser</code> operator and a resource processor that sets <code>data_stream.dataset</code> and <code>data_stream.namespace</code>, along with the <code>elasticsearch</code> exporter, in <code>otel</code> mapping mode. This article focuses on the decisions and results; the complete configuration and commands are in the companion notebook. </p><h3>Why the json_parser uses parse_to: attributes</h3><p>The collector parses the source timestamp and fields and then sends records to the dedicated <code>logs-audit_demo.otel-lab</code> data stream. The exporter builds that name from the dataset and namespace and appends .otel to the dataset.</p><p>Unlike the documented example, which uses <code>parse_to: body</code>, the lab's <code>json_parser</code> uses <code>parse_to: attributes</code>, so the parsed values go under <code>attributes</code> and the original JSON string stays in <code>body.text</code>. With <code>parse_to: body</code>, the fields would end up in <code>body.structured</code> and the original string would be lost. All six stored strings matched the fixture lines in this test. Elastic Common Schema–based (ECS-based) integrations can use root fields, such as <code>user.name</code>, so check the schema before reusing a query.</p><h3>Reconstruct an actor’s timeline with one ES|QL query</h3><p>In Kibana Discover, select <a href="https://www.elastic.co/docs/reference/query-languages/esql"><strong>ES|QL</strong></a>, Elastic's piped query language. The time picker uses the time zone of the <code>dateFormat:tz</code> advanced setting, which defaults to the browser's, so set it to UTC in <strong>Stack Management</strong> &gt;<strong> Advanced Settings</strong> before entering the absolute window <code>2026-09-07T04:59:00Z</code> through <code>2026-09-07T05:06:00Z</code>. Then run the following:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1d1b251b05fb2c0d/6acdf4ad2b0ddfd2d4870db3/image6.png" alt="ES|QL query returning four audit log events for user alex from the logs-audit_demo.otel-lab data stream" /><p>Notice that the last event is a failed role change. So far, we only know that alex tried to change a role and was denied, which isn’t the same as a permission change or a compromised account. To find out what happened, the next step is to look at identity-provider and resource activity within the same tenant and time window.</p><p>When comparing the screenshot with the query, keep in mind that the fixture uses UTC and Kibana displays timestamps in the browser's time zone (in this case, UTC−05:00). That’s why the rows show 00:00, while the time window is set in UTC.</p><p>Discover's time picker constrains this query; API clients need an explicit timestamp filter, and the notebook adds one. When handing evidence to a reviewer, record:</p><ul><li><p>The query.</p></li><li><p>The UTC window.</p></li><li><p>The source.</p></li><li><p>The retrieval time.</p></li><li><p>The result limit.</p></li></ul><p><code>LIMIT 100</code> is a bounded investigation view, not an exhaustive export.</p><h3>Confirm that every audit event arrived</h3><p>The data stream held six records with six distinct event IDs before and after a normal collector restart, because the <code>filelog</code> receiver keeps its file offsets and the exporter keeps its sending queue in the <code>file_storage</code> extension. At scale, also monitor source coverage, parse failures, queue saturation, and delivery lag. This six-event check is a functional test, not an ingestion benchmark or an exactly once guarantee.</p><h2>Set a log retention policy that you can verify</h2><p>A retention setting should be something that you can verify. On ECH, the built-in <code>logs-*-*</code> templates hand new data streams to the <code>logs</code> index lifecycle management (ILM) policy, so the lab gave its stream a dedicated index template, configured as follows: </p><ul><li><p>Priority 500, higher than the built-in logs templates that also match the stream name, so Elasticsearch applies this template instead of them.</p></li><li><p>Copies the built-in <code>logs-otel@template</code> component list.</p></li><li><p>Restricted to <code>logs-audit_demo.otel-*</code>.</p></li><li><p>Sets <code>index.lifecycle.prefer_ilm</code> to false.</p></li><li><p>Defines a 365-day <a href="https://www.elastic.co/docs/manage-data/lifecycle/data-stream">data stream lifecycle</a>.</p></li></ul><p>The data stream API then reported the backing index as managed by data stream lifecycle, and the lifecycle read-back reported <code>effective_retention: 365d</code>:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt84f935218424e641/6acdf4e2bcf6e02117e55c13/image7.png" alt="Kibana Dev Tools lifecycle API response showing 365d data retention on the audit log data stream" /><h3>Why your audit data stream ignores the retention policy you set</h3><p>Data stream lifecycle retention is a minimum retention mechanism based on backing-index lifecycle timing, not an exact per-event deletion deadline. A successful read-back proves configuration, not a year of availability. Rehearse retrieving the oldest required records, and review source coverage regularly. For the retention interface and ILM comparison, see <a href="https://www.elastic.co/observability-labs/blog/simplifying-retention-management-with-streams">retention management with Streams</a>.</p><h3>Capacity planning for long log retention</h3><p>Long retention also changes capacity planning. Daily ingest volume multiplied by retention days gives a starting raw volume; mappings, compression, replicas, query concurrency, and tier placement affect the actual resources needed. Size and test against representative investigations across realistic data ages.</p><h3>Query older audit logs with searchable snapshots</h3><p><a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore/searchable-snapshots">Searchable snapshots</a> keep snapshot-backed indices queryable through Elasticsearch. Fully mounted indices can serve older data from the cold tier; partially mounted indices use a shared local cache in the frozen tier. This gives recent and older evidence a common query surface, with storage and retrieval tradeoffs to test for your workload:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta2529daa1ead6e14/6acdf50b6ac96c019fa0d4e4/image5.png" alt="Searchable snapshots diagram: hot, cold and frozen tiers keeping older audit logs queryable with ES|QL" /><p>The audit lab took a snapshot of its six-event backing index and mounted it. It then reran the same actor query:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd5170c364d51f0be/6acdf5262b822ecbd992177c/image2.png" alt="The same ES|QL audit log query rerun on a mounted searchable snapshot, returning the same four events" /><p>The same four events were returned. This used full_copy on available hot nodes with an Enterprise license; it didn’t benchmark cold/frozen costs or latency. The backing snapshot must remain available while its mounted index uses it, and local cache isn’t a backup. The separate lab mount also needs its own retention and cleanup plan.</p><h2>Who can read and change your audit logs</h2><p>Use <a href="https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/kibana-role-management">role-based access</a> to give investigators <code>read</code> and <code>view_index_metadata</code> on the required audit data, plus Discover read access in the intended Kibana space. Keep retention and snapshot administration in separate roles. Roles are additive, so a reader role doesn’t remove permissions granted elsewhere.</p><p>The collector's API key only carries <code>create_doc</code> and <code>auto_configure</code> on its stream, and the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-security-has-privileges">has privileges API</a> confirmed that it has no <code>index</code>, <code>write</code>, <code>delete</code>, or <code>manage</code> privilege there. That limits the collector's authority; it doesn’t make the records immutable against administrators. Also plan <a href="https://www.elastic.co/docs/deploy-manage/security/logging-configuration/enabling-audit-logs">Elasticsearch and Kibana audit logging</a> for the evidence platform itself, subject to deployment and subscription requirements.</p><h3>Connecting Kubernetes audit logs to container runtime data</h3><p>For the Security Labs crossover, <a href="https://www.elastic.co/security-labs/threat-command/kubernetes-audit-logs-container-escape">correlating Kubernetes audit logs with container runtime data</a> shows how API activity connects to what runs inside a workload. Stable identities and resource context make that connection possible. Our small application fixture demonstrates timeline reconstruction; it doesn’t validate those Kubernetes detection rules.</p><h2>Where to start with audit log management</h2><p>Start with one audit source and one question that you must answer months later. Verify delivery, preserve the required originals, rehearse searches across recent and retained data, and review who can change the evidence path. Elastic brings collection, structured investigation, searchable retention, and scoped access together; your policy defines the evidence that those capabilities must preserve.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/audit-logging-retention-elasticsearch</link>
    <guid isPermaLink="false">audit-logging-retention-elasticsearch</guid>
    <category><![CDATA[Logs Analytics]]></category>
    <category><![CDATA[OpenTelemetry]]></category>
    <category><![CDATA[Data Management]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte2fed541b53cb56d/6acdf37498f3c875c1b79c99/image4.png" length="0" type="image/png"/>
    <pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[14 alerts, 1 incident: Measuring alerting rule noise with ES|QL in Elasticsearch]]></title>
    <description><![CDATA[Grouping keys decide how many alerts one incident produces, and because Kibana stamps every alert document with a rule revision, you can measure your alert noise reduction against the same incident that caused the alerts in the first place.]]></description>
    <content:encoded><![CDATA[<p>One bad deploy on a payments platform opened 18 alert episodes in seven minutes, all from one incident. A single miscalibrated rule produced 14 of those episodes across only 7 alert instances. The median episode stayed active for one minute, so most of them had recovered before anyone could open the page.</p>
<p>This is an alert noise reduction exercise done with queries. Kibana writes every alert into a hidden index and keeps it there after recovery, so we put three questions to that <a href="https://www.elastic.co/docs/explore-analyze/alerts-cases/alerts">alert history</a> with ES|QL. Was this one incident or many, where did it start, and which rule produced more noise than signal? Then we change the grouping key on the noisy rule and replay the same failure, and the same query comes back with one episode that lasts seven minutes.</p>
<p>Kibana stamps every alert document with a rule revision, so the before and after of that tuning change sit in one index. At the end, an <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a> agent answers the same questions over the same alert history.</p>
<p>Everything below was tested on Elasticsearch and Kibana 9.5.2 on Elastic Cloud, with OpenTelemetry Collector Contrib 0.157.0.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8859a2c225fa1e53/6ab8cbec4d574224fabc2701/01-architecture.png" alt="Three payments services emit logs through the OTel Collector into one Elasticsearch data stream, where three rules turn them into alert episodes stored in .alerts-*: fourteen short episodes at revision 0 against one long episode at revision 1, all queried back with ES|QL and joined against the service catalog" /></p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Elasticsearch and Kibana 9.5 or later, on <a href="https://www.elastic.co/cloud">Elastic Cloud</a> or <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">self-managed</a>.</li>
<li>Kibana privileges to create and edit <a href="https://www.elastic.co/docs/explore-analyze/alerts-cases/alerts/rule-type-es-query">Elasticsearch query rules</a> and <a href="https://www.elastic.co/docs/solutions/observability/incident-management/create-custom-threshold-rule">Observability custom threshold rules</a>, plus read access to the <code>.alerts-*</code> indices.</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-collector-releases">OpenTelemetry Collector Contrib</a> 0.157.0 or later, and an Elasticsearch API key with ingest privileges for the OTLP endpoint.</li>
<li><a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a> enabled, for the final section only. Everything before it is plain ES|QL and works without it.</li>
</ul>
<h2 id="whyalerthistorybeatsalertnotifications">Why alert history beats alert notifications</h2>
<p>A notification answers one short-lived question: who should look at this right now?</p>
<p>Most of the questions that make alerting better arrive later, and they are questions about data:</p>
<ul>
<li>Did 18 alert episodes represent 18 problems, or one incident evaluated from 18 angles?  </li>
<li>Which service showed the first symptom, and did the failure follow the dependency chain?  </li>
<li>Which rules produce short, repetitive episodes that nobody can act on?  </li>
<li>Did last month's threshold change actually reduce noise?</li>
</ul>
<p>Inside Elastic, this framing has a name: alerts as data.</p>
<p>The idea is that every alert and every state transition is a fact in time, stored with explicit semantics, so correlation becomes a query across streams rather than a bespoke feature.</p>
<p>It also matches what shows up in real operations channels: a single infrastructure failure can open dozens of alerts within seconds, and flapping alerts with miscalibrated thresholds are a familiar source of page fatigue.</p>
<p>Both problems are hard to solve from notifications and straightforward to study from preserved alert history.</p>
<h2 id="whatkibanaalertingstoresineveryalertdocument">What Kibana alerting stores in every alert document</h2>
<p>Kibana <a href="https://www.elastic.co/docs/explore-analyze/alerts-cases/alerts">alerting rules</a> write alerts-as-data documents into hidden indices that you can query like any other index.</p>
<p>The two aliases used in this article are <code>.alerts-stack.alerts-default</code> for Elasticsearch query rules and <code>.alerts-observability.threshold.alerts-default</code> for the Observability custom threshold rule.</p>
<p>The unit of storage is what we will call an episode: one document that covers one alert instance from the moment it becomes active until it recovers.</p>
<p>The fields that matter for investigation:</p>
<p>| Field | What it tells you |
| :---- | :---- |
| <code>kibana.alert.instance.id</code> | The grouping-key value this episode belongs to |
| <code>kibana.alert.start</code>, <code>kibana.alert.end</code> | When the episode opened and recovered |
| <code>kibana.alert.duration.us</code> | How long it stayed active |
| <code>kibana.alert.status</code> | <code>active</code> or <code>recovered</code> |
| <code>kibana.alert.flapping</code>, <code>kibana.alert.flapping_history</code> | Whether Kibana marked it as rapidly changing state |
| <code>kibana.alert.rule.name</code>, <code>kibana.alert.rule.revision</code> | Which rule, and which version of it |
| <code>kibana.alert.grouping</code> | The structured grouping values, for example <code>service.name</code> |
| <code>kibana.alert.reason</code> | The human-readable condition at trigger time |
| <code>kibana.alert.url</code> | A deep link back to the rule's data view |</p>
<p>Two properties make the rest of the article possible.</p>
<p>First, a new episode for the same instance creates a new document, so counting documents counts episodes.</p>
<p>Alert documents and notifications are decoupled: whether an episode also pages anyone depends on the actions attached to the rule and on their frequency, which can be every check, only on status changes, or a summary on a custom interval.</p>
<p>Our three rules have no actions attached, so nobody was paged during the lab; everything we count from here on is an alert document.</p>
<p>Second, grouping fields with ECS names such as <code>service.name</code> are copied to the top level of the alert document, which means alert history can join against other indices.</p>
<p>One caveat before you query: alert indices are wide.</p>
<p>The alerts index on this deployment maps roughly 1,400 fields, so every ES|QL query in this article projects a narrow set of columns with <code>KEEP</code>, and you should do the same, especially in tools that feed an LLM.</p>
<p>Because these are hidden system indices, the Discover editor cannot always introspect their fields, so it may flag the query with a warning or even an error badge while still running it and returning rows. Trust the result count in the toolbar, not the badge.</p>
<p>One more orientation note: Kibana screenshots below render timestamps in the browser's local time (UTC-5 in this lab), while the query result tables in the text use UTC.</p>
<h2 id="thelabhowonebaddeployproducesanalertstorm">The lab: how one bad deploy produces an alert storm</h2>
<p>To generate an honest storm, we need services that fail in a chain, not a single threshold breach.</p>
<p>The scenario: <code>fraud-scorer</code> 2.3.0 ships with slow model loading, the payments queue backs up, <code>payment-gateway</code> requests start timing out, and customers see failed checkouts on <code>payments-web</code>.</p>
<p>Every service writes structured JSON logs, and an <a href="https://github.com/open-telemetry/opentelemetry-collector-releases">OpenTelemetry Collector</a> tails them with a <code>filelog</code> receiver and ships them over OTLP/HTTP to the deployment's <a href="https://www.elastic.co/docs/reference/opentelemetry">Elastic OpenTelemetry endpoint</a> using an API key.</p>
<pre><code>receivers:
  filelog/payment_gateway:
    include:
      - ${env:LAB_DIR}/events/payment-gateway.jsonl
    start_at: end
    resource:
      service.name: payment-gateway
      service.namespace: payments
      service.version: 5.1.2
      cloud.region: us-central1
    operators:
      - id: parse_json
        type: json_parser
        parse_from: body
      - id: parse_severity
        type: severity_parser
        parse_from: attributes.severity_text
      - id: use_message_as_body
        type: move
        from: attributes.message
        to: body

exporters:
  otlphttp/elasticsearch:
    endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT}
    headers:
      Authorization: ApiKey ${env:ES_API_KEY}
</code></pre>
<p>A transform processor routes everything into one data stream, <code>logs-payments.stormlab-default</code>, and the records land with <code>service.name</code>, <code>log.level</code>, custom string attributes under <code>labels.*</code>, and numeric attributes under <code>numeric_labels.*</code>.</p>
<p>The full collector configuration for all three services, and a notebook that reproduces the whole lab, are in the <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/observability-labs/alerts-as-data-log-derived-signals-v2">companion repository</a>.</p>
<p>We also load one small index that will do a lot of work later: a service catalog.</p>
<pre><code>PUT payments_service_catalog
{
  "settings": { "index.mode": "lookup" },
  "mappings": {
    "properties": {
      "service.name": { "type": "keyword" },
      "team":         { "type": "keyword" },
      "tier":         { "type": "keyword" },
      "depends_on":   { "type": "keyword" }
    }
  }
}
</code></pre>
<p>Three documents map each service to its owning team, tier, and upstream dependency: <code>fraud-scorer</code> depends on <code>payments-queue</code>, <code>payment-gateway</code> on <code>fraud-scorer</code>, and <code>payments-web</code> on <code>payment-gateway</code>.</p>
<pre><code>POST payments_service_catalog/_bulk
{ "index": {} }
{ "service.name": "fraud-scorer",    "team": "risk-ml",       "tier": "backend",  "depends_on": "payments-queue" }
{ "index": {} }
{ "service.name": "payment-gateway", "team": "payments-core", "tier": "edge",     "depends_on": "fraud-scorer" }
{ "index": {} }
{ "service.name": "payments-web",    "team": "storefront",    "tier": "frontend", "depends_on": "payment-gateway" }
</code></pre>
<p><code>index.mode: lookup</code> is what later allows <a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-lookup-join"><code>LOOKUP JOIN</code></a> from ES|QL.</p>
<h3 id="howgroupingkeysdecidehowmanyalertsyouget">How grouping keys decide how many alerts you get</h3>
<p>The storm is observed by three alerting rules, and their grouping keys are the real subject of this article.</p>
<p>| Rule | Rule type | Grouping key | Window | Threshold |
| :---- | :---- | :---- | :---- | :---- |
| <code>gateway-5xx-per-endpoint-status</code> | Elasticsearch query rule (ES|QL mode) | <code>labels.endpoint</code>, <code>labels.status_code</code>, <code>cloud.region</code> | 1 minute | 3 or more errors |
| <code>fraud-scorer-queue-lag</code> | Elasticsearch query rule (ES|QL mode) | <code>service.name</code>, <code>cloud.region</code> | 1 minute | Max lag of 30 seconds or more |
| <code>payments-error-rate-per-service</code> | Observability custom threshold rule | <code>service.name</code> | 2 minutes | More than 5 <code>ERROR</code> documents |</p>
<p>The first rule is deliberately miscalibrated in a way we have all shipped at some point: it groups gateway errors by endpoint and status code with a one-minute window and a low threshold.</p>
<pre><code>FROM logs-payments.stormlab*
| WHERE service.name == "payment-gateway" AND log.level == "ERROR"
| STATS error_count = COUNT(*) BY labels.endpoint, labels.status_code, cloud.region
| WHERE error_count &gt;= 3
</code></pre>
<p>It runs every minute as an Elasticsearch query rule in ES|QL mode, and each returned row becomes one alert instance, so <code>/api/payments,503,us-central1</code> and <code>/api/payments/confirm,504,us-central1</code> alert separately.</p>
<p>The second rule watches the first domino directly: consumer lag on the fraud queue, reported by the service in <code>numeric_labels.queue_lag_seconds</code>.</p>
<pre><code>FROM logs-payments.stormlab*
| WHERE service.name == "fraud-scorer" AND numeric_labels.queue_lag_seconds IS NOT NULL
| STATS max_lag_seconds = MAX(numeric_labels.queue_lag_seconds) BY service.name, cloud.region
| WHERE max_lag_seconds &gt;= 30
</code></pre>
<p>The third rule is an Observability <a href="https://www.elastic.co/docs/solutions/observability/incident-management/create-custom-threshold-rule">custom threshold rule</a>: more than five <code>ERROR</code> documents in two minutes, grouped by <code>service.name</code>.</p>
<p>It represents the sane default: one stable alert instance per affected service. It also demonstrates that different rule types feed the same investigation because all of them write alerts-as-data documents.</p>
<p>All three rules carry the tag <code>alerts-as-data-v2</code>, which is how every query below isolates them.</p>
<p>Tags on rules are not cosmetic: <code>kibana.alert.rule.tags</code> is stored on every alert document, so a tag is effectively a queryable dataset label for your alert history.</p>
<h3 id="replayingtheincident18alertepisodesinsevenminutes">Replaying the incident: 18 alert episodes in seven minutes</h3>
<p>We replay the incident: three minutes of healthy baseline, the 2.3.0 deploy, queue lag climbing past the threshold, six minutes of rotating gateway 5xx bursts, customer-visible errors, then a rollback and recovery.</p>
<p>The Alerts page during the storm looks like the pager felt.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltef758fb7ee9a57f4/6ab8cbff25e2f26bda5b00c3/02-alerts-storm.png" alt="Kibana Alerts page during the storm: 12 episodes already open or recovered, six active at once, and the same endpoint and status instances flipping between Active and Recovered" /></p>
<p>Notice the table, not just the count.</p>
<p>The same instance IDs appear both as <code>Active</code> and as <code>Recovered</code> within minutes of each other, which is churn you can see but not yet measure.</p>
<p>When the dust settles, the preserved history can be queried in Discover's ES|QL mode.</p>
<p>The raw material of the storm, the error logs themselves, confirms the cascade shape:</p>
<pre><code>FROM logs-payments.stormlab*
| WHERE labels.incident_id == "payments-brownout-20260722" AND log.level == "ERROR"
| STATS errors = COUNT(*) BY minute = DATE_TRUNC(1 minutes, @timestamp), service.name
| SORT minute
</code></pre>
<p>![Discover ES|QL view of error counts per minute and service: fraud-scorer errors first, then payment-gateway at seven per minute, then payments-web]]<a href="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1ced7aef9d407283/6ab8cc24688f6518c589ec0b/04-first-symptoms.png">3</a></p>
<p>Now we leave the logs behind and interrogate only the alert history.</p>
<h2 id="howtotellifanalertstormwasoneincidentormany">How to tell if an alert storm was one incident or many</h2>
<pre><code>FROM .alerts-*
| WHERE kibana.alert.rule.tags == "alerts-as-data-v2"
| STATS episodes = COUNT(*),
        alert_instances = COUNT_DISTINCT(kibana.alert.instance.id),
        first_episode = MIN(kibana.alert.start),
        last_episode = MAX(kibana.alert.start)
  BY rule = kibana.alert.rule.name
| SORT episodes DESC
</code></pre>
<p>| rule | episodes | alert_instances | first_episode | last_episode |
| :---- | ----: | ----: | :---- | :---- |
| gateway-5xx-per-endpoint-status | 14 | 7 | 16:36:38 | 16:42:38 |
| payments-error-rate-per-service | 3 | 3 | 16:36:41 | 16:37:41 |
| fraud-scorer-queue-lag | 1 | 1 | 16:35:35 | 16:35:35 |</p>
<p>Eighteen episodes, one incident, and every one of them opened inside a seven-minute window.</p>
<p>The gateway rule alone produced 14 of the 18 episodes while tracking only 7 distinct instances, which means half of its episodes were re-opens of something that had just recovered.</p>
<p>That single row is the quantified version of a complaint that usually stays anecdotal: this rule fires more than it helps.</p>
<h2 id="howtofindwhichservicefailedfirstwithesql">How to find which service failed first with ES|QL</h2>
<p>Because the grouping fields were ECS names, each episode carries <code>service.name</code> at the top level, and we can join the alert history against the service catalog.</p>
<pre><code>FROM .alerts-*
| WHERE kibana.alert.rule.tags == "alerts-as-data-v2" AND service.name IS NOT NULL
| STATS first_symptom = MIN(kibana.alert.start) BY service.name
| LOOKUP JOIN payments_service_catalog ON service.name
| KEEP first_symptom, service.name, team, tier, depends_on
| SORT first_symptom
</code></pre>
<p>| first_symptom | service.name | team | tier | depends_on |
| :---- | :---- | :---- | :---- | :---- |
| 16:35:35 | fraud-scorer | risk-ml | backend | payments-queue |
| 16:36:41 | payment-gateway | payments-core | edge | fraud-scorer |
| 16:37:41 | payments-web | storefront | frontend | payment-gateway |</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1ced7aef9d407283/6ab8cc24688f6518c589ec0b/04-first-symptoms.png" alt="Discover ES|QL view of the first-symptom query joining alert history with the payments service catalog" /></p>
<p>The alert order matches the dependency chain exactly: the first symptom belongs to team risk-ml, the edge degraded 66 seconds later, and customers noticed a minute after that.</p>
<p>There is a subtler deduction available in this table.</p>
<p>The earliest alerting service depends on <code>payments-queue</code>, which never alerted at all, so the most likely true origin sits one hop upstream of the first visible symptom, and that is where a post-incident review should start reading logs.</p>
<p>Also note which rule is absent from this analysis: the noisy gateway rule grouped by endpoint and status code, so its episodes carry no <code>service.name</code> and cannot participate in the topology join.</p>
<p>A grouping key is the data contract for your future investigations.</p>
<h2 id="howtoscorewhichalertingruleisnoisiest">How to score which alerting rule is noisiest</h2>
<p>A noise scorecard query condenses rule quality into one table.</p>
<pre><code>FROM .alerts-*
| WHERE kibana.alert.rule.tags == "alerts-as-data-v2"
| EVAL minutes_active = COALESCE(kibana.alert.duration.us, 0) / 60000000.0
| STATS episodes = COUNT(*),
        alert_instances = COUNT_DISTINCT(kibana.alert.instance.id),
        flapping_episodes = SUM(CASE(kibana.alert.flapping == true, 1, 0)),
        median_minutes_active = MEDIAN(minutes_active)
  BY rule = kibana.alert.rule.name, revision = kibana.alert.rule.revision
| SORT episodes DESC
</code></pre>
<p>| rule | revision | episodes | instances | flapping | median minutes active |
| :---- | ----: | ----: | ----: | ----: | ----: |
| gateway-5xx-per-endpoint-status | 0 | 14 | 7 | 0 | 1.0 |
| payments-error-rate-per-service | 0 | 3 | 3 | 0 | 7.0 |
| fraud-scorer-queue-lag | 0 | 1 | 1 | 0 | 9.0 |</p>
<p>Read the last column first.</p>
<p>A median active time of one minute means the typical episode from the gateway rule was already resolving itself before the on-call could even sign in, while the two well-grouped rules produced episodes long enough to represent the actual incident.</p>
<p>The <code>flapping</code> column is Kibana's own attempt at the same problem, and it is damage control.</p>
<p>The real fix is upstream, in the rule definition, and the alert history has just told us exactly what to change: the grouping key manufactures instances, and the one-minute window manufactures re-opens.</p>
<h2 id="alertnoisereductionfixingtheruleandprovingitworked">Alert noise reduction: fixing the rule and proving it worked</h2>
<p>The rule's own detail page already summarizes the problem: 14 alerts in 24 hours, every one of them from a single incident, produced by the definition on the right.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte457ea7bdb8c61f6/6ab8cc35f9d7ed34d45e8cc3/05-rule-detail-rev0.png" alt="Rule detail page of the noisy gateway rule: 14 alerts, zero active, with the endpoint and status grouping visible in the ES|QL definition" /></p>
<p>We update the rule rather than replace it, changing the grouping to <code>service.name</code>, widening the window to five minutes, and raising the threshold.</p>
<pre><code>FROM logs-payments.stormlab*
| WHERE service.name == "payment-gateway" AND log.level == "ERROR"
| STATS error_count = COUNT(*) BY service.name, cloud.region
| WHERE error_count &gt;= 10
</code></pre>
<p>Updating a rule in place matters for one specific reason: Kibana stamps every alert document with <code>kibana.alert.rule.revision</code>, so the before and after of a tuning change are separable in the same index with no bookkeeping on our side.</p>
<p>Then we replay a compressed version of the same failure: same deploy, same lag ramp, same rotating gateway bursts, same customer impact, rollback after seven minutes.</p>
<p>The scorecard query runs unchanged, and now the revision column earns its place:</p>
<p>| rule | revision | episodes | instances | flapping | median minutes active |
| :---- | ----: | ----: | ----: | ----: | ----: |
| gateway-5xx-per-endpoint-status | 0 | 14 | 7 | 0 | 1.0 |
| payments-error-rate-per-service | 0 | 6 | 3 | 0 | 6.0 |
| fraud-scorer-queue-lag | 0 | 2 | 1 | 0 | 8.0 |
| gateway-5xx-per-endpoint-status | 1 | 1 | 1 | 0 | 7.0 |</p>
<p>The unchanged rules now show both incidents in their rows, six and two episodes, which is exactly what stable rules should do.</p>
<p>The gateway rule splits into two rows because the revision changed, and the contrast is the entire argument of this article in four numbers.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb8f816570cfca616/6ab8cc45fb64e075972dd74e/06-noise-scorecard.png" alt="Discover ES|QL view of the noise scorecard grouped by rule and revision, showing revision 1 of the gateway rule producing a single stable episode" /></p>
<p>Same failure shape, one episode instead of fourteen, and the median episode now lasts seven minutes instead of one, so it spans the incident instead of a fraction of it.</p>
<p>This is the part of alerting work that usually relies on faith, and here it is a query result: the tuning change is verified against the same incident pattern, in the same index, by the same ES|QL.</p>
<p>Nothing about this loop is specific to our lab rule.</p>
<p>Any rule that tags its alerts can be scored, tuned, and re-scored this way, which turns alert tuning from an opinion into a measurement.</p>
<h2 id="queryingalerthistorywithanagentbuilderagent">Querying alert history with an Agent Builder agent</h2>
<p>Every query above is deterministic, which makes them ideal tools for an AI agent.</p>
<p>We register three ES|QL tools in <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a>, each one a parameterized version of a query from this article with an explicit <code>KEEP</code> projection, and wire them into an agent called Alert Historian.</p>
<pre><code>POST kbn:/api/agent_builder/tools
{
  "id": "alert_noise_scorecard",
  "type": "esql",
  "description": "Noise scorecard per alerting rule and revision: episodes, distinct instances, flapping episodes, median minutes active.",
  "configuration": {
    "query": "FROM .alerts-* | WHERE kibana.alert.rule.tags == \"alerts-as-data-v2\" AND DATE_DIFF(\"hours\", kibana.alert.start, NOW()) &lt;= ?lookback_hours | EVAL minutes_active = COALESCE(kibana.alert.duration.us, 0) / 60000000.0 | STATS episodes = COUNT(*), alert_instances = COUNT_DISTINCT(kibana.alert.instance.id), flapping_episodes = SUM(CASE(kibana.alert.flapping == true, 1, 0)), median_minutes_active = MEDIAN(minutes_active) BY rule = kibana.alert.rule.name, revision = kibana.alert.rule.revision | KEEP rule, revision, episodes, alert_instances, flapping_episodes, median_minutes_active | SORT episodes DESC",
    "params": {
      "lookback_hours": {
        "type": "integer",
        "description": "How many hours of alert history to analyze, for example 6"
      }
    }
  }
}
</code></pre>
<p>The agent's instructions define the vocabulary (an episode is one alert document), the method (timeline first, then propagation, then scorecard), and the honesty rules (report numbers and timestamps from tool output, never invent alerts).</p>
<p>We ask it one question: what happened in the last three hours, was it one incident or many, which team owns the first symptom, and did the gateway rule tuning reduce noise?</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4ef21427e82f02b3/6ab8cc572f8bb23e405e8070/07-agent-two-storms.png" alt="Agent Builder conversation where Alert Historian identifies two storms separated by a quiet gap, with 23 episodes across 3 rules" /></p>
<p>The agent called all three tools and got the structure right: it separated the history into two storms with a quiet gap, one per failure wave, rather than blending them into one event.</p>
<p>It attributed the first symptom to team risk-ml, reproduced the upstream deduction about the queue that never alerted, and then answered the tuning question with the revision table.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ebe9a3561cff889/6ab8cc696ad09533cb4259df/08-agent-summary.png" alt="End of the Alert Historian conversation: team risk-ml named as the owner of the first symptom, the deduction about the upstream queue that never alerted, and revision 0 compared against revision 1" /></p>
<p>Its verdict on the fix reads like a review comment backed by data: fourteen episodes at revision 0 against one stable episode at revision 1, with the median episode going from one minute to seven.</p>
<p>The agent is not doing anything we could not do by hand.</p>
<p>That is precisely the point: because alerts are durable, structured data with explicit semantics, a language model can reason over patterns and histories instead of reacting to a single notification, and every number it states is traceable to a tool call you can rerun.</p>
<h2 id="runningalerthistoryqueriesinproduction">Running alert history queries in production</h2>
<p>A few things to decide before you rely on alert history in production.</p>
<h3 id="retentionisalearningwindowdecision">Retention is a learning-window decision</h3>
<p>Alert indices follow an ILM policy, and the default keeps far more than the few hours this lab needed, but if you want quarter-over-quarter rule quality trends, align the policy with that ambition deliberately.</p>
<h3 id="scopealertindexreadaccessnarrowly">Scope alert index read access narrowly</h3>
<p>Alert documents embed rule parameters, reasons, and deep links, so treat <code>.alerts-*</code> read privileges like any other operational dataset and grant the narrowest index patterns that work, for example only the observability alert aliases.</p>
<h3 id="groupingkeysareadatacontract">Grouping keys are a data contract</h3>
<p>Use ECS field names in <code>BY</code> clauses when they exist, keep cardinality bounded, and never put secrets or personal data into grouping values, because they become <code>kibana.alert.instance.id</code> and live for the retention window.</p>
<h3 id="projectcolumnsaggressivelywithkeep">Project columns aggressively with <code>KEEP</code></h3>
<p>With around 1,400 mapped fields in the alert indices, an unprojected query is unpleasant for humans and actively harmful inside LLM tools, so end every reusable query with <code>KEEP</code>.</p>
<h3 id="alertstormsalsostressyourautomation">Alert storms also stress your automation</h3>
<p>If a rule triggers a workflow per alert, fourteen episodes mean fourteen executions doing the same work, which is another argument for pointing automation at the aggregated alert dataset rather than at each notification.</p>
<h2 id="wheretostartwithyourownnoisiestrule">Where to start with your own noisiest rule</h2>
<p>The lab in this article is intentionally small, and every piece of it generalizes.</p>
<p>Score your noisiest real rule with the scorecard query, fix its grouping or window, and let <code>kibana.alert.rule.revision</code> tell you whether the fix held.</p>
<p>Join your alert history against whatever catalog you already have; a CMDB export in a lookup index is enough, and the propagation order becomes a query.</p>
<p>If you use Agent Builder, wrap your two or three most-used investigation queries as tools, and the next storm review starts with a conversation instead of a wall of notifications.</p>
<p>The <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/observability-labs/alerts-as-data-log-derived-signals-v2">companion notebook</a> builds everything used here: the catalog index, the log data stream, the three rules, both incident replays, the queries, and the Agent Builder tools and agent. It runs for about 25 minutes because the rules evaluate once a minute and the episode durations are the data.</p>
<p>A notification tells you something broke.</p>
<p>The alert history tells you how your detection is performing, and it is already sitting in an index waiting to be queried.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/esql-alert-noise-reduction</link>
    <guid isPermaLink="false">esql-alert-noise-reduction</guid>
    <category><![CDATA[Incident Management]]></category>
    <category><![CDATA[Logs Analytics]]></category>
    <category><![CDATA[Data Management]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2306ef56ba70a634/6ab8cbb44d5742d665bc26fd/header.png" length="0" type="image/png"/>
    <pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Cross-project search for Elastic Observability: one query across every linked project]]></title>
    <description><![CDATA[Keep your observability data where it lives, and still search, alert, and monitor across every linked Serverless project, easily!]]></description>
    <content:encoded><![CDATA[<p>A single query can now reach every Serverless project you own. Link your projects once and every search, dashboard and alerting rule after that can span all of them. Discover, dashboards and ES|QL all operate with no duplication of data between projects and no remote index prefixes in your queries. Cross-project search (CPS) is <a href="https://www.elastic.co/blog/cross-project-search-elastic-serverless-ga">generally available</a> on <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a>.</p><p>If you run Observability on Elastic Cloud Hosted (ECH), this will feel familiar. <a href="https://www.elastic.co/docs/explore-analyze/cross-cluster-search">Cross-cluster search</a> (CCS) has long enabled teams to query across remote clusters, and it's a mainstay in large scale ECH environments. CPS takes the same idea to Serverless, and makes linking even simpler: no topology mapping, certificate configuration or per connection credentials. You link projects in the Cloud console and Elastic manages the rest.</p><h2>What is cross-project search?</h2><p>Serverless organizes your data into <strong>projects</strong>: Elasticsearch, Observability, or Security, each with its own scaling, permissions, and lifecycle. That isolation is a feature: it preserves data locality where needed, simplifies cost attribution, and lets teams own their own data. The tradeoff, historically, has been visibility. If your logs live in one project and your APM data lives in another, or if every business unit runs its own Observability project, getting a unified view has typically meant duplicating data into a shared store.</p><p>CPS removes that tradeoff. You designate an <strong>origin project</strong> (the project you search from) and link it to other projects you want included in that search. Once linked, an unqualified query like <code>FROM logs*</code> or a Discover session automatically spans the origin plus every linked project; no special syntax, no remote index prefixes. Kibana's project picker lets anyone narrow a query to just the current project or expand it to everything in scope, and administrators can set a default scope per Kibana space so a local incident-response space stays local by default while a global operations space starts broad. Access is enforced per project using your existing Elastic Cloud roles, so linking a project for search doesn't quietly grant broader permissions than a user already has.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2cee1f522a706b1c/6ab29c62a8762835045d68c9/Screenshot_2026-09-22_at_6.18.44_p.m..png" alt="Diagram showing an origin Observability project linking to multiple linked projects across regions and project types, with queries fanning out and results merging back automatically" /><p></p><h3>What's the difference between cross-project search and cross-cluster search?</h3><h3></h3><p></p><p>Cross-cluster search (ECH, self-managed)</p><p>Cross-project search (Serverless)</p><p>Where you link</p><p>Remote cluster connections, configured per cluster</p><p>Link projects in the Elastic Cloud console</p><p>Credentials</p><p>Certificates or API keys you issue and rotate</p><p>Managed by Elastic</p><p>Network setup</p><p>Network topology you map and maintain</p><p>None to configure</p><p>Query syntax</p><p>Remote cluster name referenced in the query</p><p>Unqualified query, no remote index prefix</p><p>Scope control</p><p>Set per query</p><p>Project picker, per-space defaults, and tag-based routing</p><p>Where CCS asks administrators to configure remote cluster connections, exchange certificates or API keys, and reference remote cluster names in every query, CPS asks you to link projects in the UI and query the way you already do. It's the same underlying idea: remote search without moving data, adapted to a control plane where infrastructure is meant to disappear.</p><h2>Why cross-project search matters for Observability teams</h2><p>Cross-project search matters most the moment your observability data stops living in one place. Observability organizations typically split their data by team, environment, region, or customer, usually for good reasons: isolating noisy neighbors, controlling blast radius, meeting data residency requirements, or giving each team ownership over its own ingest and retention. The problem shows up during an incident or a cross-cutting investigation, when someone needs to see across those boundaries without waiting for a data migration or standing up a consolidated cluster.</p><p>A few examples of where CPS changes the equation:</p><h3>Keep observability data in its region and still search globally</h3><p>Teams operating in multiple regions or clouds often need to keep observability data in the region it was generated, whether for regulatory reasons or to limit cross-region egress. CPS lets each region keep its own Observability project while a central project links to all of them, so an SRE team can run one query to check whether an anomaly in one region is also showing up elsewhere, without pulling all that data into one place first.</p><h3>Isolate environments without losing the incident view</h3><p>Splitting production, staging, and per-service or per-team projects makes scaling and cost predictable, and it limits how far a bad deploy or a runaway job can spread. It also means an on-call engineer used to have to check several places during an incident. With CPS, on-call can work from a single origin project and pull in whichever linked environments matter to the investigation, then narrow back down once things are resolved.</p><h3>Central teams monitoring many business units or customers</h3><p>Platform or Observability Center of Excellence (CoE) teams that operate on behalf of many business units, or Managed Service Providers (MSPs) monitoring several customer environments, can give each unit or customer its own project, with its own retention, access, and cost, while the central team links across all of them for cross-cutting reporting and anomaly detection. Each project owner keeps control of their own data; the center gets the aggregate view it needs.</p><h3>Avoid ingesting the same data twice</h3><p>When two teams need the same data for different purposes, the old options were duplicate ingestion pipelines or a shared, less isolated deployment. With linked projects, data is ingested once and queried from wherever it's needed, which keeps both storage costs and pipeline maintenance down.</p><p>This applies across solution types too: CPS links Elasticsearch, Observability, and Security projects interchangeably, so a Security team and an Observability team can each keep their own project, with their own retention and access controls, while querying the same underlying logs. A security analyst investigating an alert can query infrastructure or service context from a linked Observability project in Discover, dashboards, or ES|QL, and an SRE debugging an incident can query related signals from a linked Security project the same way, all without either team re-ingesting the other's data.</p><h2>What does cross-project search support in Elastic Observability?</h2><p>At GA, CPS is available across Elasticsearch, Observability, and Security project types (Observability and Security projects require the Complete tier). For Elastic Observability specifically, application and infrastructure monitoring, alerting rules, SLOs, machine learning anomaly detection (including APM anomaly detection), and Synthetics work across linked projects, alongside the platform-level capabilities every project gets: Discover, dashboards, and ES|QL. Project routing lets you target a query to a subset of linked projects using predefined tags (alias, cloud provider, region) or your own custom tags, combined with boolean logic, so "all EU projects" or "every environment except staging" can be expressed once and reused.</p><p>This release also raises the default number of projects you can link to a single origin, well beyond the tech preview limit, with room to go higher for larger deployments. Any existing project can now act as an origin rather than only newly created ones. For programmatic access, you only need a Cloud API key to call CPS from your own tooling. A new dedicated cloud role can now grant those keys for programmatic access.</p><p>A few nuances are worth knowing before you design around CPS:</p><ul><li><strong>Alerting</strong>: Alerting rules defined in the origin project can evaluate data across linked projects. The alerts they generate stay local to the project where the alerting rule lives.</li><li><strong>SLOs</strong>: SLOs don't follow the project picker in the Kibana header. Instead, you set scope in the SLO create and edit form, choosing this project, all linked projects, or a specific subset, since changing scope rebuilds the SLO's underlying computation. You can't view SLOs created in linked projects from the current project, but you can create an SLO here based on data that lives in those projects.</li><li><strong>Synthetics</strong>: Overview, monitor details, certificates, errors, and journeys honor the Kibana header project picker and show linked-project monitors as read-only. Creating, editing, and managing monitors stay in the origin project, and Synthetics status and TLS rules still evaluate origin monitors only.</li><li><strong>Streams</strong>: Not CPS-aware yet; Streams stay scoped to the project where they were created.</li></ul><h2>How do you set up cross-project search?</h2><p>Setting up cross-project search takes a few minutes:</p><ol><li>From your origin project's settings in the Elastic Cloud console, open <strong>Link projects</strong>.</li><li>Select the projects you want to link, individually or in bulk.</li><li>Save.</li><li>Open Discover, run an ES|QL query, or build a dashboard as you normally would: linked project data is included automatically, and the project picker lets you narrow or widen the scope at any time.</li></ol><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd604c155e0ef0f17/6aad2c627e66d4c520015488/cps-link-projects-screenshot-embedded.svg" alt="The Link projects screen in the Elastic Cloud console, showing project search and filters (project type, cloud provider, region, tags) and a list of candidate projects to select for linking" /><p>For the full configuration walkthrough, including linking topologies (hub-and-spoke, shared data project, or full mesh) and how to scope project routing with tags and expressions, see the <a href="https://www.elastic.co/docs/deploy-manage/cross-project-search-config">cross-project search documentation</a> and the <a href="https://www.elastic.co/docs/solutions/observability/cross-project-search">Observability-specific guide</a>.</p><h2>Cross-project search pricing</h2><p>CPS pricing has two components: a charge for data retained in each linked project you query, and a charge for data egress generated by cross-project query traffic, tracked as its own line item so you can see what CPS is adding to your transfer costs. There's no separate per-link fee. Because actual cost depends on your data volumes, retention settings, and how many projects you link, use the <a href="https://cloud.elastic.co/pricing/serverless">Elastic Cloud Serverless pricing estimator</a> to model costs for your own environment against your own ingest and retention numbers.</p><h2>Start using cross-project search</h2><p>If you're already running Elastic Observability on Serverless, cross-project search is available in your project settings today: link your projects and run your first cross-project query in Discover. If you haven't tried Elastic Cloud Serverless yet, <a href="https://cloud.elastic.co/serverless-registration">start a free trial</a> and see how far a single query can reach.</p><p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p><p><em>Elastic, Elasticsearch, and associated marks are trademarks, logos, or registered trademarks of Elasticsearch B.V. in the United States and other countries. All other company and product names are trademarks, logos, or registered trademarks of their respective owners.</em></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/cross-project-search-serverless-observability</link>
    <guid isPermaLink="false">cross-project-search-serverless-observability</guid>
    <category><![CDATA[Data Management]]></category>
    <category><![CDATA[Logs Analytics]]></category>
    <category><![CDATA[Incident Management]]></category>
    <dc:creator><![CDATA[Vinay Chandrasekhar]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt871cded13b7f8cb8/6aad22506ea6417ea141fe46/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Migrate Datadog Kubernetes dashboards to Elastic Observability in under an hour]]></title>
    <description><![CDATA[See how the migration CLI translates a real Datadog Kubernetes dashboard into validated Kibana panels and uploads it to your cluster in under an hour, no manual widget rebuilds required.]]></description>
    <content:encoded><![CDATA[<p>The <a href="https://github.com/elastic/observability-migration-platform">Observability Migration Platform</a> takes a Datadog Kubernetes dashboard and turns it into ES|QL-backed Lens panels in Kibana. It validates queries against your live cluster before upload, and the whole process typically fits in under an hour. This walkthrough uses the <strong>Kubernetes - Overview</strong> board: pod CPU, working set memory, pod phases, and CrashLoopBackOff counts. Elasticsearch runs ES|QL time series queries up to 30× faster than Prometheus on common gauge and counter workloads in published benchmarks, with up to 2.5× better storage efficiency. Review the migration report and enable alerts when you are ready.</p>
<h2 id="thedatadogkubernetesdashboardusedinthismigration">The Datadog Kubernetes dashboard used in this migration</h2>
<p>The walkthrough uses <strong>Kubernetes - Overview</strong> from <code>infra/datadog/dashboards/integrations/kubernetes.json</code> in the migration repository. It is a cluster-wide board with the signals operators check during an incident: pod counts, CPU and memory by host or pod, non-running pods, and containers stuck in CrashLoopBackOff.</p>
<p>Below are representative queries from the source dashboard:</p>
<pre><code># Pod CPU by host
sum:kubernetes.cpu.usage.total{$scope,$cluster,$label,$node} by {host}
</code></pre>
<pre><code># Pod memory by pod
sum:kubernetes.memory.usage{$scope,$deployment,$statefulset,$replicaset,$daemonset,$cluster,$namespace,!pod_name:no_pod,$label,$service,$node} by {pod_name}
</code></pre>
<pre><code># Pods not running (pressure / scheduling signal)
sum:kubernetes_state.pod.status_phase{$scope,$cluster,$namespace,$deployment,$statefulset,$replicaset,$daemonset,!pod_phase:running,!pod_phase:succeeded,$label,$node,$service} by {kube_cluster_name,kube_namespace,pod_phase}
</code></pre>
<pre><code># CrashLoopBackOff
sum:kubernetes_state.container.status_report.count.waiting{$cluster,$namespace,$deployment,$statefulset,$replicaset,$daemonset,reason:crashloopbackoff,$scope,$daemonset,$label,$node,$service} by {pod_name}
</code></pre>
<p>If this board translates cleanly, most production Datadog Kubernetes folders are worth testing with the same workflow.</p>
<h2 id="whydatadogtoelasticmigrationisfasternow">Why Datadog-to-Elastic migration is faster now</h2>
<p>The migration platform automates the query translation and panel rebuilds that used to dominate Datadog moves. Elasticsearch stores Kubernetes metrics efficiently and runs the ES|QL queries those panels use. See <a href="https://www.elastic.co/observability-labs/blog/prometheus-metrics-elasticsearch-faster-cheaper-datadog">Elasticsearch as a metrics backend</a> for benchmark context and storage comparisons.</p>
<p>The platform maps Datadog queries to Kibana panels, validates ES|QL against live data, and writes artifacts you can inspect before anything goes to production.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>You need an <a href="https://www.elastic.co/docs/solutions/observability/get-started">Elastic Observability Serverless</a> project, a <a href="https://www.elastic.co/docs/deploy-manage/api-keys/serverless-project-api-keys">project API key</a>, and the migration CLI installed from the <a href="https://github.com/elastic/observability-migration-platform">Observability Migration Platform</a> repository.</p>
<p>Export your endpoints and API key:</p>
<pre><code>export ELASTICSEARCH_ENDPOINT="https://YOUR_ES_ENDPOINT"
export KIBANA_ENDPOINT="https://YOUR_KIBANA_ENDPOINT"
export KEY="YOUR_API_KEY"
</code></pre>
<p>Install the CLI and confirm the toolchain:</p>
<pre><code>python3 -m venv .venv
.venv/bin/pip install ".[all]"
.venv/bin/obs-migrate doctor
</code></pre>
<p>The <code>doctor</code> command checks compile and lint dependencies. Resolve any errors before you migrate production dashboards. Pin a release tag if you plan to run this in CI.</p>
<p>To pull dashboards from the Datadog API instead of JSON files, copy <code>datadog_creds.env.example</code> to <code>datadog_creds.env</code> and set <code>DD_API_KEY</code>, <code>DD_APP_KEY</code>, and <code>DD_SITE</code>.</p>
<h2 id="ingestkubernetesmetricsfirst">Ingest Kubernetes metrics first</h2>
<p>Empty panels after upload usually mean Elasticsearch does not yet have the series the Datadog queries reference. Make sure to confirm ingest before you run the migration.</p>
<p>There are two common paths to do so:</p>
<ol>
<li>OpenTelemetry into managed OTLP with Kubernetes receivers (<code>kubeletstats</code>, <code>k8s_cluster</code>), then explore in Discover</li>
<li>Existing Prometheus or agent pipelines that already write pod and node metrics to <code>metrics-*</code></li>
</ol>
<p>The migration CLI accepts <code>--field-profile otel</code> to map Datadog tags such as <code>pod_name</code>, <code>kube_namespace</code>, and <code>kube_cluster_name</code> to OpenTelemetry fields like <code>kubernetes.pod.name</code> and <code>kubernetes.namespace</code>. If panels are empty after migration, verify field mapping and the selected time range before you change translator settings.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9d1b8627fecca77c/6a85cd129a32f11916a7e026/metrics-exploration.jpg" alt="Kubernetes metrics exploration in Discover with live CPU and memory charts after OpenTelemetry ingest" /></p>
<h2 id="runthedatadogdashboardmigrationcli">Run the Datadog dashboard migration CLI</h2>
<p>Export the Datadog dashboard JSON from the UI, or copy the sample <code>kubernetes.json</code> from <code>infra/datadog/dashboards/integrations/</code> in the migration repo. Place files in a directory such as <code>./datadog_k8s_exports/</code>.</p>
<p>Run the migration from that directory:</p>
<pre><code>datadog-migrate \
  --source files \
  --input-dir ./datadog_k8s_exports \
  --output-dir ./migration_output \
  --assets all \
  --field-profile otel \
  --data-view "metrics-*" \
  --logs-index "logs-*" \
  --upload \
  --kibana-url "$KIBANA_ENDPOINT" \
  --kibana-api-key "$KEY" \
  --ensure-data-views \
  --create-alert-rules \
  --validate \
  --es-url "$ELASTICSEARCH_ENDPOINT" \
  --es-api-key "$KEY"
</code></pre>
<p>These flags matter for Kubernetes boards:</p>
<ul>
<li><code>--field-profile otel</code> maps Datadog Kubernetes fields to OpenTelemetry field names in Elasticsearch</li>
<li><code>--assets all</code> includes dashboards and Datadog monitor definitions when present</li>
<li><code>--validate</code> runs emitted ES|QL against your cluster before upload</li>
<li><code>--create-alert-rules</code> creates Kibana rules in a disabled state</li>
</ul>
<p>The unified CLI performs the same work:</p>
<pre><code>obs-migrate migrate \
  --source datadog \
  --input-mode files \
  --input-dir ./datadog_k8s_exports \
  --output-dir ./migration_output \
  --assets all \
  --field-profile otel \
  --data-view "metrics-*" \
  --logs-index "logs-*" \
  --validate \
  --es-url "$ELASTICSEARCH_ENDPOINT" \
  --es-api-key "$KEY" \
  --kibana-url "$KIBANA_ENDPOINT" \
  --kibana-api-key "$KEY" \
  --upload \
  --create-alert-rules
</code></pre>
<p>To fetch a dashboard from Datadog directly:</p>
<pre><code>datadog-migrate \
  --source api \
  --env-file datadog_creds.env \
  --dashboard-ids YOUR_DASHBOARD_ID \
  --output-dir ./migration_output \
  --assets all \
  --field-profile otel \
  --data-view "metrics-*" \
  --validate \
  --es-url "$ELASTICSEARCH_ENDPOINT" \
  --es-api-key "$KEY" \
  --kibana-url "$KIBANA_ENDPOINT" \
  --kibana-api-key "$KEY" \
  --upload
</code></pre>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb0d737676b7d2089/6a85cd1543c0b782232f065e/migration-flow.png" alt="End-to-end Observability Migration Platform flow from Datadog extract through translate, validate, compile, and upload to Kibana" /></p>
<h2 id="validatethemigrateddatadogdashboardinkibana">Validate the migrated Datadog dashboard in Kibana</h2>
<p>Open Kibana → <strong>Dashboards</strong> and locate the migrated <strong>Kubernetes - Overview</strong> board. Confirm that cluster and namespace pod counts, CPU and memory series, pod phase panels, CrashLoopBackOff widgets, and deployment replica charts return data for your selected time range.</p>
<p>If you migrated monitors, open <strong>Observability → Rules</strong>. Imported rules remain disabled until you enable them after reviewing thresholds.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab0ca21249e6b216/6a85cd17078290f6ba3217a4/kibana-k8s-overview.jpg" alt="Kubernetes overview dashboard in Kibana with cluster and node CPU, memory, and readiness views after Datadog migration" /></p>
<p>The CLI also writes local artifacts under <code>./migration_output/</code>:</p>
<ul>
<li><code>dashboards/yaml/</code> contains the translated dashboard definition.</li>
<li><code>dashboards/migration_report.json</code> lists panels that translated automatically and panels flagged for manual review.</li>
<li><code>alerts/</code> contains monitor translations when monitors were included in the export.</li>
</ul>
<h2 id="handlemanualreviewpanels">Handle manual-review panels</h2>
<p>Some Datadog widget types do not translate on the first pass. Exotic formulas, log-only panels, and unsupported widgets appear as manual-review entries in the migration report rather than as silently broken charts.</p>
<p>| Result | Recommended action |
| --- | --- |
| Panel returns data | Accept the translation and continue |
| Panel is empty | Confirm metric names and <code>data_stream.dataset</code> values in <code>metrics-*</code>, then widen or shift the time range |
| Manual-review marker | Open the original Datadog query and simplify or redesign the panel |
| Monitor never fires | Confirm the rule is enabled and thresholds match your environment |</p>
<p>Datadog coverage is narrower than Grafana in some areas. Read the migration report before you commit to full parity with leadership. The platform prefers conservative failures over uploading panels that look correct but query the wrong fields.</p>
<h2 id="relateddatadogandgrafanamigrationguides">Related Datadog and Grafana migration guides</h2>
<p>For the Grafana PromQL version of this workflow, see <a href="https://www.elastic.co/observability-labs/blog/grafana-elastic-kubernetes-dashboard-migration">Migrate your Grafana Kubernetes dashboard to Elastic Observability</a>. For platform-level context, see <a href="https://www.elastic.co/observability-labs/blog/migrate-datadog-grafana-dashboards-alerts-to-kibana">Migrating Datadog and Grafana dashboards and alerts to Kibana</a>. Review <a href="https://github.com/elastic/observability-migration-platform/blob/main/docs/known-limitations.md">known limitations</a> before you migrate every production folder.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/datadog-kubernetes-dashboard-migration</link>
    <guid isPermaLink="false">datadog-kubernetes-dashboard-migration</guid>
    <category><![CDATA[Kubernetes]]></category>
    <category><![CDATA[Metrics]]></category>
    <category><![CDATA[Data Management]]></category>
    <dc:creator><![CDATA[Peter Simkins]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb63e39bf3a11e972/6a85cd1a9bf994ca880a05af/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How Prometheus Remote Write Ingestion Works in Elasticsearch]]></title>
    <description><![CDATA[A look under the hood at Elasticsearch's Prometheus Remote Write implementation: protobuf parsing, metric type inference, TSDS mapping, and data stream routing.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch recently added native support for the Prometheus Remote Write protocol.
You can point Prometheus (or Grafana Alloy) at an Elasticsearch endpoint and ship metrics without any adapter in between.</p>
<p>This post looks at what happens inside Elasticsearch when a Remote Write request arrives.</p>
<p>If you want to understand the implementation, evaluate how Elasticsearch compares to other Prometheus-compatible backends, or contribute, this is the post for you.
A companion post, <a href="https://www.elastic.co/blog/prometheus-remote-write-elasticsearch">Ship Prometheus Metrics to Elasticsearch with Remote Write</a>, covers the setup and configuration side.</p>
<h2 id="requestlifecyclefromhttptoindexeddocuments">Request lifecycle: from HTTP to indexed documents</h2>
<p>A quick note on the Prometheus data model before we dive in: Prometheus stores all metric values as 64-bit floats and treats the metric name as just another label (<code>__name__</code>).
The storage engine itself is agnostic of whether a value is a counter or a gauge.
Keep this in mind as we walk through how Elasticsearch maps these concepts.</p>
<p>Here is the full path of a Remote Write request through Elasticsearch:</p>
<ol>
<li><strong>HTTP layer</strong> — The endpoint receives a compressed protobuf payload, checks indexing pressure, decompresses with Snappy, and parses the protobuf <code>WriteRequest</code>.</li>
<li><strong>Document construction</strong> — Each sample in each time series becomes an Elasticsearch document with <code>@timestamp</code>, <code>labels.*</code>, and <code>metrics.*</code> fields.</li>
<li><strong>Bulk indexing</strong> — All documents from a single request are written to the target data stream via a single bulk call.</li>
</ol>
<p>The sections below walk through each stage in detail.</p>
<h3 id="httplayer">HTTP layer</h3>
<p>The endpoint accepts <code>application/x-protobuf</code> POST requests.
The incoming request body is tracked against the same <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/pressure">indexing pressure limits</a> that protect the bulk indexing API.
If the cluster is already under heavy indexing load, the request gets rejected with a 429 before any parsing happens.</p>
<p>Prometheus compresses Remote Write payloads with Snappy.
Elasticsearch decompresses the body in a streaming fashion without materializing it into a single contiguous allocation, and validates the declared uncompressed size against a configurable maximum to guard against decompression bombs.</p>
<p>The decompressed body is then deserialized as a protobuf <code>WriteRequest</code>.
Each <code>WriteRequest</code> contains a list of <code>TimeSeries</code> entries, and each <code>TimeSeries</code> contains a set of labels (key-value pairs) and a list of samples (timestamp + float64 value).</p>
<h3 id="documentconstruction">Document construction</h3>
<p>For each sample in each time series, Elasticsearch builds an index request.
Here is what a single document looks like:</p>
<pre><code>{
  "@timestamp": "2026-04-01T12:00:00.000Z",
  "data_stream": {
    "type": "metrics",
    "dataset": "generic.prometheus",
    "namespace": "default"
  },
  "labels": {
    "__name__": "http_requests_total",
    "job": "prometheus",
    "instance": "localhost:9090",
    "method": "GET",
    "status": "200"
  },
  "metrics": {
    "http_requests_total": 1027.0
  }
}
</code></pre>
<p>All labels from the Prometheus time series (including <code>__name__</code>) end up in the <code>labels.*</code> fields.
The metric value goes into <code>metrics.&lt;metric_name&gt;</code>, where <code>&lt;metric_name&gt;</code> is the value of the <code>__name__</code> label.</p>
<p>Time series without a <code>__name__</code> label are dropped entirely, and the samples are counted as failures.
Non-finite values (NaN, Infinity, negative Infinity) are silently skipped.
This includes Prometheus staleness markers, which use a special NaN bit pattern (<code>0x7ff0000000000002</code>) to signal that a series has disappeared.</p>
<h3 id="onesampleonedocument">One sample, one document</h3>
<p>You might wonder whether storing each individual sample as its own document creates significant storage overhead, especially for labels.
A common pattern to reduce that overhead was to group all metrics sharing the same labels and timestamp into a single document.</p>
<p>With recent TSDB improvements, that optimization is no longer necessary.
Elasticsearch has trimmed the per-document storage overhead to the point where there is negligible difference between packing many metrics in a single document and writing each sample separately.
A dedicated post covering these TSDB storage improvements in detail is coming soon.</p>
<h3 id="bulkindexing">Bulk indexing</h3>
<p>All documents from a single Remote Write request are sent to Elasticsearch via a single bulk request.
Each document targets the data stream <code>metrics-{dataset}.prometheus-{namespace}</code> and is indexed as an append-only create operation.</p>
<h2 id="metrictypeinference">Metric type inference</h2>
<p>Remote Write v1 does not reliably transmit metric types alongside samples.
Prometheus sends metadata (type, help text, unit) in separate requests roughly once per minute, and those requests may land on a different node than the samples.
Buffering samples until metadata arrives is not practical in a distributed system, so Elasticsearch infers the type from naming conventions instead.</p>
<p>Metric names ending in <code>_total</code>, <code>_sum</code>, <code>_count</code>, or <code>_bucket</code> are mapped as counters.
Everything else defaults to gauge.
This is a well-established convention that other Prometheus-compatible backends use as well.</p>
<pre><code>http_requests_total             → counter
request_duration_seconds_sum    → counter
request_duration_seconds_count  → counter
request_duration_seconds_bucket → counter
process_resident_memory_bytes   → gauge
go_goroutines                   → gauge
</code></pre>
<p>The heuristic can be wrong.
A metric like <code>temperature_total</code> (if someone named a gauge that way) would be misclassified as a counter.
The main consequence today is that some ES|QL functions like <code>rate()</code> require the metric type to be a counter and will reject a misclassified gauge.
For PromQL, we plan to lift this restriction so that <code>rate()</code> works regardless of the declared type, which will make incorrect inference less consequential.</p>
<p>You can override the inference by creating a <code>metrics-prometheus@custom</code> component template with custom dynamic templates.
For example, to treat all <code>*_counter</code> fields as counters:</p>
<pre><code>PUT /_component_template/metrics-prometheus@custom
{
  "template": {
    "mappings": {
      "dynamic_templates": [
        {
          "counter": {
            "path_match": "metrics.*_counter",
            "mapping": {
              "type": "double",
              "time_series_metric": "counter"
            }
          }
        }
      ]
    }
  }
}
</code></pre>
<p>Custom dynamic templates are merged with the built-in ones, so the default naming-convention rules still apply for metrics you don't explicitly override.</p>
<h2 id="theindextemplate">The index template</h2>
<p>Elasticsearch installs a built-in index template that matches <code>metrics-*.prometheus-*</code>.
This template is what makes field type inference work without manual mapping configuration.</p>
<p><strong>TSDS mode</strong> is enabled, which gives you time-based partitioning, optimized storage, <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds#time-series-dimension">deduplication</a>, and the ability to downsample data as it ages.</p>
<p><strong><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/passthrough">Passthrough</a> object fields</strong> are used for both the <code>labels</code> and <code>metrics</code> namespaces.
This serves three purposes:</p>
<ol>
<li><p><strong>Namespace isolation</strong>: Labels and metrics live in separate object namespaces (<code>labels.*</code> and <code>metrics.*</code>), so a label named <code>status</code> and a metric named <code>status</code> cannot conflict with each other.</p></li>
<li><p><strong>Dimension identification</strong>: The <code>labels</code> passthrough object is configured with <code>time_series_dimension: true</code>, which means every field under <code>labels.*</code> is automatically treated as a TSDS dimension.
When Prometheus sends a time series with a label you have never seen before, it becomes a dimension without any explicit field mapping.</p></li>
<li><p><strong>Transparent queries</strong>: You don't need to write the <code>labels.</code> or <code>metrics.</code> prefix in ES|QL or PromQL.
A query can reference <code>job</code> instead of <code>labels.job</code>, or <code>http_requests_total</code> instead of <code>metrics.http_requests_total</code>.
The passthrough mapping handles the resolution.</p></li>
</ol>
<p><strong>Dynamic inference for metrics</strong> applies the naming-convention heuristics described above.
When a new metric name appears for the first time, its field mapping is created automatically under <code>metrics.*</code> with the correct <code>time_series_metric</code> annotation.</p>
<p><strong>Failure store</strong> is enabled.
Documents that fail indexing (for example, due to a mapping conflict where the same metric name appears with incompatible types) are routed to a separate failure store instead of being dropped silently.</p>
<h2 id="datastreamrouting">Data stream routing</h2>
<p>The three URL patterns map directly to data stream names:</p>
<p>| URL pattern | Data stream |
|---|---|
| <code>/_prometheus/api/v1/write</code> | <code>metrics-generic.prometheus-default</code> |
| <code>/_prometheus/metrics/{dataset}/api/v1/write</code> | <code>metrics-{dataset}.prometheus-default</code> |
| <code>/_prometheus/metrics/{dataset}/{namespace}/api/v1/write</code> | <code>metrics-{dataset}.prometheus-{namespace}</code> |</p>
<p>This lets you separate metrics from different Prometheus instances or environments into different data streams.
That separation is useful for a few reasons.</p>
<p><strong>Lifecycle isolation</strong>: you can apply different retention policies per data stream.
Production metrics might be kept for 90 days, while dev metrics might expire after 7 days.</p>
<p><strong>Access control</strong>: you can scope API keys to specific data streams.
A team's Prometheus instance writes to <code>metrics-teamA.prometheus-prod</code>, and their API key only has access to that stream.</p>
<p><strong>Query performance</strong>: PromQL queries and Grafana dashboards can be scoped to a specific index pattern, avoiding scans of unrelated data.</p>
<h2 id="errorhandlingandtheremotewritespec">Error handling and the Remote Write spec</h2>
<p>The Remote Write spec defines two response classes: retryable (5xx, 429) and non-retryable (4xx).
Prometheus uses this distinction to decide whether to retry or drop a failed request.</p>
<p>Elasticsearch returns 429 (Too Many Requests) if any sample in the bulk request was rejected due to indexing pressure.
This signals Prometheus to back off and retry with exponential backoff.</p>
<p>For partial failures (some samples indexed, others rejected), the response includes a summary.
It reports how many samples failed, grouped by target index and status code, along with a sample error message from each group.</p>
<p>Time series without a <code>__name__</code> label result in a 400 error for those samples.
Non-finite values (NaN, Infinity) are silently dropped: Prometheus receives a success response and will not retry.</p>
<p>NaN appears most commonly for summary quantiles when no observations have been recorded (for example, a p99 latency metric before any requests arrive) and for staleness markers.
The practical impact of dropping these is limited today: for most queries, a missing sample behaves similarly to a NaN one, since PromQL's lookback window fills the gap with the last known value either way.
The more significant gap is staleness markers, which are covered below.</p>
<h2 id="whatsnextremotewritev2andbeyond">What's next: Remote Write v2 and beyond</h2>
<p>Remote Write v2 is still experimental, which is why the current implementation starts with v1.
But v2 addresses several of v1's shortcomings.</p>
<p><strong>Metadata alongside samples</strong>: v2 sends metric type, unit, and description with each time series in the same request.
This eliminates the need for naming-convention heuristics entirely.</p>
<p><strong>Native histograms</strong>: v2 supports Prometheus native histograms, which map naturally to Elasticsearch's <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/exponential-histogram"><code>exponential_histogram</code></a> field type.
Classic histograms (one counter per bucket boundary) are verbose and lose precision at query time.
Native histograms are more compact and more accurate.</p>
<p><strong>Dictionary encoding</strong>: v2 replaces repeated label strings with integer references, reducing payload size significantly for high-cardinality label sets.</p>
<p><strong>Created timestamps</strong>: counters in v2 include a "created" timestamp that marks when the counter was initialized.
This allows backends to detect counter resets more accurately than the current heuristic (value decreased since last sample).</p>
<p>Beyond v2, there are two other items in consideration for future enhancements.</p>
<p><strong>Staleness marker support</strong>: currently, staleness markers (the special NaN that Prometheus writes when a scrape target disappears) are dropped.
Supporting them would allow correct PromQL lookback behavior and avoid the 5-minute "trailing data" artifact where a disappeared series still appears in query results.</p>
<p><strong>Shared metric field</strong>: the current layout creates a separate field for each metric name (<code>metrics.http_requests_total</code>, <code>metrics.go_goroutines</code>, etc.).
This works, but it means the number of field mappings grows with the number of distinct metric names, which is why the field limit is set to 10,000 for Prometheus data streams.
A different approach we're considering is to store the metric name only in the <code>__name__</code> label and write the metric value to a single shared field.
This eliminates the field explosion problem entirely and more closely matches how Prometheus stores data internally.
This direction is part of the broader effort to make Elasticsearch's metrics storage more efficient and more compatible with Prometheus conventions.</p>
<h2 id="availability">Availability</h2>
<p>The Prometheus Remote Write endpoint is available now on <a href="https://cloud.elastic.co/serverless-registration">Elasticsearch Serverless</a> with no additional configuration.</p>
<p>For self-managed clusters, check out <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">start-local</a> to get up and running quickly.</p>
<p>If you run into issues or have feedback, open an issue on the <a href="https://github.com/elastic/elasticsearch">Elasticsearch repository</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch-architecture</link>
    <guid isPermaLink="false">prometheus-remote-write-elasticsearch-architecture</guid>
    <category><![CDATA[Metrics]]></category>
    <category><![CDATA[Data Management]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5bbe42711414c88f/6a7f19d2eab5be381320aaec/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to cut Elasticsearch log storage costs with LogsDB]]></title>
    <description><![CDATA[Learn how to enable LogsDB index mode in Elasticsearch and measure real storage savings. We compare a standard index against a LogsDB index using Apache logs and show how much storage you can reclaim.]]></description>
    <content:encoded><![CDATA[<p>LogsDB is a specialized Elasticsearch index mode that gives you full functionality at a fraction of the storage cost. Your Kibana dashboards, searches, alerts, and visualizations all continue to work exactly as before. No data is discarded. No queries need to be updated. No workflows break. It is one setting, and everything else gets cheaper.</p>
<p>In benchmarks, LogsDB brought a dataset from <strong>162.7 GB down to 39.4 GB</strong> — a <strong>76% reduction in storage</strong>. You can explore the full nightly benchmark results at <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/logsdb/nightly/default/90d">elasticsearch-benchmarks.elastic.co</a>.</p>
<p>In this tutorial you'll reproduce the experiment yourself using Kibana Dev Tools and an Apache logs dataset. You'll create two identical indices, ingest the same documents into both, and measure the storage difference with the <code>_stats</code> API. By the end, you'll see a 44% reduction on your test data — and understand exactly why production numbers push even higher.</p>
<blockquote>
  <p><strong>Already on Elasticsearch 9.2+?</strong> Any data stream with a <code>logs-</code> prefix already uses LogsDB by default. Jump to <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-index-mode-storage-savings#what-about-your-existing-logs">What about your existing logs?</a> to verify your setup.</p>
  <p><strong>Want the full picture?</strong> For the engineering history behind these savings — how Lucene doc values, synthetic <code>_source</code>, index sorting, and ZSTD were developed and stacked over twelve years — see <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-storage-evolution"><em>Elasticsearch over the years: how LogsDB cuts index size by up to 75%</em></a>.</p>
</blockquote>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Elasticsearch 8.17+ cluster, Elastic Cloud deployment, or Serverless</li>
<li>Kibana with Dev Tools access</li>
<li>Some logs</li>
<li>Basic familiarity with running API calls in Kibana Dev Tools</li>
</ul>
<h2 id="howlogsdbsavesstorage">How LogsDB saves storage</h2>
<p>LogsDB stacks three mechanisms to achieve its storage reduction:</p>
<ul>
<li><strong>Index sorting</strong> — documents are sorted by <code>host.name</code> then <code>@timestamp</code>, grouping similar log lines so compression codecs find far more repeated patterns. Sorting alone accounts for roughly 30% of the savings.</li>
<li><strong>ZSTD compression with delta/GCD/run-length encoding</strong> — <code>best_compression</code> switches from LZ4 to Zstandard and applies numeric codecs to each doc values column. The standard index in this tutorial uses LZ4, so part of what you're measuring is the full package LogsDB delivers automatically.</li>
<li><strong>Synthetic <code>_source</code></strong> — Elasticsearch skips storing the raw JSON blob entirely and reconstructs <code>_source</code> on demand from doc values, adding another 20–40% of savings on top.</li>
</ul>
<blockquote>
  <p><strong>Synthetic <code>_source</code> trade-offs:</strong> Field ordering in returned documents may differ from the original, and some edge cases around multi-value array fields behave differently. For most log analytics workloads these differences are invisible, but check the <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-index-mode-storage-savings#next-steps">synthetic <code>_source</code> documentation</a> before enabling it in latency-sensitive applications.</p>
</blockquote>
<p>For a deep dive into the architecture behind each mechanism, see <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-storage-evolution"><em>Elasticsearch over the years: how LogsDB cuts index size by up to 75%</em></a>.</p>
<p>Let's now walk through the steps you can take to enable LogsDB and measure the storage savings.</p>
<h2 id="step1collectlogswithelasticagent">Step 1: Collect logs with Elastic Agent</h2>
<p>The recommended way to ingest Apache logs into Elasticsearch is through Elastic Agent with the Apache integration. It handles collection, parsing, ECS field mapping, and routing automatically.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt909c6349a734ff01/6a7f09dbc2cc09cfe3249426/integration.png" alt="Elastic Agent Apache integration setup in Kibana" /></p>
<p>Browse all available integrations in the <a href="https://www.elastic.co/integrations">Elastic integrations catalog</a>.</p>
<p>Once the Agent is collecting logs and routing them to <code>logs-apache.access-*</code>, move to the next step.</p>
<h2 id="step2createthetwoindices">Step 2: Create the two indices</h2>
<p>All commands in this tutorial are run in <strong>Kibana Dev Tools</strong>.</p>
<p>Create one standard index and one LogsDB index with identical field mappings. The only difference is <code>"index.mode": "logsdb"</code>.</p>
<p><strong>Standard index:</strong></p>
<pre><code>PUT /apache-standard
{
  "mappings": {
    "properties": {
      "@timestamp":                  { "type": "date" },
      "host.name":                   { "type": "keyword" },
      "http.request.method":         { "type": "keyword" },
      "url.path":                    { "type": "keyword" },
      "http.version":                { "type": "keyword" },
      "http.response.status_code":   { "type": "integer" },
      "http.response.bytes":         { "type": "integer" },
      "http.request.referrer":       { "type": "keyword" },
      "user_agent.original":         { "type": "keyword" }
    }
  }
}
</code></pre>
<p><strong>LogsDB index:</strong></p>
<pre><code>PUT /apache-logsdb
{
  "settings": {
    "index.mode": "logsdb"
  },
  "mappings": {
    "properties": {
      "@timestamp":                  { "type": "date" },
      "host.name":                   { "type": "keyword" },
      "url.path":                    { "type": "keyword" },
      "http.request.method":         { "type": "keyword" },
      "http.version":                { "type": "keyword" },
      "http.response.status_code":   { "type": "integer" },
      "http.response.bytes":         { "type": "integer" },
      "http.request.referrer":       { "type": "keyword" },
      "user_agent.original":         { "type": "keyword" }
    }
  }
}
</code></pre>
<p>That single <code>"index.mode": "logsdb"</code> line activates all three storage mechanisms. Elasticsearch enables these additional settings behind the scenes — you don't set any of them manually:</p>
<pre><code>{
  "index.sort.field":              ["host.name", "@timestamp"],
  "index.sort.order":              ["asc", "desc"],
  "index.codec":                   "best_compression",
  "index.mapping.ignore_malformed": true,
  "index.mapping.ignore_above":    8191
}
</code></pre>
<h2 id="step3reindexthelogs">Step 3: Reindex the logs</h2>
<p>Use the <code>_reindex</code> API to copy the same documents into both test indices:</p>
<pre><code>POST /_reindex
{
  "source": { "index": "logs-apache.access-*" },
  "dest":   { "index": "apache-standard" }
}

POST /_reindex
{
  "source": { "index": "logs-apache.access-*" },
  "dest":   { "index": "apache-logsdb" }
}
</code></pre>
<p>Both indices now hold identical documents, so the storage comparison in the next step reflects only the index mode difference.</p>
<h2 id="step4forcemergeforafaircomparison">Step 4: Force merge for a fair comparison</h2>
<p>Before measuring, force merge both indices to a single segment:</p>
<pre><code>POST /apache-standard/_forcemerge?max_num_segments=1

POST /apache-logsdb/_forcemerge?max_num_segments=1
</code></pre>
<p>These calls block until the merge finishes. Wait for both responses before continuing.</p>
<p><strong>Why this matters:</strong> Elasticsearch writes data into multiple Lucene segments before merging them in the background. Measuring mid-merge gives artificially inflated numbers because each segment is compressed independently. Forcing a single segment shows the real steady-state storage footprint you'd see in a mature production index.</p>
<blockquote>
  <p><strong>Only run <code>_forcemerge</code> on indices that are no longer being written to.</strong> Force merging an index that is still receiving writes is resource-intensive and can impact ingestion performance. In production, you can use <a href="https://www.elastic.co/docs/manage-data/lifecycle/index-lifecycle-management">Index Lifecycle Management (ILM)</a> to automate force merges as part of the warm or cold phase, once an index is rolled over and no longer actively ingested into.</p>
</blockquote>
<h2 id="step5measurethedifference">Step 5: Measure the difference</h2>
<pre><code>GET /apache-standard/_stats?filter_path=indices.*.primaries.store

GET /apache-logsdb/_stats?filter_path=indices.*.primaries.store
</code></pre>
<p>The <code>filter_path</code> parameter keeps the response focused. Look for <code>primaries.store.size_in_bytes</code> in each response.</p>
<p>In our test with Apache log records, the results were:</p>
<p>| Index            | Documents | Size     |
|------------------|-----------|----------|
| apache-standard  | 111,818   | 15.37 MB |
| apache-logsdb    | 111,818   | 8.6 MB   |
| <strong>Reduction</strong>    |           | <strong>44%</strong>  |</p>
<p>To put this in perspective: at 1 TB of log data, LogsDB brings that down to around 560 GB. That's 450 GB saved without any changes to your queries. At production scale with billions of documents and synthetic <code>_source</code> enabled, savings push to 76% — taking 162.7 GB down to 39.4 GB in our benchmark.</p>
<h2 id="visualizeinkibana">Visualize in Kibana</h2>
<p>To see the storage difference visually, open Kibana and go to <strong>Management → Stack Management → Index Management</strong>. You'll see both indices listed with their current sizes side by side.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltedf2816dfc31a34b/6a7f09deb43770a7a64d6b65/index-stats.png" alt="Kibana Index Management showing storage comparison between standard and LogsDB indices" /></p>
<blockquote>
  <p><strong>Why Kibana shows larger numbers than <code>_stats</code>:</strong> Kibana Index Management displays the total index size including all replica shards. The <code>_stats</code> query above uses <code>primaries</code> to report primary shards only. The ratio between the two indices remains the same either way.</p>
</blockquote>
<h2 id="whataboutyourexistinglogs">What about your existing logs?</h2>
<h3 id="elasticsearch92alreadyenabledbydefault">Elasticsearch 9.2+ (already enabled by default)</h3>
<p>Since 9.2, any data stream matching the <code>logs-*</code> naming pattern automatically uses LogsDB. You're likely already saving storage without any configuration change.</p>
<p>Verify your existing data streams:</p>
<pre><code>GET /.ds-logs-*/_settings?filter_path=*.settings.index.mode
</code></pre>
<p>If you see <code>"index.mode": "logsdb"</code> in the responses, you're already getting the savings.</p>
<h3 id="elasticsearch8xor9091enableperdatastreamviaindextemplate">Elasticsearch 8.x or 9.0–9.1 (enable per data stream via index template)</h3>
<p>For earlier versions, enable LogsDB on a data stream by updating its index template. This affects all new indices created from that template — existing indices are not changed, so the transition is safe and gradual.</p>
<p><strong>Option A — Update an existing template:</strong></p>
<pre><code>PUT _index_template/logs-myapp-template
{
  "index_patterns": ["logs-myapp-*"],
  "data_stream": {},
  "template": {
    "settings": {
      "index.mode": "logsdb"
    }
  },
  "priority": 200
}
</code></pre>
<p><strong>Option B — Check and patch an existing integration template:</strong></p>
<p>First, find the template managing your data stream:</p>
<pre><code>GET _index_template/logs-apache*
</code></pre>
<p>Then add the <code>index.mode</code> setting to the <code>template.settings</code> block using a <code>PUT _index_template/&lt;name&gt;</code> call with the full template body including your addition.</p>
<p>After updating the template, the next index rollover will use LogsDB. Trigger a rollover immediately if you don't want to wait:</p>
<pre><code>POST /logs-myapp-default/_rollover
</code></pre>
<p><strong>Upgrading from 8.x to 9.0+:</strong> Existing data streams are not changed automatically. Only new rollovers will use LogsDB. There is no data loss and no reindexing required — the savings accumulate as new indices roll over.</p>
<h2 id="whataboutqueryperformance">What about query performance?</h2>
<p>LogsDB does not significantly impact query performance for typical log analytics workloads. The index sorting by <code>host.name</code> and <code>@timestamp</code> can actually <em>improve</em> range query and aggregation performance on those fields, since matching documents are stored adjacently. Queries that don't filter on those fields perform comparably to a standard index.</p>
<p>For indexing throughput data across releases, see the <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-storage-evolution#performance-not-just-storage">performance section</a> of the companion article.</p>
<h2 id="conclusion">Conclusion</h2>
<p>LogsDB activates with a single <code>"index.mode": "logsdb"</code> setting and delivers measurable storage savings immediately: 44% in our hands-on test, and 76% (162.7 GB → 39.4 GB) in production benchmarks with synthetic <code>_source</code>. On Elasticsearch 9.2+, <code>logs-*</code> data streams already use LogsDB by default. For 8.x or earlier 9.x clusters, a one-line index template change enables it on your next rollover with no data loss and no reindexing required.</p>
<h2 id="nextsteps">Next steps</h2>
<ul>
<li><a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/logs-data-stream-integrations">LogsDB index mode documentation</a></li>
<li><a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/logs-data-stream">Configuring a logs data stream</a></li>
<li><a href="https://www.elastic.co/search-labs/blog/elasticsearch-logsdb-index-mode">LogsDB GA announcement</a></li>
<li><a href="https://www.elastic.co/search-labs/blog/elastic-logsdb-tsds-enhancements">LogsDB and TSDS performance benchmarks</a></li>
</ul>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-index-mode-storage-savings</link>
    <guid isPermaLink="false">elasticsearch-logsdb-index-mode-storage-savings</guid>
    <category><![CDATA[Data Management]]></category>
    <category><![CDATA[Logs Analytics]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt99ec1c2ec2a7af55/6a7f09e23ce8e203b0cf5277/header.png" length="0" type="image/png"/>
    <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch over the years — how LogsDB cuts index size by up to 75% at no throughput cost]]></title>
    <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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta3a4d65c4de793d4/6a7f09e73cab1cd73c0e4734/storage-breakdown-v3-bold@2x.png" alt="Standard vs LogsDB storage breakdown" /></p>
<h2 id="thewriteoverhead">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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbf0f50c8d9b1c3c5/6a7f09e963e95944bd73dca6/dual-storage-bold@2x.png" alt="One incoming log event fans out to four on-disk structures" />
<em>One write, four on-disk structures: <code>_source</code> (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.</em></p>
<h2 id="whycolumnarstoragemattersforcompression">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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta2548bca8f377f56/6a7f09eceab5bec47820a589/doc-values-columns-bold@2x.png" alt="Row-oriented vs column-oriented storage" />
<em>Row-oriented <code>_source</code> keeps all fields for each document in one blob — doc0 through doc5 each carry <code>host.name</code>, <code>@timestamp</code>, <code>status</code>, <code>duration_ms</code>, and more jumbled together. Column-oriented doc values restructure the same data so all <code>host.name</code> values sit in one column, all timestamps in another, all status codes in another. Compression codecs can then run on each contiguous column independently.</em></p>
<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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcab1e02d2192a98b/6a7f09efb6b7345f6ae48cc6/numeric-codec-pipeline-bold@2x.png" alt="Numeric codec pipeline: RAW → DELTA → GCD → BIT-PACK" />
<em>Four sorted <code>@timestamps</code> 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.</em></p>
<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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1c5e01c48262da24/6a7f09f1de231589e4fd7afd/index-sorting-bold@2x.png" alt="Index sorting: arrival order → sorted by host.name → after RLE" />
<em>Five hosts — api-01, api-02, db-01, web-01, web-02 — scattered randomly in arrival order (left). Sorting by <code>host.name</code> 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).</em></p>
<p>Elasticsearch never sorted by default. Documents landed in arrival order, compressed with DEFLATE. We left a lot on the table.</p>
<h2 id="howwegothere20122026">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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte4b2c68318492f3d/6a7f09f4227b1c2e2b5984aa/sparse-doc-values-bold@2x.png" alt="Dense vs sparse doc values encoding" />
<em>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 <code>error_code</code> with 2 of 16 docs populated (12% fill), sparse is 81% smaller: 24 B vs 128 B. For <code>request_path</code> at 88% fill, sparse is larger: 168 B vs 128 B. Lucene picks per field; sparse wins below ~67% fill.</em></p>
<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 "timeout" 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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc4dd1dc99cf753b0/6a7f09f76693f85f60663e1f/match-only-text-bold@2x.png" alt="text vs match_only_text inverted index storage" />
<em><code>text</code> stores each term with its frequency and every position it appears at. <code>match_only_text</code> keeps only the doc IDs — enough to find the document, nothing more. The <code>timeout</code> term appears twice in this message (positions 1 and 4), which is exactly the kind of data that gets dropped.</em></p>
<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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta1aaae98650a025b/6a7f09fa3cab1c48250e4740/synthetic-source-bold@2x.png" alt="Standard _source vs synthetic _source" />
<em>Without LogsDB, Elasticsearch writes every log event twice: once as a raw <code>_source</code> blob on disk, once into doc values columns. LogsDB skips the blob entirely. At read time, a <code>GET &lt;index&gt;/_doc/1</code> request gathers field values from doc values and assembles the document on the fly.</em></p>
<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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt91e7d6426bc3bcf1/6a7f09fd9090b02c5584e8bf/shard-routing-bold@2x.png" alt="Shard routing: standard, routed, routed + sorted" />
<em>Data stream <code>.ds-logs-nginx-default-00001</code> with six hosts across three shards. STANDARD (hashed by <code>_id</code>): all host colors scattered randomly. ROUTED (<code>route_on_sort_fields</code>): 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.</em></p>
<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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6a8f273087631cf2/6a7f0a00ea068d7193f09d4b/recovery-source-bold@2x.png" alt="Index size written: _recovery_source eliminated" />
<em>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 <code>_recovery_source</code> blob to reconstructing documents from doc values.</em></p>
<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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0621df09262d5bf7/6a7f0a03c2cc097ec224943a/pattern-text-bold@2x.png" alt="TEXT vs PATTERN_TEXT field type" />
<em>TEXT stores each log message as a full string per document — eight copies of near-identical blobs. PATTERN_TEXT decomposes them: the shared template <code>Session opened for user {} from {} via TLS</code> is stored once with ID T0, and only the variable columns (<code>user</code>, <code>ip</code>) are stored per document — alice/10.0.1.42, bob/10.0.1.87, carol/10.0.2.11, and so on.</em></p>
<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 id="performancenotjuststorage">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>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7d5f55de5dbef47b/6a7f0a06bd2198e809757fa1/performance-over-time-bold@2x.png" alt="LogsDB throughput and storage on disk over time" />
<em>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 <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/logsdb/nightly/default/90d">elasticsearch-benchmarks.elastic.co</a>.</em></p>
<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 id="howtoenableit">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>PUT _index_template/logs-template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "index.mode": "logsdb"
    }
  }
}
</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>PUT _index_template/logs-template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "index.mode": "logsdb",
      "index.logsdb.route_on_sort_fields": 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 id="whatsnext">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>
    <link>https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-storage-evolution</link>
    <guid isPermaLink="false">elasticsearch-logsdb-storage-evolution</guid>
    <category><![CDATA[Data Management]]></category>
    <category><![CDATA[Logs Analytics]]></category>
    <dc:creator><![CDATA[Luca Wintergerst]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8dc9db2d94cde133/6a7f0a089090b01c7a84e8c5/elasticsearch-logsdb-storage-evolution.png" length="0" type="image/png"/>
    <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Continuous profiling: The key to more efficient and cost-effective applications]]></title>
    <description><![CDATA[In this post, we discuss why computational efficiency is important and how Elastic Universal Profiling enables your business to use continuous profiling in production environments to make the software that runs your business as efficient as possible.]]></description>
    <content:encoded><![CDATA[<p>Recently, Elastic Universal Profiling<sup>TM</sup> became <a href="https://www.elastic.co/blog/continuous-profiling-is-generally-available">generally available</a>. It is the part of our Observability solution that allows users to do <em>whole system, continuous profiling</em> in production environments. If you're not familiar with continuous profiling, you are probably wondering what Universal Profiling is and why you should care. That's what we will address in this post.</p>
<h2 id="efficiencyisimportantagain">Efficiency is important (again)</h2>
<p>Before we jump into continuous profiling, let's start with the "Why should I care?" question. To do that, I'd like to talk a bit about efficiency and some large-scale trends happening in our industry that are making efficiency, specifically computational efficiency, important again. I say again because in the past, when memory and storage on a computer was very limited and you had to worry about every byte of code, efficiency was an important aspect of developing software.</p>
<h3 id="theendofmooreslaw">The end of Moore’s Law</h3>
<p>First, the <a href="https://en.wikipedia.org/wiki/Moore's_law">Moore's Law</a> era is drawing to a close. This was inevitable simply due to physical limits of how small you can make a transistor and the connections between them. For a long time, software developers had the luxury of not worrying about complexity and efficiency because the next generation of hardware would mitigate any negative cost or performance impact.</p>
<p><em>If you can't rely on an endless progression of ever faster hardware, you should be interested in computational efficiency.</em></p>
<h3 id="themovetosoftwareasaservice">The move to Software-as-a-Service</h3>
<p>Another trend to consider is the shift from software vendors that sold customers software to run themselves to Software-as-a-Service businesses. A traditional software vendor didn't have to worry too much about the efficiency of their code. That issue largely fell to the customer to address; a new software version might dictate a hardware refresh to the latest and most performant. For a SaaS business, inefficient software usually degrades the customer’s experience and it certainly impacts the bottom line.</p>
<p><em>If you are a SaaS business in a competitive environment, you should be interested in computational efficiency.</em></p>
<h3 id="cloudmigration">Cloud migration</h3>
<p>Next is the ongoing <a href="https://www.elastic.co/observability/cloud-migration">cloud migration</a> to cloud computing. One of the benefits of cloud computing is the ease of scaling, both hardware and software. In the cloud, we are not constrained by the limits of our data centers or the next hardware purchase. Instead we simply spin up more cloud instances to mitigate performance problems. In addition to infrastructure scalability, microservices architectures, containerization, and the rise of Kubernetes and similar orchestration tools means that scaling services is simpler than ever. It's not uncommon to have thousands of instances of a service running in a cloud environment. This ease of scaling accounts for another trend, namely that many businesses are dealing with skyrocketing cloud computing costs.</p>
<p><em>If you are a business with ever increasing cloud costs, you should be interested in computational efficiency.</em></p>
<h3 id="ourchangingclimate">Our changing climate</h3>
<p>Lastly, if none of those reasons pique your interest, let's consider a global problem that all of us should have in mind — namely, climate change. There are many things that need to be addressed to tackle climate change, but with our dependence on software in every part of our society, computational efficiency is certainly something we should be thinking about.</p>
<p>Thomas Dullien, distinguished engineer at Elastic and one of the founders of Optymize points out that if you can save 20% on 800 servers, and assume 300W power consumption for each server, that code change is worth 160 metric tons of CO<sub>2</sub> saved per year. That may seem like a drop in the bucket but if all businesses focus more on computational efficiency, it will make an impact. Also, let's not forget the financial benefits: those 160 metric tons of CO<sub>2</sub> savings also represent a significant annual cost savings.</p>
<p><em>If you live on planet Earth, you should be interested in computational efficiency.</em></p>
<h2 id="performanceengineering">Performance engineering</h2>
<p>Who's job is it to worry about computational efficiency? Application developers usually pay at least some attention to efficiency as they develop their code. Profiling is a common approach for a developer to understand the performance of their code, and there is an entire portfolio of profiling tools available. Frequently, however, schedule pressures trump time spent on performance analysis and computational efficiency. In addition, performance problems may not become apparent until an application is running at scale in production and interacting (and competing) with everything else in that environment. Many profiling tools are not well suited to use in a production environment because they require code instrumentation and recompilation and add significant overhead.</p>
<p>When inefficient code makes it into production and begins to cause performance problems, the next line of defense is the Operations or SRE team. Their mission is to keep everything humming, and performance problems will certainly draw attention. Observability tools such as APM can shed light on these types of issues and lead the team to a specific application or service, but these tools have limits into the observability of the full system. Third-party libraries and operating system kernels functions remain hidden without a profiling solution in the production environment.</p>
<p>So, what can these teams do when there is a need to investigate a performance problem in production? That's where continuous profiling comes into the picture.</p>
<h2 id="continuousprofiling">Continuous profiling</h2>
<p>Continuous profiling is not a new idea. Google published a <a href="https://research.google/pubs/pub36575/">paper about it</a> in 2010 and began implementing continuous profiling in its environments around that time. Facebook and Netflix followed suit not long afterward.</p>
<p>Typically, continuous profiling tools have been the domain of dedicated performance engineering or operating system engineering teams, which are usually only found at extremely large scale enterprises like the ones mentioned above. The key idea is to run profiling on every server, all of the time. That way, when your observability tools point you to a specific part of an application, but you need a more detailed view into exactly where that application is consuming CPU resources, the profiling data will be there, ready to use.</p>
<p>Another benefit of continuous profiling is that it provides a view of CPU intensive software across your entire environment — whether that is a very CPU intensive function or the aggregate of a relatively small function that is run thousands of times a second in your environment.</p>
<p>While profiling tools are not new, most of them have significant gaps. Let's look at a couple of the most significant ones.</p>
<ul>
<li><strong>Limited visibility.</strong> Modern distributed applications are composed of a complex mix of building blocks, including custom software functions, third-party software libraries, networking software, operating system services, and more and more often, orchestration software such as <a href="https://kubernetes.io/">Kubernetes</a>. To fully understand what is happening in an application, you need visibility into each piece. However, even if a developer has the ability to profile their own code, everything else remains invisible. To make matters worse, most profiling tools require instrumenting the code, which adds overhead and therefore even your developers’ code is not profiled in production.</li>
<li><strong>Missing symbols in production.</strong> All of these pieces of code building blocks typically have descriptive names (some more intuitive than others) so that developers can understand and make sense of them. In a running program, these descriptive names are usually referred to as <strong>symbols</strong>. For a human being to make sense of the execution of a running application, these names are very important. Unfortunately, almost always, any software running in production has these human readable symbols stripped away for space efficiency since they are not needed by the CPU executing the software. Without all of the symbols, it makes it much more difficult to understand the full picture of what's happening in the application. To illustrate this, think of the last time you were in an SMS chat on your mobile device and you only had some of the people in the chat group in your address book while the rest simply appeared as phone numbers — this makes it very hard to tell who is saying what.</li>
</ul>
<h2 id="elasticuniversalprofilingcontinuousprofilingforall">Elastic Universal Profiling: Continuous profiling for all</h2>
<p>Our goal is to allow any business, large or small, to make computational efficiency a core consideration for all of the software that they run. Universal Profiling imposes very low overhead on your servers so it can be used in production and it provides visibility to everything running on every machine. It opens up the possibility of seeing the financial unit cost and CO<sub>2</sub> impact of every line of code running on every system in your business. How do we do that?</p>
<h3 id="wholesystemvisibilitysimple">Whole-system visibility — SIMPLE</h3>
<p>Universal Profiling is based on <a href="https://www.elastic.co/blog/ebpf-observability-security-workload-profiling">eBPF</a>, which means that it imposes very low overhead (our goal is less than 1% CPU and less than 250MB of RAM) on your servers because it doesn't require code instrumentation. That low overhead means it can be run continuously, on every server, even in production.</p>
<p>eBPF also lets us deploy a single profiler agent on a host and peek inside the operating system to see every line of code executing on the CPU. That means we have visibility into all of those application building blocks described above — the operating system itself as well as <a href="https://en.wikipedia.org/wiki/Containerization_(computing)">containerization and orchestration frameworks</a> without complex configuration.</p>
<h3 id="allthesymbols">All the symbols</h3>
<p>A key part of Universal Profiling is our hosted symbolization service. This means that symbols are not required on your servers, which not only eliminates a need for recompiling software with symbols, but it also helps to reduce overhead by allowing the Universal Profiling agent to send very sparse data back to the Elasticsearch platform where it is enriched with all of the missing symbols. Since we maintain a repository of most popular third-party software libraries and Linux operating system symbols, the Universal Profiling UI can show you all the symbols.</p>
<h3 id="yourfavoritelanguageandthensome">Your favorite language, and then some</h3>
<p>Universal Profiling is multilanguage. We support all of today’s popular programming languages, including Python, Go, Java (and any other JVM-based languages), Ruby, NodeJS, PHP, Perl, and of course, C and C++, which is critical since these languages still underly so many third-party libraries used by the other languages. In addition, we support profiling <a href="https://en.wikipedia.org/wiki/Machine_code">native code</a> a.k.a. machine language.</p>
<p>Speaking of native code, all profiling tools are tied to a specific type of CPU. Most tools today only support the Intel x86 CPU architecture. Universal Profiling supports both x86 and ARM-based processors. With the expanding use of ARM-based servers, especially in cloud environments, Universal Profiling future-proofs your continuous profiling.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5fa0007bcb27d7b6/6a83ffb863e95931e374778e/elastic-blog-1-universal-profiling.png" alt="A flamegraph showing traces across Python, Native, Kernel, and Java code" /></p>
<p>Many businesses today employ polyglot programming — that is, they use multiple languages to build an application — and Universal Profiling is the only profiler available that can build a holistic view across all of these languages. This will help you look for hotspots in the environment, leading you to "unknown unknowns" that warrant deeper performance analysis. That might be a simple interest rate calculation that should be efficient and lightweight but, surprisingly, isn't. Or perhaps it is a service that is reused much more frequently than originally expected, resulting in thousands of instances running across your environment every second, making it a prime target for efficiency improvement.</p>
<h3 id="visualizeyourimpact">Visualize your impact</h3>
<p>Elastic Universal Profiling has an intuitive UI that immediately shows you the impact of any given function, including the time it spends executing on the CPU and how much that costs both in dollars and in carbon emissions.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4073680455313cb0/6a83ffbcfc63ab419c655f39/elastic-blog-2-universal-profiling-flamegraph.png" alt="Annualized dollar cost and CO2 emissions for any function" /></p>
<p>Finally, with the level of software complexity in most production environments, there's a good chance that making a code change will have unanticipated effects across the environment. That code change may be due to a new feature being rolled out or a change to improve efficiency. In either case, a differential view, before and after the change, will help you understand the impact.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbf09ac4457d6c078/6a83ffbf4c4bfb4aeacd6900/elastic-blog-3.png" alt="Performance, CO2, and cost improvements of a more efficient hashing function" /></p>
<h2 id="letsrecap">Let's recap</h2>
<p>Computational efficiency is an important topic, both from the perspective of the ultra-competitive business climate we all work in and from living through the challenges of our planet's changing climate. Improving efficiency can be a challenging endeavor, but we can't even begin to attempt to make improvements without knowing where to focus our efforts. Elastic Universal Profiling is here to provide every business with visibility into computational efficiency.</p>
<p>How will you use Elastic Universal Profiling in your business?</p>
<ul>
<li>If you are an application developer or part of the site reliability team, Universal Profiling will provide you with unprecedented visibility into your applications that will not only help you troubleshoot performance problems in production, but also understand the impact of new features and deliver an optimal user experience.</li>
<li>If you are involved in cloud and infrastructure financial management and capacity planning, Universal Profiling will provide you with unprecedented visibility into the unit cost of every line of code that your business runs.</li>
<li>If you are involved in your business’s <a href="https://www.elastic.co/blog/sustainability-elastic-6-months-reflection">ESG</a> initiative, Universal Profiling will provide you with unprecedented visibility into your CO<sub>2</sub> emissions and open up new avenues for reducing your carbon footprint.</li>
</ul>
<p>These are just a few examples. For more ideas, read how <a href="https://www.elastic.co/customers/appomni">AppOmni benefits from Elastic Universal Profiling</a>.</p>
<p>You can <a href="https://www.elastic.co/guide/en/observability/current/profiling-get-started.html">get started</a> with Elastic Universal Profiling right now!</p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/continuous-profiling-efficient-cost-effective-applications</link>
    <guid isPermaLink="false">continuous-profiling-efficient-cost-effective-applications</guid>
    <category><![CDATA[APM]]></category>
    <category><![CDATA[Infrastructure Monitoring]]></category>
    <category><![CDATA[Data Management]]></category>
    <dc:creator><![CDATA[John Knoepfle]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8efe6e25a7389fad/6a83ffc2227b1ce0985a173b/the-end-of-databases-A_(1).jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 27 Oct 2023 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Improving the Elastic APM UI performance with continuous rollups and service metrics]]></title>
    <description><![CDATA[We made significant improvements to the UI performance in Elastic APM to make it scale with even the most demanding workloads, by pre-aggregating metrics at the service level, and storing the metrics at different levels of granularity.]]></description>
    <content:encoded><![CDATA[<p>In today's fast-paced digital landscape, the ability to monitor and optimize application performance is crucial for organizations striving to deliver exceptional user experiences. At Elastic, we recognize the significance of providing our user base with a reliable <a href="https://www.elastic.co/observability">observability platform</a> that scales with you as you’re onboarding thousands of services that produce terabytes of data each day. We have been diligently working behind the scenes to enhance our solution to meet the demands of even the largest deployments.</p>
<p>In this blog post, we are excited to share the significant strides we have made in improving the UI performance of Elastic APM. Maintaining a snappy user interface can be a challenge when interactively summarizing the massive amounts of data needed to provide an overview of the performance for an entire enterprise-scale service inventory. We want to assure our customers that we have listened, taken action, and made notable architectural changes to elevate the scalability and maturity of our solution.</p>
<h2 id="architecturalenhancements">Architectural enhancements</h2>
<p>Our journey began back in the 7.x series where we noticed that doing ad-hoc aggregations on raw <a href="https://www.elastic.co/guide/en/apm/guide/current/data-model-transactions.html">transaction</a> data put Elasticsearch<sup>®</sup> under a lot of pressure in large-scale environments. Since then, we’ve begun to pre-aggregate the transactions into transaction metrics during ingestion. This has helped to keep the performance of the UI relatively stable. Regardless of how busy the monitored application is and how many transaction events it is creating, we’re just querying pre-aggregated metrics that are stored at a constant rate. We’ve enabled the metrics-powered UI by default in <a href="https://github.com/elastic/kibana/issues/92024">7.15</a>.</p>
<p>However, when showing an inventory of a large number of services over large time ranges, the number of metric data points that need to be aggregated can still be large enough to cause performance issues. We also create a time series for each distinct set of dimensions. The dimensions include metadata, such as the transaction name and the host name. Our <a href="https://www.elastic.co/guide/en/apm/guide/current/data-model-metrics.html#_transaction_metrics">documentation</a> includes a full list of all available dimensions. If there’s a very high number of unique transaction names, which could be a result of improper instrumentation (see <a href="https://www.elastic.co/guide/en/kibana/current/troubleshooting.html#troubleshooting-too-many-transactions">docs</a> for more details), this will create a lot of individual time series that will need to be aggregated when requesting a summary of the service’s overall performance. Global labels that are added to the APM Agent configuration are also added as dimensions to these metrics, and therefore they can also impact the number of time series. Refer to the FAQs section below for more details.</p>
<p>Within the 8.7 and 8.8 releases, we’ve addressed these challenges with the following architectural enhancements that aim to reduce the number of documents Elasticsearch needs to search and aggregate on-the-fly, resulting in faster response times:</p>
<ul>
<li><strong>Pre-aggregation of transaction metrics into service metrics.</strong> Instead of aggregating all distinct time series that are created for each individual transaction name on-the-fly for every user request, we’re already pre-aggregating a summary time series for each service during data ingestion. Depending on how many unique transaction names the services have, this reduces the number of documents Elasticsearch needs to look up and aggregate by a factor of typically 10–100. This is particularly useful for the <a href="https://www.elastic.co/guide/en/kibana/master/services.html">service inventory</a> and the <a href="https://www.elastic.co/guide/en/kibana/master/service-overview.html">service overview</a> pages.</li>
<li><strong>Pre-aggregation of all metrics into different levels of granularity.</strong> The APM UI chooses the most appropriate level of granularity, depending on the selected time range. In addition to the metrics that are stored at a 1-minute granularity, we’re also summarizing and storing metrics at a 10-minute and 60-minute granularity level. For example, when looking at a 7-day period, the 60-minute data stream is queried instead of the 1-minute one, resulting in 60x fewer documents for Elasticsearch to examine. This makes sure that all graphs are rendered quickly, even when looking at larger time ranges.</li>
<li><strong>Safeguards on the number of unique transactions per service for which we are aggregating metrics.</strong> Our agents are designed to keep the cardinality of the transaction name low. But in the wild, we’ve seen some services that have a huge amount of unique transaction names. This used to cause performance problems in the UI because APM Server would create many time series that the UI needed to aggregate at query time. In order to protect APM Server from running out of memory when aggregating a large number of time series for each unique transaction name, metrics were published without aggregating when limits for the number of time series were reached. This resulted in a lot of individual metric documents that needed to be aggregated at query time. To address the problem, we've introduced a system where we aggregate metrics in a dedicated overflow bucket for each service when limits are reached. Refer to our <a href="https://www.elastic.co/guide/en/kibana/8.8/troubleshooting.html#troubleshooting-too-many-transactions">documentation</a> for more details.</li>
</ul>
<p>The exact factor of the document count reduction depends on various conditions. But to get a feeling for a typical scenario, if your services, on average, have 10 instances, no instance-specific global labels, 100 unique transaction names each, and you’re looking at time ranges that can leverage the 60m granularity, you’d see a reduction of documents that Elasticsearch needs to aggregate by a factor of 180,000 (10 instances x 100 transaction names x 60m x 3 because we’re also collapsing the event.outcome dimension). While the response times of Elasticsearch aggregations isn’t exactly scaling linearly with the number of documents, there is a strong correlation.</p>
<h2 id="faqs">FAQs</h2>
<h3 id="whenupgradingtothelatestversionwillmyolddataalsoloadfaster">When upgrading to the latest version, will my old data also load faster?</h3>
<p>Updating to 8.8 doesn’t immediately make the UI faster. Because the improvements are powered by pre-aggregations that APM Server is doing during ingestion, only new data will benefit from it. For that reason, you should also make sure to update APM Server as well. The UI can still display data that was ingested using an older version of the stack.</p>
<h3 id="iftheuiisbasedonmetricscanistillsliceanddiceusingcustomlabels">If the UI is based on metrics, can I still slice and dice using custom labels?</h3>
<p>High cardinality analysis is a big strength of Elastic Observability, and this focus on pre-aggregated metrics does not compromise that in any way.</p>
<p>The UI implements a sophisticated fallback mechanism that uses service metrics, transaction metrics, or raw transaction events, depending on which filters are applied. We’re not creating metrics for each user.id, for example. But you can still filter the data by user.id and the UI will then use raw transaction events. Chances are that you’re looking at a narrow slice of data when filtering by a dimension that is not available on the pre-aggregated metrics, therefore aggregations on the raw data are typically very fast.</p>
<p>Note that all global labels that are added to the APM agent configuration are part of the dimension of the pre-aggregated metrics, with the exception of RUM (see more details in <a href="https://github.com/elastic/apm-server/issues/11037">this issue</a>).</p>
<h3 id="caniusethepreaggregatedmetricsincustomdashboards">Can I use the pre-aggregated metrics in custom dashboards?</h3>
<p>Yes! If you use <a href="https://www.elastic.co/guide/en/kibana/current/lens.html">Lens</a> and select the "APM" data view, you can filter on either metricset.name:service_transaction or metricset.name:transaction, depending on the level of detail you need. Transaction latency is captured in transaction.duration.histogram, and successful outcomes and failed outcomes are stored in event.success_count. If you don't need a distribution of values, you can also select the transaction.duration.summary field for your metric aggregations, which should be faster. If you want to calculate the failure rate, here's a <a href="https://www.elastic.co/guide/en/kibana/current/lens.html#lens-formulas">Lens formula</a>: 1 - (sum(event.success_count) / count(event.success_count)). Note that the only granularity supported here is 1m.</p>
<h3 id="dotheadditionalmetricshaveanimpactonthestorage">Do the additional metrics have an impact on the storage?</h3>
<p>While we’re storing more metrics than before, and we’re storing all metrics in different levels of granularity, we were able to offset that by enabling <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html#synthetic-source">synthetic source</a> for all metric data streams. We’ve even increased the default retention for the metrics in the coarse-grained granularity levels, so that the 60m rollup data streams are now stored for 390 days. Please consult our <a href="https://www.elastic.co/guide/en/apm/guide/current/apm-data-streams.html">documentation</a> for more information about the different metric data streams.</p>
<h3 id="aretherelimitsontheamountoftimeseriesthatapmservercanaggregate">Are there limits on the amount of time series that APM Server can aggregate?</h3>
<p>APM Server performs pre-aggregations in memory, which is fast, but consumes a considerable amount of memory. There are limits in place to protect APM Server from running out of memory, and from 8.7, most of them scale with available memory by default, meaning that allocating more memory to APM Server will allow it to handle more unique pre-aggregation groups like services and transactions. These limits are described in <a href="https://www.elastic.co/guide/en/apm/guide/current/data-model-metrics.html#_aggregated_metrics_limits_and_overflows">APM Server Data Model docs</a>.</p>
<p>On the APM Server roadmap, we have plans to move to a LSM-based approach where pre-aggregations are performed with the help of disks in order to reduce memory usage. This will enable APM Server to scale better with the input size and cardinality.</p>
<p>A common pitfall when working with pre-aggregations is to add instance-specific global labels to APM agents. This may exhaust the aggregation limits and cause metrics to be aggregated under the overflow bucket instead of the corresponding service. Therefore, make sure to follow the best practice of only adding a limited set of global labels to a particular service.</p>
<h2 id="validation">Validation</h2>
<p>To validate the effectiveness of the new architecture, and to ensure that the accuracy of the data is not negatively affected, we prepared a test environment where we generated 35K+ transactions per minute in a timespan of 14 days resulting in approximately 850 million documents.</p>
<p>We’ve tested the queries that power our service inventory, the service overview, and the transaction details using different time ranges (1d, 7d, 14d). Across the board, we’ve seen orders of magnitude improvements. Particularly, queries across larger time ranges that benefit from using the coarse-grained metrics in addition to the pre-aggregated service metrics saw incredible reductions of the response time.</p>
<p>We’ve also validated that there’s no loss in accuracy when using the more coarse-grained metrics for larger time ranges.</p>
<p>Every environment will behave a bit differently, but we’re confident that the impressive improvements in response time will translate well to setups of even bigger scale.</p>
<h2 id="plannedimprovements">Planned improvements</h2>
<p>As mentioned in the FAQs section, the number of time series for transaction metrics can grow quickly, as it is the product of multiple dimensions. For example, given a service that runs on 100 hosts and has 100 transaction names that each have 4 transaction results, APM Server needs to track 40,000 (100 x 100 x 4) different time series for that service. This would even exceed the maximum per-service limit of 32,000 for APM Servers with 64GB of main memory.</p>
<p>As a result, the UI will show an entry for “Remaining Transactions” in the Service overview page. This tracks the transaction metrics for a service once it hits the limit. As a result, you may not see all transaction names of your service. It may also be that all distinct transaction names are listed, but that the transaction metrics for some of the instances of that service are combined in the “Remaining Transactions” category.</p>
<p>We’re currently considering restructuring the dimensions for the metrics to avoid that the combination of the dimensions for transaction name and service instance-specific dimensions (such as the host name) lead to an explosion of time series. Stay tuned for more details.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The architectural improvements we’ve delivered in the past releases provide a step-function in terms of the scalability and responsiveness of our UI. Instead of having to aggregate massive amounts of data on-the-fly as users are navigating through the user interface, we pre-aggregate the results for the most common queries as data is coming in. This ensures we have the answers ready before users have even asked their most frequently asked questions, while still being able to answer ad-hoc questions.</p>
<p>We are excited to continue supporting our community members as they push boundaries on their growth journey, providing them with a powerful and mature platform that can effortlessly handle the demands of the largest workloads. Elastic is committed to its mission to enable everyone to find the answers that matter. From all data. In real time. At scale.</p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/apm-ui-performance-continuous-rollups-service-metrics</link>
    <guid isPermaLink="false">apm-ui-performance-continuous-rollups-service-metrics</guid>
    <category><![CDATA[APM]]></category>
    <category><![CDATA[Metrics]]></category>
    <category><![CDATA[Data Management]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner,Yngrid Coello,Dario Gieselaar,Carson Ip]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt569decf8bd6851af/6a85cbf2342d6985a621b0fd/elastic-blog-header-ui.png" length="0" type="image/png"/>
    <pubDate>Thu, 29 Jun 2023 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Simplifying log data management: Harness the power of flexible routing with Elastic]]></title>
    <description><![CDATA[The reroute processor, available as of Elasticsearch 8.8, allows customizable rules for routing documents, such as logs, into data streams for better control of processing, retention, and permissions with examples that you can try on your own.]]></description>
    <content:encoded><![CDATA[<p>In Elasticsearch 8.8, we’re introducing the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/reroute-processor.html">reroute processor</a> in technical preview that makes it possible to send documents, such as logs, to different <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/data-streams.html">data streams</a>, according to flexible routing rules. When using Elastic Observability, this gives you more granular control over your data with regard to retention, permissions, and processing with all the potential benefits of the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a>. While optimized for data streams, the reroute processor also works with classic indices. This blog post contains examples on how to use the reroute processor that you can try on your own by executing the snippets in the <a href="https://www.elastic.co/guide/en/kibana/current/console-kibana.html">Kibana dev tools</a>.</p>
<p>Elastic Observability offers a wide range of <a href="https://www.elastic.co/integrations/data-integrations?solution=observability">integrations</a> that help you to monitor your applications and infrastructure. These integrations are added as policies to <a href="https://www.elastic.co/guide/en/fleet/current/elastic-agent-installation.html">Elastic agents</a>, which help ingest telemetry into Elastic Observability. Several examples of these integrations include the ability to ingest logs from systems that send a stream of logs from different applications, such as <a href="https://www.elastic.co/guide/en/kinesis/current/aws-firehose-setup-guide.html">Amazon Kinesis Data Firehose</a>, <a href="https://docs.elastic.co/en/integrations/kubernetes">Kubernetes container logs</a>, and <a href="https://docs.elastic.co/integrations/tcp">syslog</a>. One challenge is that these multiplexed log streams are sending data to the same Elasticsearch data stream, such as logs-syslog-default. This makes it difficult to create parsing rules in ingest pipelines and dashboards for specific technologies, such as the ones from the <a href="https://docs.elastic.co/en/integrations/nginx">Nginx</a> and <a href="https://docs.elastic.co/en/integrations/apache">Apache</a> integrations. That’s because in Elasticsearch, in combination with the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a>, the processing and the schema are both encapsulated in a data stream.</p>
<p>The reroute processor helps you tease apart data from a generic data stream and send it to a more specific one. You may use that mechanism to send logs to a data stream that is set up by the Nginx integration, for example, so that the logs are parsed with that integration and you can use the integration’s prebuilt dashboards or create custom ones with the fields, such as the url, the status code, and the response time that the Nginx pipeline has parsed out of the Nginx log message. You can also split out/separate regular Nginx logs and errors with the reroute processor, providing further separation ability and categorization of logs.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt512e74af45a03dab/6a85cdff99083f664140fa2d/blog-elastic-routing-pipeline.png" alt="routing pipeline" /></p>
<h2 id="exampleusecase">Example use case</h2>
<p>To use the reroute processor, first:</p>
<ol>
<li><p>Ensure you are on Elasticsearch 8.8</p></li>
<li><p>Ensure you have permissions to manage indices and data streams</p></li>
<li><p>If you don’t already have an account on <a href="https://cloud.elastic.co/registration?fromURI=/home">Elastic Cloud</a>, sign up for one</p></li>
</ol>
<p>Next, you’ll need to <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/set-up-a-data-stream.html">set up a data stream</a> and create a custom Elasticsearch <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest.html">ingest pipeline</a> that is called as the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html#set-default-pipeline">default pipeline</a>. Below we go through this step by step for the “mydata” data set that we’ll simulate ingesting container logs into. We start with a basic example and extend it from there.</p>
<p>The following steps should be utilized in the Elastic console, which is found at <strong>Management -&gt; Dev tools -&gt; Console</strong>. First, we need an an ingest pipeline and a template for the data stream:</p>
<pre><code>PUT _ingest/pipeline/logs-mydata
{
  "description": "Routing for mydata",
  "processors": [
    {
      "reroute": {
      }
    }
  ]
}
</code></pre>
<p>This creates an ingest pipeline with an empty reroute processor. To make use of it, we need an index template:</p>
<pre><code>PUT _index_template/logs-mydata
{
  "index_patterns": [
    "logs-mydata-*"
  ],
  "data_stream": {},
  "priority": 200,
  "template": {
    "settings": {
      "index.default_pipeline": "logs-mydata"
    },
    "mappings": {
      "properties": {
        "container.name": {
          "type": "keyword"
        }
      }
    }
  }
}
</code></pre>
<p>The above template is applied to all data that is shipped to logs-mydata-*. We have mapped container.name as a keyword, as this is the field we will be using for routing later on. Now, we send a document to the data stream and it will be ingested into logs-mydata-default:</p>
<pre><code>POST logs-mydata-default/_doc
{
  "@timestamp": "2023-05-25T12:26:23+00:00",
  "container": {
    "name": "foo"
  }
}
</code></pre>
<p>We can check that it was ingested with the command below, which will show 1 result.</p>
<pre><code>GET logs-mydata-default/_search
</code></pre>
<p>Without modifying the routing processor, this already allows us to route documents. As soon as the reroute processor is specified, it will look for data_stream.dataset and data_stream.namespace fields by default and will send documents to the corresponding data stream, according to the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a> logs-\&lt;dataset&gt;-\&lt;namespace&gt;. Let’s try this out:</p>
<pre><code>POST logs-mydata-default/_doc
{
  "@timestamp": "2023-03-30T12:27:23+00:00",
  "container": {
"name": "foo"
  },
  "data_stream": {
    "dataset": "myotherdata"
  }
}
</code></pre>
<p>As can be seen with the GET logs-mydata-default/_search command, this document ended up in the logs-myotherdata-default data stream. But instead of using default rules, we want to create our own rules for the field container.name. If the field is container.name = foo, we want to send it to logs-foo-default. For this we modify our routing pipeline:</p>
<pre><code>PUT _ingest/pipeline/logs-mydata
{
  "description": "Routing for mydata",
  "processors": [
    {
      "reroute": {
        "tag": "foo",
        "if" : "ctx.container?.name == 'foo'",
        "dataset": "foo"
      }
    }
  ]
}
</code></pre>
<p>Let's test this with a document:</p>
<pre><code>POST logs-mydata-default/_doc
{
  "@timestamp": "2023-05-25T12:26:23+00:00",
  "container": {
    "name": "foo"
  }
}
</code></pre>
<p>While it would be possible to specify a routing rule for each container name, you can also route by the value of a field in the document:</p>
<pre><code>PUT _ingest/pipeline/logs-mydata
{
  "description": "Routing for mydata",
  "processors": [
    {
      "reroute": {
        "tag": "mydata",
        "dataset": [
          "{{container.name}}",
          "mydata"
        ]
      }
    }
  ]
}
</code></pre>
<p>In this example, we are using a field reference as a routing rule. If the container.name field exists in the document, it will be routed — otherwise it falls back to mydata. This can be tested with:</p>
<pre><code>POST logs-mydata-default/_doc
{
  "@timestamp": "2023-05-25T12:26:23+00:00",
  "container": {
    "name": "foo1"
  }
}

POST logs-mydata-default/_doc
{
  "@timestamp": "2023-05-25T12:26:23+00:00",
  "container": {
    "name": "foo2"
  }
}
</code></pre>
<p>This creates the data streams logs-foo1-default and logs-foo2-default.</p>
<p><em>NOTE: There is currently a limitation in the processor that requires the fields specified in a <code>{{field.reference}}</code> to be in a nested object notation. A dotted field name does not currently work. Also, you’ll get errors when the document contains dotted field names for any</em> <em>data_stream.*</em> <em>field. This limitation will be</em> <a href="https://github.com/elastic/elasticsearch/pull/96243"><em>fixed</em></a> <em>in 8.8.2 and 8.9.0.</em></p>
<h2 id="apikeys">API keys</h2>
<p>When using the reroute processor, it is important that the API keys specified have permissions for the source and target indices. For example, if a pattern is used for routing from logs-mydata-default, the API key must have write permissions for <code>logs-*-*</code> as data could end up in any of these indices (see example further down).</p>
<p>We’re currently <a href="https://github.com/elastic/integrations/issues/5989">working</a> <a href="https://github.com/elastic/integrations/issues/6255">on</a> extending the API key permissions for our <a href="https://www.elastic.co/integrations/data-integrations">integrations</a> so that they allow for routing by default if you’re running a Fleet-managed Elastic Agent.</p>
<p>If you’re using a standalone Elastic Agent, or any other shipper, you can use this as a template to create your API key:</p>
<pre><code>POST /_security/api_key
{
  "name": "ingest_logs",
  "role_descriptors": {
    "ingest_logs": {
      "cluster": [
        "monitor"
      ],
      "indices": [
        {
          "names": [
            "logs-*-*"
          ],
          "privileges": [
            "auto_configure",
            "create_doc"
          ]
        }
      ]
    }
  }
}
</code></pre>
<h2 id="futureplans">Future plans</h2>
<p>In Elasticsearch 8.8, the reroute processor was released in technical preview. The plan is to adopt this in our data sink integrations like syslog, k8s, and others. Elastic will provide default routing rules that just work out of the box, but it will also be possible for users to add their own rules. If you are using our integrations, follow <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html#pipelines-for-fleet-elastic-agent">this guide</a> on how to add a custom ingest pipeline.</p>
<h2 id="tryitout">Try it out!</h2>
<p>This blog post has shown some sample use cases for document based routing. Try it out on your data by adjusting the commands for index templates and ingest pipelines to your own data, and get started with <a href="https://cloud.elastic.co/registration?fromURI=/home">Elastic Cloud</a> through a 7-day free trial. Let us know via <a href="https://ela.st/reroute-feedback">this feedback form</a> how you’re planning to use the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/reroute-processor.html">reroute processor</a> and whether you have suggestions for improvement.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/simplifying-log-data-management-flexible-routing</link>
    <guid isPermaLink="false">simplifying-log-data-management-flexible-routing</guid>
    <category><![CDATA[Data Management]]></category>
    <category><![CDATA[Logs Analytics]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner,Nicolas Ruflin]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc40c1bcfa3417a50/6a85ce0293ffb91c1ab91481/observability-digital-transformation-1.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 13 Jun 2023 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to use Elasticsearch and Time Series Data Streams for observability metrics]]></title>
    <description><![CDATA[With Time Series Data Streams (TSDS), Elasticsearch introduces optimized storage for metrics time series. Check out how we use it for Elastic Observability.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch is used for a wide variety of data types — one of these is metrics. With the introduction of Metricbeat many years ago and later our APM Agents, the metric use case has become more popular. Over the years, Elasticsearch has made many improvements on how to handle things like metrics aggregations and sparse documents. At the same time, <a href="https://www.elastic.co/guide/en/kibana/current/tsvb.html">TSVB visualizations</a> were introduced to make visualizing metrics easier. One concept that was missing that exists for most other metric solutions is the concept of time series with dimensions.</p>
<p>Mid 2021, the Elasticsearch team <a href="https://github.com/elastic/elasticsearch/issues/74660">embarked</a> on making Elasticsearch a much better fit for metrics. The team created <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/tsds.html">Time Series Data Streams (TSDS)</a>, which were released in 8.7 as generally available (GA).</p>
<p>This blog post dives into how TSDS works and how we use it in Elastic Observability, as well as how you can use it for your own metrics.</p>
<h2 id="aquickintroductiontotsds">A quick introduction to TSDS</h2>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/tsds.html">Time Series Data Streams (TSDS)</a> are built on top of data streams in Elasticsearch that are optimized for time series. To create a data stream for metrics, an additional setting on the data stream is needed. As we are using data streams, first an Index Template has to be created:</p>
<pre><code>PUT _index_template/metrics-laptop
{
  "index_patterns": [
    "metrics-laptop-*"
  ],
  "data_stream": {},
  "priority": 200,
  "template": {
    "settings": {
      "index.mode": "time_series"
    },
    "mappings": {
      "properties": {
        "host.name": {
          "type": "keyword",
          "time_series_dimension": true
        },
        "packages.sent": {
          "type": "integer",
          "time_series_metric": "counter"
        },
        "memory.usage": {
          "type": "double",
          "time_series_metric": "gauge"
        }
      }
    }
  }
}
</code></pre>
<p>Let's have a closer look at this template. On the top part, we mark the index pattern with metrics-laptop-*. Any pattern can be selected, but it is recommended to use the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a> for all your metrics. The next section sets the "index.mode": "time_series" in combination with making sure it is a data_stream: "data_stream": {}.</p>
<h3 id="dimensions">Dimensions</h3>
<p>Each time series data stream needs at least one dimension. In the example above, host.name is set as a dimension field with "time_series_dimension": true. You can have up to 16 dimensions by default. Not every dimension must show up in each document. The dimensions define the time series. The general rule is to pick fields as dimensions that uniquely identify your time series. Often this is a unique description of the host/container, but for some metrics like disk metrics, the disk id is needed in addition. If you are curious about default recommended dimensions, have a look at this <a href="https://github.com/elastic/ecs/pull/2172">ECS contribution</a> with dimension properties.</p>
<h2 id="reducedstorageandincreasedqueryspeed">Reduced storage and increased query speed</h2>
<p>At this point, you already have a functioning time series data stream. Setting the index mode to time series automatically turns on <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html#synthetic-source">synthetic source</a>. By default, Elasticsearch typically duplicates data three times:</p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Column-oriented_DBMS#Row-oriented_systems">row-oriented storage</a> (_source field)</li>
<li><a href="https://en.wikipedia.org/wiki/Column-oriented_DBMS#Column-oriented_systems">column-oriented storage</a> (doc_values: true for aggregations)</li>
<li>indices (index: true for filtering and search)</li>
</ul>
<p>With synthetic source, the _source field is not persisted; instead, it is reconstructed from the doc values. Especially in the metrics use case, there are little benefits to keeping the source.</p>
<p>Not storing it means a significant reduction in storage. Time series data streams sort the data based on the dimensions and the time stamp. This means data that is usually queried together is stored together, which speeds up query times. It also means that the data points for a single time series are stored alongside each other on disk. This enables further compression of the data as the rate at which a counter increases is often relatively constant.</p>
<h2 id="metrictypes">Metric types</h2>
<p>But to benefit from all the advantages of TSDS, the field properties of the metrics fields must be extended with the <code>time_series_metric: {type}</code>. Several <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/tsds.html#time-series-metric">types are supported</a> — as an example, gauge and counter were used above. Giving Elasticsearch knowledge about the metric type allows Elasticsearch to offer more optimized queries for the different types and reduce storage usage further.</p>
<p>When you create your own templates for data streams under the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a>, it is important that you set "priority": 200 or higher, as otherwise the built-in default template will apply.</p>
<h2 id="ingestadocument">Ingest a document</h2>
<p>Ingesting a document into a TSDS isn't in any way different from ingesting documents into Elasticsearch. You can use the following commands in Dev Tools to add a document, and then search for it and also check out the mappings. Note: You have to adjust the @timestamp field to be close to your current date and time.</p>
<pre><code># Add a document with `host.name` as the dimension
POST metrics-laptop-default/_doc
{
  # This timestamp neesd to be adjusted to be current
  "@timestamp": "2023-03-30T12:26:23+00:00",
  "host.name": "ruflin.com",
  "packages.sent": 1000,
  "memory.usage": 0.8
}

# Search for the added doc, _source will show up but is reconstructed
GET metrics-laptop-default/_search

# Check out the mappings
GET metrics-laptop-default
</code></pre>
<p>If you do search, it still shows _source but this is reconstructed from the doc values. The additional field added above is @timestamp. This is important as it is a required field for any data stream.</p>
<h2 id="whyisthisallimportantforobservability">Why is this all important for Observability?</h2>
<p>One of the advantages of the Elastic Observability solution is that in a single storage engine, all signals are brought together in a single place. Users can query logs, metrics, and traces together without having to jump from one system to another. Because of this, having a great storage and query engine not only for logs but also metrics is key for us.</p>
<h2 id="usageoftsdsinintegrations">Usage of TSDS in integrations</h2>
<p>With <a href="https://www.elastic.co/integrations/data-integrations">integrations</a>, we give our users an out of the box experience to integrate with their infrastructure and services. If you are using our integrations, eventually you will automatically get all the benefits of TSDS for your metrics assuming you are on version 8.7 or newer.</p>
<p>Currently we are working through the list of our integration packages, add the dimensions, metric type fields and then turn on TSDS for the metrics data streams. What this means is as soon as the package has all properties enabled, the only thing you have to do is upgrade the integration and everything else will happen automatically in the background.</p>
<p>To visualize your time series in Kibana, use <a href="https://www.elastic.co/guide/en/kibana/current/lens.html">Lens</a>, which has native support built in for TSDS.</p>
<h2 id="learnmore">Learn more</h2>
<p>If you switch over to TSDS, you will automatically benefit from all the future improvements Elasticsearch is making for metrics time series, be it more efficient storage, query performance, or new aggregation capabilities. If you want to learn more about how TSDS works under the hood and all available config options, check out the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/tsds.html">TSDS documentation</a>. What Elasticsearch supports in 8.7 is only the first iteration of the metrics time series in Elasticsearch.</p>
<p><a href="https://www.elastic.co/blog/whats-new-elasticsearch-8-7-0">TSDS can be used since 8.7</a> and will be in more and more of our integrations automatically when integrations are upgraded. All you will notice is lower storage usage and faster queries. Enjoy!</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/time-series-data-streams-observability-metrics</link>
    <guid isPermaLink="false">time-series-data-streams-observability-metrics</guid>
    <category><![CDATA[Data Management]]></category>
    <category><![CDATA[Metrics]]></category>
    <dc:creator><![CDATA[Nicolas Ruflin]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf2ff8f2a6b4ee6f6/6a85cef7331d7aa71cc3184f/ebpf-monitoring.jpeg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 04 May 2023 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>