<?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[Aleksandar Panov - 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[Aleksandar Panov - Elastic Observability Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad972c1c27dbefc6/6a88d9782904ea5e8511d473/observability-labs-thumbnail.png</url>
      <link>https://www.elastic.co/observability-labs/author/aleksandar-panov</link>
    </image>
    <link>https://www.elastic.co/observability-labs/author/aleksandar-panov</link>
    <atom:link href="https://www.elastic.co/observability-labs/rss/author/aleksandar-panov.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Sun, 27 Sep 2026 00:36:21 GMT</lastBuildDate>
  <item>
    <title><![CDATA[One OTLP endpoint, three teams, zero routing rules: Elasticsearch Streams AI Partitioning]]></title>
    <description><![CDATA[Stop writing log routing rules upfront. See how Streams AI Partitioning reads your data, proposes child streams, and lets you set per-team retention in minutes.]]></description>
    <content:encoded><![CDATA[<p>Ship logs from three teams into one Elastic OTLP endpoint, and <a href="https://www.elastic.co/docs/solutions/observability/streams/management/partitioning">Streams AI Partitioning</a> routes them into per-team child streams, with no routing rules written upfront. In this post, you generate 115 multi-team log records, let the AI analyze what arrived and propose partitions, refine the suggestions in plain English, then set retention independently per team: 90 days for payments, 30 for checkout, 7 for notifications. The entire workflow runs inside Elastic Observability without touching index templates or ILM policies.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc0aa396438b856c5/6a85ce06342d693b6421b14f/image1.png" alt="logs.otel parent stream partitioned into per-team child streams" /></p>
<h2 id="whymultiteamlogroutingneedsstructure">Why multi-team log routing needs structure</h2>
<p>Multi-team Elasticsearch deployments typically converge on a single shared index, which works until teams need different retention periods, sharding settings or processor pipelines.</p>
<p>Before <a href="https://www.elastic.co/docs/solutions/observability/streams/streams">Streams</a>, you had to set up your ingestion scripts to send data to different indices, or data streams, or use the <a href="https://www.elastic.co/docs/reference/enrich-processor/reroute-processor">reroute</a> processor to define the data destination based on some field name.</p>
<p>With AI Partitioning, you let the data arrive first. Then the AI analyzes what showed up, reviews the suggestions it proposes, refines them as needed, and applies them. The result is a set of <a href="https://www.elastic.co/docs/solutions/observability/streams/wired-streams">wired child streams</a> that inherit retention, processors, and schema from the parent, while still allowing you to override any of those per child.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7f1ab44aa0327f17/6a85ce0999083f683140fa31/image2.png" alt="Before and after diagram: single retention for all teams becomes per-team retention with AI Partitioning" /></p>
<h2 id="whatyouneedbeforeusingstreamsaipartitioning">What you need before using Streams AI Partitioning</h2>
<p>Before running the example, three things need to be in place:</p>
<ol>
<li><strong>Wired Streams enabled.</strong> On Elastic Cloud Serverless and Elastic Cloud Hosted 9.4+, wired streams are on by default. If you upgraded from an earlier version, open the Streams app and confirm the toggle is on under Settings.</li>
<li><strong>The Elastic Managed LLM connector.</strong> Go to <strong>Stack Management &gt; Connectors &gt; Create connector &gt; <a href="https://www.elastic.co/docs/reference/kibana/connectors-kibana/elastic-managed-llm">Elastic Managed LLM</a></strong>. Keep in mind the following considerations:</li>
<li>This connector ships preconfigured and does not require an external account or API key.</li>
<li>Any <a href="https://www.elastic.co/docs/reference/kibana/connectors-kibana/gen-ai-connectors">generative AI connector</a> works with the feature.</li>
<li>Note that Elastic Managed LLMs <a href="https://www.elastic.co/pricing/serverless-search">incur a cost per million tokens</a> for input and output.</li>
<li>The account you use needs the <code>manage_inference</code> cluster privilege (the built-in <code>inference_admin</code> role grants it).</li>
<li><strong>The Managed OTLP endpoint URL and an API key.</strong> Open <strong>Cloud Console &gt; Manage &gt; Application endpoints &gt; Ingest</strong>. Copy the <a href="https://www.elastic.co/docs/reference/opentelemetry/motlp">Managed OTLP endpoint URL</a> and generate an API key from that same panel.</li>
</ol>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4fa44d988e9a0819/6a85ce0b342d690b0521b153/image3.png" alt="Managed OTLP endpoint URL and Component ID in Elastic Cloud Console" /></p>
<p>Once these are ready, open <strong>Observability &gt; Streams</strong> and confirm a <code>logs.otel</code> wired stream is listed. That stream is the parent we will partition.</p>
<p><em>If <strong>Streams</strong> does not appear in the sidebar, your Kibana space may be using a solution view other than Observability. You can change it in Stack Management &gt; <a href="https://www.elastic.co/docs/deploy-manage/manage-spaces">Spaces</a> &gt; edit your space &gt; set Solution view to <strong>Observability</strong>.</em></p>
<h2 id="generatingmultiteamlogdata">Generating multi-team log data</h2>
<p>Our example uses three apps produced by teams from the same fictional company:</p>
<ul>
<li><code>payments-api</code>: structured JSON with <code>transaction_id</code> and <code>amount_cents</code>. Sensitive data, long retention needs.</li>
<li><code>checkout-web</code>: JSON with <code>cart_id</code> and <code>customer_id</code>. Mostly INFO and ERROR.</li>
<li><code>notifications-worker</code>: less structured, with <code>recipient</code> and <code>channel</code>. High volume.</li>
</ul>
<p>We use a Python script with the <a href="https://opentelemetry.io/docs/languages/python/">OpenTelemetry Python SDK</a> to emit logs for all three teams over OTLP directly to the <a href="https://www.elastic.co/observability-labs/blog/elastic-managed-otlp-endpoint-for-opentelemetry">Managed OTLP endpoint</a>. The full code, including setup and execution, is in the <a href="https://github.com/Delacrobix/Taming-the-Log-Chaos-with-Streams-AI-Partitioning/blob/main/notebook.ipynb">companion notebook</a>.</p>
<p>Each team is defined with a service name, a set of message templates, and a function that generates team-specific attributes:</p>
<pre><code>TEAMS = {
    "payments": {
        "service": "payments-api",
        "messages": [
            ("INFO", "charge captured tx={tx} amount_cents={amt}"),
            ("ERROR", "charge declined tx={tx} reason=insufficient_funds"),
            ("INFO", "refund issued tx={tx} amount_cents={amt}"),
        ],
        "extra": lambda: {
            "transaction_id": f"tx_{random.randint(10000, 99999)}",
            "amount_cents": random.randint(100, 50000),
        },
    },
    "checkout": {
        "service": "checkout-web",
        "messages": [
            ("INFO", "cart updated cart={cart} customer={cust}"),
            ("INFO", "checkout started cart={cart} customer={cust}"),
            ("ERROR", "checkout failed cart={cart} stage=address_validation"),
        ],
        "extra": lambda: {
            "cart_id": f"c_{random.randint(1000, 9999)}",
            "customer_id": f"u_{random.randint(100, 999)}",
        },
    },
    "notifications": {
        "service": "notifications-worker",
        "messages": [
            ("INFO", "email queued recipient={rcp} channel=email"),
            ("INFO", "sms queued recipient={rcp} channel=sms"),
            ("ERROR", "webhook failed recipient={rcp} channel=webhook status=503"),
        ],
        "extra": lambda: {
            "recipient": f"+1555{random.randint(1000000, 9999999)}",
            "channel": random.choice(["email", "sms", "webhook"]),
        },
    },
}
</code></pre>
<p>Setting <code>elasticsearch.index</code> to <code>logs.otel</code> as a resource attribute routes the data into the wired streams root instead of the default OTLP data stream.</p>
<pre><code>def setup_provider():
    resource = Resource.create({"elasticsearch.index": "logs.otel"})
    provider = LoggerProvider(resource=resource)
    provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
    set_logger_provider(provider)
    handler = LoggingHandler(level=logging.INFO, logger_provider=provider)
    root = logging.getLogger()
    root.setLevel(logging.INFO)
    root.addHandler(handler)
    return provider
</code></pre>
<p>Run the notebook to emit 115 records with an uneven split across teams.</p>
<p>Open <strong>Observability &gt; Streams &gt; <code>logs.otel</code></strong> and switch to the <strong>Partitioning</strong> tab. You should see the ingested data in the preview panel, with attributes like <code>team</code>, <code>service.name</code>, and the team-specific fields visible in the columns.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltae43e2a923d42095/6a85ce0f5c2790885df59b6f/image4.png" alt="Partitioning tab on logs.otel with the Get partitions suggestions button and data preview" /></p>
<h2 id="howstreamsaipartitioningproposeschildstreams">How Streams AI Partitioning proposes child streams</h2>
<p>Streams AI Partitioning analyzes up to 1,000 documents from the parent stream, identifies attribute clustering and cardinality distribution, then proposes child streams, keyed on whichever field best separates the data logically (the ML approach behind this analysis is detailed in <a href="https://www.elastic.co/observability-labs/blog/automated-log-parsing-ml-streams">automated log parsing in Streams</a>).</p>
<p>For the data we just emitted, the AI proposed three child streams keyed on <code>attributes.service.name</code>:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt54fb99bf7450bb1a/6a85ce11d6cf29559dbb093b/image5.png" alt="Review partitioning suggestions keyed on attributes.service.name" /></p>
<p>Each suggestion shows a <a href="https://www.elastic.co/docs/solutions/observability/streams/management/streamlang">Streamlang</a> condition and the percentage of sampled documents that would match. The AI picked <code>service.name</code> because it is a standard OpenTelemetry attribute and a natural identifier for any single workload.</p>
<p>This is a reasonable first proposal, but it is worth thinking about what happens as the deployment grows. Right now there are three services because there are three teams. Tomorrow, Payments might add a <code>refunds-api</code> and a <code>fraud-detector</code>. Each new service would mechanically create another child stream, and over time you would end up with dozens of partitions for what is really just three organizational boundaries.</p>
<p>Elastic's <a href="https://www.elastic.co/docs/solutions/observability/streams/management/partitioning#streams-partitioning-recommendations">partitioning recommendations</a> prefer logical groupings like team or technology type, and aim for tens of partitions rather than hundreds. A <code>team</code>-keyed partitioning is more stable because Payments stays one child stream no matter how many services that team operates.</p>
<h2 id="refiningstreamsaipartitioningsuggestionsinnaturallanguage">Refining Streams AI Partitioning suggestions in natural language</h2>
<p>After reviewing the AI's initial suggestions in Streams AI Partitioning, click <strong>Modify suggestions</strong> to open a free-text prompt.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4c4c5bfe6a47ec17/6a85ce14342d6955af21b159/image6.png" alt="Modify suggestions modal with a plain-English prompt to partition by attributes.team" /></p>
<p>After submitting, the AI regenerates the suggestions. Now the three cards are keyed on <code>attributes.team</code> instead of <code>service.name</code>:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt64002e7c2459f0b9/6a85ce17f5f1a028cc2ec969/image7.png" alt="Regenerated suggestions keyed on attributes.team, all three selected" /></p>
<p>Select all three and click <strong>Accept selected</strong>. A confirmation dialog shows the streams that will be created, each with its <code>WHERE attributes.team equals &lt;team&gt;</code> condition.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt67702e77c93a7775/6a85ce1a9a32f1e5eda7e042/image8.png" alt="Create 3 streams confirmation dialog listing payments, checkout, and notifications" /></p>
<p>Click <strong>Create all streams</strong>. The Partitioning tab now shows the three child streams as part of the <code>logs.otel</code> parent:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc0aa396438b856c5/6a85ce06342d693b6421b14f/image1.png" alt="Partitioning tab showing logs.otel.payments, logs.otel.checkout, and logs.otel.notifications" /></p>
<p>Every new document arriving at the OTLP endpoint will be routed into the correct child according to these conditions. You can open any child stream to verify its data. For example, <code>logs.otel.checkout</code> shows only checkout logs:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd89be7d1789e0361/6a85ce1dba7acc0afb992192/image9.png" alt="logs.otel.checkout child stream data preview showing only checkout-web events" /></p>
<h2 id="howdoyousetperteamlogretentioninelasticsearchstreams">How do you set per-team log retention in Elasticsearch Streams?</h2>
<p>After Streams AI Partitioning creates child streams, each one can have its own lifecycle configuration independently of the parent. Because wired streams use a parent-child hierarchy, every child inherits retention, processors, and schema from the parent by default. You only need to override the partitions you need to change.</p>
<p>Open the child stream <code>logs.otel.payments</code> and go to the <a href="https://www.elastic.co/docs/solutions/observability/streams/management/retention">Retention</a> tab. Click <strong>Edit retention method</strong>, select <strong>Custom period</strong>, and set it to 90 days.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4181096276f4ba5/6a85ce2098292622ff583944/image10.png" alt="Edit data retention dialog with Custom period set to 90 days" /></p>
<p>Do the same for the other teams with the retention that fits their needs:</p>
<p>| Stream | Retention | Rationale |
| :---- | :---- | :---- |
| <code>logs.otel.payments</code> | 90 days | Sensitive financial data, compliance requirements |
| <code>logs.otel.checkout</code> | 30 days | Useful for debugging, no long-term need |
| <code>logs.otel.notifications</code> | 7 days | High volume, low value after delivery confirmation |</p>
<h2 id="conclusionfromsharedindextoperteamstreamswithoutroutingrules">Conclusion: from shared index to per-team streams, without routing rules</h2>
<p>A shared Elastic deployment with several teams shipping logs is the normal starting point. Organizing it used to mean writing routing rules upfront or maintaining separate index templates and ILM policies by hand.</p>
<p>With Streams AI Partitioning, the workflow is different: you let the data arrive, let the AI read what showed up, refine the suggestions in natural language when they need adjusting, and accept.</p>
<p>The result is a set of child streams that inherit everything from the parent while giving each team its own retention and processing, without any manual template management.</p>
<h2 id="nextsteps">Next steps</h2>
<ul>
<li>Try the <a href="https://github.com/Delacrobix/Taming-the-Log-Chaos-with-Streams-AI-Partitioning/blob/main/notebook.ipynb">companion notebook</a> to generate your own multi-team data.</li>
<li>Read <a href="https://www.elastic.co/observability-labs/blog/simplifying-retention-management-with-streams">How Streams in Elastic Observability Simplifies Retention Management</a> for a deeper look at the retention model.</li>
<li>Read <a href="https://www.elastic.co/observability-labs/blog/elastic-streams-processing">Streams Processing: Stop Fighting with Grok</a> to explore the parsing side of Streams when teams need different processors.</li>
<li>Read <a href="https://www.elastic.co/observability-labs/blog/elastic-observability-streams-ai-logs-investigations">Introducing Streams for Observability</a> for the broader investigation story Streams is part of.</li>
</ul>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/elasticsearch-streams-ai-partitioning-log-routing</link>
    <guid isPermaLink="false">elasticsearch-streams-ai-partitioning-log-routing</guid>
    <category><![CDATA[Logs Analytics]]></category>
    <category><![CDATA[OpenTelemetry]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Aleksandar Panov]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltaf5bc8b7f4699ea7/6a85ce2393ffb97251b9148d/header.png" length="0" type="image/png"/>
    <pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>