> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://respan.ai/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://respan.ai/docs/_mcp/server.

# Elasticsearch (tracing)

> Trace synchronous and asynchronous Elasticsearch operations with Respan, including searches, indexing, bulk requests, responses, and failures.

[Elasticsearch](https://www.elastic.co/elasticsearch) is a distributed search and analytics engine. Respan's instrumentation wraps the official Python client's shared transport boundary, so the synchronous `Elasticsearch` client and asynchronous `AsyncElasticsearch` client receive the same tracing coverage.

Each request emits a canonical Respan task span with its operation, sanitized target, status, timing, and optional request and response content.

#### Set up Respan

Create an account at [platform.respan.ai](https://platform.respan.ai) and grab an [API key](https://platform.respan.ai/platform/gateway/api-keys).

Run `npx @respan/cli setup` to set up with your coding agent.

#### Example projects

* [Python package README](https://github.com/RespanAI/respan/tree/main/python-sdks/instrumentations/respan-instrumentation-elasticsearch)
* [Runnable Python examples](https://github.com/respanai/respan-example-projects/tree/main/python/tracing/elasticsearch)

#### Tracing

## Setup

#### Install packages

```bash
pip install respan-ai respan-instrumentation-elasticsearch "elasticsearch[async]>=8.13,<10"
```

#### Set environment variables

```bash
export RESPAN_API_KEY="YOUR_RESPAN_API_KEY"
```

Your Elasticsearch connection can point to a local cluster, Elastic Cloud, or another compatible deployment.

#### Initialize and run

```python
import os

from elasticsearch import Elasticsearch
from respan import Respan, workflow
from respan_instrumentation_elasticsearch import ElasticsearchInstrumentor

respan = Respan(
    api_key=os.environ["RESPAN_API_KEY"],
    app_name="elasticsearch-tracing",
    instrumentations=[ElasticsearchInstrumentor()],
)
client = Elasticsearch("http://localhost:9200")


@workflow(name="elasticsearch_search_workflow")
def search_articles():
    client.index(
        index="articles",
        id="doc-1",
        document={"title": "Tracing Elasticsearch with Respan"},
    )
    return client.search(
        index="articles",
        query={"match": {"title": "tracing"}},
    )


try:
    print(search_articles().body)
finally:
    client.close()
    respan.shutdown()
```

Passing `ElasticsearchInstrumentor()` to `Respan` activates and later deactivates the transport patches for you.

#### View your trace

Open the [Traces page](https://platform.respan.ai/platform/traces) and search for workflow name `elasticsearch_search_workflow`. The workflow contains nested `elasticsearch.index` and `elasticsearch.search` task spans.

## Asynchronous client

`AsyncElasticsearch` is covered by the same instrumentor; no additional setup is required.

```python
import asyncio
import os

from elasticsearch import AsyncElasticsearch
from respan import Respan, workflow
from respan_instrumentation_elasticsearch import ElasticsearchInstrumentor

respan = Respan(
    api_key=os.environ["RESPAN_API_KEY"],
    instrumentations=[ElasticsearchInstrumentor()],
)


@workflow(name="elasticsearch_async_bulk_workflow")
async def index_and_search(client: AsyncElasticsearch):
    await client.bulk(
        operations=[
            {"index": {"_index": "articles", "_id": "async-1"}},
            {"title": "Async Elasticsearch tracing"},
        ]
    )
    return await client.search(index="articles", query={"match_all": {}})


async def main():
    client = AsyncElasticsearch("http://localhost:9200")
    try:
        result = await index_and_search(client)
        print(result.body)
    finally:
        await client.close()
        respan.shutdown()


asyncio.run(main())
```

## Covered operations

The transport-level integration covers every request made by the official clients. Common operation names include:

| Operation  | Examples                                                                   |
| ---------- | -------------------------------------------------------------------------- |
| Documents  | `index`, `get`, `exists`, `update`, and `delete`                           |
| Search     | `search`, `count`, and multi-search requests                               |
| Bulk       | Synchronous and asynchronous bulk indexing and deletion                    |
| Indices    | Refresh and other index-management requests                                |
| Cluster    | Cluster health and other cluster APIs                                      |
| Other APIs | Requests not matched above are still traced as Elasticsearch request spans |

## Content capture

Request and response bodies are captured by default. Disable content capture when documents, queries, or results may contain sensitive data:

```python
from respan import Respan
from respan_instrumentation_elasticsearch import ElasticsearchInstrumentor

respan = Respan(
    instrumentations=[ElasticsearchInstrumentor(capture_content=False)],
)
```

With capture disabled, spans retain the operation, sanitized target, HTTP status, timing, and exception type while omitting request and response bodies. Document and task identifiers in targets are sanitized, and request headers are never captured.

## Errors

Transport exceptions are re-raised after the span records OpenTelemetry error status, `status_code`, and `error.message`. HTTP error responses are marked the same way. When content capture is disabled, backend response details and exception messages are replaced with a generic status or exception type.

```python
from elasticsearch import NotFoundError

try:
    client.get(index="articles", id="missing-document")
except NotFoundError as exc:
    print(f"Expected Elasticsearch status: {exc.meta.status}")
```

## Configuration

| Parameter             | Type               | Default | Description                                                                                                |
| --------------------- | ------------------ | ------- | ---------------------------------------------------------------------------------------------------------- |
| `capture_content`     | `bool`             | `True`  | Capture request and response bodies in canonical entity input and output.                                  |
| `max_attribute_chars` | `int`              | `16000` | Maximum serialized size for each structured input or output attribute; values below 512 are raised to 512. |
| `request_hook`        | `callable \| None` | `None`  | Called with `(span, method, target, kwargs)` before the transport request.                                 |
| `response_hook`       | `callable \| None` | `None`  | Called with `(span, response_body)` after a successful transport response.                                 |
| `api_key`             | `str \| None`      | `None`  | `Respan` option that falls back to `RESPAN_API_KEY`.                                                       |
| `base_url`            | `str \| None`      | `None`  | `Respan` option that falls back to `RESPAN_BASE_URL`.                                                      |

## Add request metadata

Use `propagate_attributes` to group Elasticsearch activity by user, thread, or application metadata.

```python
from respan import propagate_attributes


def search_for_user(user_id: str, question: str):
    with propagate_attributes(
        customer_identifier=user_id,
        thread_identifier="search-session-123",
        metadata={"index": "articles", "feature": "knowledge-search"},
    ):
        return client.search(
            index="articles",
            query={"match": {"body": question}},
        )
```

## Lifecycle

Activation and deactivation are idempotent and shared safely across multiple instrumentor instances. When you pass the instrumentor to `Respan`, call `respan.shutdown()` when the process exits so spans are flushed and the final shared transport patches are removed.