Authored by Stephen Crowley
AWS DevOps Agent was built for modern software environments: engineering teams running heterogeneous infrastructure, juggling multiple observability tools, and without enough bandwidth to manually investigate every service incident. Its GA announcement highlighted reductions in mean time to resolution from two hours to thirty minutes — but that kind of improvement only happens when the underlying telemetry data is clean, structured, and fast to query.
This is where Bronto comes in. Not as a point integration built specifically for AWS DevOps Agent, but as a telemetry layer designed from the ground up around the principles that make any AI-driven investigation successful: trust in every result, discovery before every query, full context in every response, and consistent behaviour across every dataset.
We put this to the test directly. In a side-by-side evaluation, the same system — the OpenTelemetry demo application — sent data simultaneously to Bronto, Elasticsearch/Lucene (via OpenSearch), and Grafana Loki, all exposed via their respective MCP integrations. AWS DevOps Agent ran the same incident investigation against all three, then we asked it to judge.
We gave it a deliberately neutral prompt, designed to let the agent reach its own conclusions based on what it had just experienced:
Compare Bronto, Elasticsearch/OpenSearch, and Grafana Loki for use by AI agents (like AWS DevOps Agent) performing incident investigation. Focus the comparison on what matters for AI agents, NOT human usability. Include Recommendations for AWS DevOps Agent. Do NOT focus on: Query syntax complexity (AI handles nested JSON fine), Human readability of responses, Nesting depth.
The prompt explicitly told the agent to ignore the things Bronto is often praised for by humans — simple query syntax, flat response structure, readable output. We wanted to know what mattered to the agent itself, working from its own experience querying all three systems during a live investigation.
Its verdict was unambiguous. For incident investigation, Bronto is the clear winner. The evaluation identified four factors that actually determine whether an AI agent can investigate reliably:
- Silent failure risk — Does the system fail quietly with wrong results?
- Data discovery — Can the agent explore before querying?
- Response completeness — Does one query return enough context?
- Deterministic behavior — Does the same query always produce the same behavior?
| System | AI Agent Suitability | Primary Issue |
|---|---|---|
| Bronto | High (9.4/10) | None significant |
| Elasticsearch/Lucene | Medium (5.3/10) | Silent failures from case/mapping issues |
| Grafana Loki | Low (4.6/10) | Limited discovery, minimal context |
1. Silent Failure Risk
The most critical factor for AI agents is not whether they can express a query — it's whether they can trust the result. When a query returns empty results, the agent must decide: is there genuinely no matching data, or did the query fail silently for some other reason? Unlike a human analyst who might notice something feels off and try a different approach, an AI agent treats an empty result as ground truth and continues its investigation on that basis.
Elasticsearch/Lucene uses case-sensitive keyword field matching by default. A filter written as { "term": { "service.name": "Checkout" } } returns zero results if the indexed value is checkout — and it does it silently, with no error. The same silent failure applies to wrong field types (.keyword vs analyzed), fields that exist in the data but weren't mapped at index setup time, and any variation in field naming between indices. The agent concludes "no data" when the reality is "query did not match."
Loki has fewer mapping complexities, but labels are case-sensitive and log content grep is case-sensitive by default. An agent querying {service_name="Checkout"} when the label value is checkout gets zero results with no signal that the label exists with a different casing.
Bronto returns a clear error for unknown field names. Its ILIKE operator handles any casing — $service.name = 'Checkout' matches checkout, CHECKOUT, and any other variant. When Bronto returns empty results, the agent can treat that as a trustworthy signal that no matching data exists.
| Failure Type | Bronto | Elasticsearch/Lucene | Loki |
|---|---|---|---|
| Wrong case in query value | Still works (ILIKE) | Silent empty result | Silent empty result |
| Wrong case in field name | Clear error | Silent empty result | Silent empty result |
| Field doesn't exist | Clear error | Silent empty result | Silent empty result |
Wrong field type (.keyword vs analyzed) |
N/A (consistent) | Silent wrong results | N/A |
| Typo in field name | Clear error | Silent empty result | Silent empty result |
In our evaluation, Elasticsearch/Lucene returned zero events for every error and warning query across cart, payment, and ad services — even though the events existed in the index. An agent relying on those results would have proceeded under the conclusion that no active errors existed, while multiple services were actively failing. Bronto returned all of them.
| System | When Query Returns Empty | Agent Concludes | Actually Correct? |
|---|---|---|---|
| Bronto | No matching data exists | "No errors found" | Likely correct |
| Elasticsearch/Lucene | Unknown reason | "No errors found" | May be wrong |
| Loki | Unknown reason | "No errors found" | May be wrong |
2. Data Discovery
Can the agent explore what data exists and verify a query will work before it runs it? This determines whether the agent enters an investigation with confidence or proceeds on assumptions that may silently be wrong.
Bronto treats discovery as a first-class workflow — and critically, it works across all data in Bronto, not just OpenTelemetry. The agent calls get_datasets() to get a named catalogue of every dataset. It calls get_keys(log_id="checkout") to list all searchable fields. It calls get_key_values(log_id="checkout", key="$severity_text") to enumerate the actual values present — ["INFO", "ERROR", "WARN"]. By the time the agent issues its first search query, it knows the field exists, knows what values are in it, and can form that query with confidence.
Elasticsearch/Lucene's discovery path is complex and unreliable. GET _cat/indices returns a raw index list. GET logs-checkout-*/_mapping returns hundreds of lines of nested JSON that describe field types but not values. To discover what values actually exist in a field, the agent must run a separate aggregation query — and must already know whether the field is .keyword or analyzed to construct it correctly.
Loki's discovery is limited to labels. list_loki_label_names() typically returns three or four labels: pod, service_name, stream. There is no way to discover the fields inside log content.
| Capability | Bronto | Elasticsearch/Lucene | Loki |
|---|---|---|---|
| List available datasets |
get_datasets (all sources) |
_cat/indices (raw list) |
Via labels only |
| List searchable fields |
get_keys (simple list) |
_mapping (complex nested JSON) |
Labels only (3–5 fields) |
| List field values | get_key_values |
Terms aggregation query | Label values only |
| Verify field exists before query | Easy, explicit | Must parse mapping | Cannot for log content |
| Works across non-OTEL data sources | Yes | Varies by index config | Not applicable |
| Zero-knowledge start | Yes | No | No |
3. Response Completeness
Does a single query return enough context for the agent to understand what happened, identify impact, and decide what to investigate next?
Bronto returns row-based dense JSON where every event is a self-describing object. A single error event reliably includes service name, service version, pod name, node name, namespace, trace ID, and user ID:
{
"@raw": "exporter export timeout: rpc error: code = Unavailable",
"@time": "2026-05-04 11:36:56.845 UTC",
"@status": "error",
"attributes": {
"$service.name": "checkout",
"$service.version": "2.2.0",
"$k8s.pod.name": "checkout-f86478f-xgclj",
"$k8s.node.name": "ip-192-168-46-190.ec2.internal",
"$trace_id": "367379916186c4759c7c226a0350408f",
"$user_id": "dea7494c-4867-11f1-ab4e-6edbf8c21e43"
}
}
In our live test, Bronto found 15 real error events in 3 queries, with root cause visible immediately: high memory usage causing exporter timeouts. Trace IDs were present on every event, allowing the agent to pivot to trace correlation without an additional query.
Elasticsearch/Lucene's response completeness depends entirely on what was mapped when the index was created. The response also wraps every hit in metadata (_index, _id, _score, _shards) that the agent must parse past before reaching the actual log data.
Loki returned zero application errors in the same test — only synthetic canary logs. Where it does return results, all structured context is embedded inside the raw log line string.
| Context Needed | Bronto | Elasticsearch/Lucene | Loki |
|---|---|---|---|
| Error message |
@raw field |
message field |
Log line |
| Service name | $service.name |
If mapped | If label exists |
| Service version | $service.version |
If mapped | Rarely available |
| Pod / node name | Indexed | If mapped | Rarely available |
| Trace ID | $trace_id |
If mapped | Must parse log line |
| User ID | $user_id |
If mapped | Must parse log line |
| Timestamp | Human-readable | ISO format | Nanoseconds |
| Investigation Task | Bronto | Elasticsearch/Lucene | Loki |
|---|---|---|---|
| Find error + full context | 1 query | 1–3 queries | 3–5+ queries |
| Correlate with trace | 0 additional (trace_id included) | 0–1 additional | 2+ additional |
| Identify user impact | 0 additional (user_id included) | 0–1 additional | 2+ additional |
| Find related logs by trace | 1 query | 1 query | 5+ queries (per service) |
4. Deterministic Behavior
Does the agent's investigative workflow work consistently across different services and datasets, or must it learn a different approach for each index it encounters?
What is consistent in Bronto is the workflow the agent uses to deal with variation. Discovery is the deterministic part: get_datasets() always returns the catalogue, get_keys(log_id) always returns the actual fields present in that dataset, and get_key_values(log_id, key) always returns the actual values. That same three-call pattern works identically for an OTEL dataset, a syslog dataset, or a custom JSON ingest:
# OTEL service
get_keys("checkout") → [$service.name, $severity_text, $trace_id, ...]
search: "$severity_text ILIKE 'error'"
# Custom JSON ingest with a different schema
get_keys("legacy-app") → [app_name, log_level, request_id, ...]
search: "log_level ILIKE 'error'"
# Syslog
get_keys("network-devices") → [host, facility, severity, message, ...]
search: "severity ILIKE 'err' OR severity ILIKE 'crit'"
The fields differ but the workflow does not — so the agent never has to guess.
Elasticsearch/Lucene cannot make this guarantee. There is no enforced standard for field naming — one index might store service name as service.name.keyword, another as serviceName (analyzed), another as svc. Two indices with the same conceptual data can require materially different agent logic.
| Aspect | Bronto | Elasticsearch/Lucene | Loki |
|---|---|---|---|
| Discovery workflow | Same 3 calls for any dataset |
_mapping shape varies |
Labels-only |
| Field types | Consistent within a dataset | Varies by mapping | N/A |
| Query behavior | Predictable | Depends on analyzers | Depends on labels |
| Cross-dataset patterns | Discover then query — works for any source | Per-index logic likely needed | Per-stream logic likely needed |
What Each Platform Was Built For
Each of these platforms reflects the use case it was designed for — and none of them, except Bronto, were designed with AI agents in mind.
Elasticsearch/Lucene was built around a human at a dashboard. The Query DSL is JSON because it was meant to be assembled by Kibana on behalf of an analyst clicking through visualisations. Hand the same API to an agent, and the assumptions invert: silent case-sensitivity, mapping-dependent matching, and relevance ranking that buries rare events become failure modes.
Grafana Loki was built around a single bet on cost — index only labels, store everything else as opaque log lines, charge less than indexed alternatives. The same minimalism that keeps Loki cheap keeps it opaque to an agent that needs to discover fields, correlate by trace ID, or filter on high-cardinality attributes. The cost argument has weakened independently: Loki Cloud lists $0.50/GB ingested versus Bronto's $0.10/GB.
Bronto was built for AI agent investigation from the ground up. Discovery as a first-class workflow, case-insensitive matching by default, dense self-describing JSON responses, deterministic discovery patterns across heterogeneous data sources.
| Factor | Weight | Bronto | Elasticsearch/Lucene | Loki |
|---|---|---|---|---|
| Silent failure risk | 30% | 9/10 | 4/10 | 5/10 |
| Data discovery | 25% | 10/10 | 5/10 | 4/10 |
| Response completeness | 20% | 10/10 | 7/10 | 3/10 |
| Deterministic behavior | 15% | 9/10 | 5/10 | 5/10 |
| Query simplicity | 10% | 9/10 | 6/10 | 7/10 |
| Weighted total | 9.4/10 | 5.3/10 | 4.6/10 |
Connecting Bronto to AWS DevOps Agent is straightforward: deploy Bronto's hosted MCP server, route your telemetry to it, and authorise the agent. Both are built on the same open standards, so it works the first time, with no custom skills or pre-configuration.
See It in Action
We recorded AWS DevOps Agent running a live incident investigation against Bronto — no pre-configuration, no custom skills, just the hosted MCP server connected and pointed at the OpenTelemetry demo.
The investigation completed in under a minute. Once started, the agent generated its investigation plan within eight seconds. Within sixty seconds, it had queried Bronto, identified that the ad service was experiencing a high error rate, followed the statement IDs to the specific log lines causing failures, and surfaced the symptom: ads were failing to load. After further analysis — scanning the full codebase and correlating what Bronto's logs revealed — the agent returned its root cause: the adFailure feature flag was controlled entirely by an external flagd service.
From alert to root cause, with the data layer doing exactly what it was designed to do.
Top comments (0)