> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bronto.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Microsoft Foundry observability with OpenTelemetry

> Send logs, metrics, and traces from Microsoft Foundry apps and agents to Bronto over OTLP — gen_ai spans, token usage, tool calls, and multi-agent traces.

Apps and agents built on [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/foundry/) (formerly Azure AI Foundry) are already OpenTelemetry-instrumented: the supported SDKs and agent frameworks emit spans following the [OpenTelemetry GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md) — model calls, token usage, tool invocations, and multi-agent task hierarchies.

Because that telemetry is standard OpenTelemetry, one initialisation block sends **logs, metrics, and traces** from your Foundry app straight to Bronto over OTLP/HTTP — no Bronto-specific SDK involved.

This page covers the app-level signal. For general Azure platform, diagnostic, and activity logs from the same subscription, see [Ingesting Azure Data into Bronto](/integrations/azure-overview); for the attributes Bronto surfaces and how to search them, see [LLM Observability](/ai-features/llm-observability).

<Note>
  GenAI instrumentation moves quickly. The package names, environment variables, and default attribute shapes below reflect the instrumentation available at the time of writing — confirm them against the release you deploy, and inspect a real trace before building version-sensitive dashboards.
</Note>

## Prerequisites

* A Microsoft Foundry project with an instrumented app or agent
* Python 3.10 or later, and `pip`
* A Bronto ingestion API key — see [API Keys](/Account-Management/API-Keys#create-a-new-api-key) for how to create one with ingestion permissions
* Your Bronto region: `eu` or `us`
* A dataset and collection name for the app's telemetry. With OTLP these come from resource attributes, not headers: `service.name` → dataset, `service.namespace` → collection. See [Data Organization](/Search-and-Visualize/Partitions).

## Choose an OpenTelemetry-native instrumentation

Every framework in the Foundry ecosystem has more than one way to produce GenAI telemetry: vendor distributions such as [OpenLLMetry](/integrations/openllmetry) or OpenInference wrap the framework in their own SDK, while the OpenTelemetry-native route uses either upstream OpenTelemetry instrumentation or the telemetry the framework emits itself.

This page assumes the OpenTelemetry-native route. It keeps `gen_ai.*` attributes aligned with the upstream semantic conventions, puts no vendor SDK in the request path, and means the exporter configuration below is the only Bronto-specific code you write.

| Framework                                 | OpenTelemetry-native route                                                                                                                     | Documentation                                                                                                                                                          |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OpenAI SDK / Azure OpenAI                 | `opentelemetry-instrumentation-openai-v2`, an upstream OpenTelemetry contrib package                                                           | [opentelemetry-python-contrib](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai/opentelemetry-instrumentation-openai-v2) |
| Semantic Kernel                           | Emits OpenTelemetry traces, metrics, and logs itself — no instrumentation package, but its GenAI diagnostics are behind an experimental switch | [Observability in Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel/concepts/enterprise-readiness/observability/)                                     |
| Microsoft Agent Framework                 | Emits OpenTelemetry telemetry itself, enabled through the framework's own observability setup                                                  | [Agent Framework observability](https://learn.microsoft.com/en-us/agent-framework/user-guide/observability/)                                                           |
| OpenAI Agents SDK                         | Its own tracing layer with pluggable processors; bridge it to OpenTelemetry with an OTel trace processor                                       | [OpenAI Agents SDK tracing](https://openai.github.io/openai-agents-python/tracing/)                                                                                    |
| LangChain / LangGraph                     | OTLP export, either via an OpenTelemetry instrumentation package or LangSmith's OTLP endpoint routing                                          | [LangChain](/integrations/langchain), [LangSmith OpenTelemetry](https://docs.smith.langchain.com/observability/how_to_guides/opentelemetry)                            |
| Foundry Agent Service (`azure-ai-agents`) | An instrumentor shipped with the Azure SDK, covering agent runs and tool calls                                                                 | [Observability in generative AI](https://learn.microsoft.com/en-us/azure/foundry/concepts/observability)                                                               |

Whichever you choose, everything below is identical — the exporter, the endpoints, the credential, the resource attributes — because all of these emit through the OpenTelemetry providers you configure.

<Warning>
  Verify the exact package name, import path, and enablement call for your framework against its own documentation before copying this table into a runbook. Only the OpenAI SDK row is exercised by the code samples below.
</Warning>

## Install the SDK and instrumentation

```bash theme={"dark"}
pip install \
  opentelemetry-sdk \
  opentelemetry-exporter-otlp-proto-http \
  opentelemetry-instrumentation-openai-v2
```

Swap the last package for the instrumentation your framework uses, or drop it entirely for a framework that emits OpenTelemetry itself.

## Endpoints and authentication

Bronto has one OTLP/HTTP endpoint per signal, per region. Every request carries the API key in the `x-bronto-api-key` header.

| Signal  | EU region                                   | US region                                   |
| ------- | ------------------------------------------- | ------------------------------------------- |
| Logs    | `https://ingestion.eu.bronto.io/v1/logs`    | `https://ingestion.us.bronto.io/v1/logs`    |
| Metrics | `https://ingestion.eu.bronto.io/v1/metrics` | `https://ingestion.us.bronto.io/v1/metrics` |
| Traces  | `https://ingestion.eu.bronto.io/v1/traces`  | `https://ingestion.us.bronto.io/v1/traces`  |

The SDK appends `/v1/logs`, `/v1/metrics`, and `/v1/traces` to `OTEL_EXPORTER_OTLP_ENDPOINT` automatically, so configure the **base** URL and let one setting cover all three signals:

```bash theme={"dark"}
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingestion.<REGION>.bronto.io"   # eu or us
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_HEADERS="x-bronto-api-key=<YOUR_API_KEY>"
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE="delta"

# Bronto routing: service.name → dataset, service.namespace → collection.
export OTEL_SERVICE_NAME="foundry-agent"
export OTEL_RESOURCE_ATTRIBUTES="service.namespace=ai-apps,deployment.environment=production"
```

## Configure logs, metrics, and traces

Add this module to your project and call `configure_bronto_telemetry()` once, at the very top of your entrypoint — before the instrumented client is constructed and before your first log statement or span.

```python otel_bronto.py theme={"dark"}
import logging

from opentelemetry import metrics, trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor


def configure_bronto_telemetry() -> tuple[TracerProvider, MeterProvider, LoggerProvider]:
    # Reads OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES — one identity for
    # all three signals, which is what makes them correlate in Bronto.
    resource = Resource.create()

    # Traces — GenAI spans from your instrumentation land here.
    tracer_provider = TracerProvider(resource=resource)
    tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
    trace.set_tracer_provider(tracer_provider)

    # Metrics — GenAI metric instruments plus anything your app records.
    reader = PeriodicExportingMetricReader(OTLPMetricExporter())
    meter_provider = MeterProvider(resource=resource, metric_readers=[reader])
    metrics.set_meter_provider(meter_provider)

    # Logs — bridges the standard logging module, so structured `extra=` fields
    # arrive as queryable attributes and existing log statements are unchanged.
    logger_provider = LoggerProvider(resource=resource)
    logger_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
    set_logger_provider(logger_provider)
    logging.getLogger().addHandler(LoggingHandler(logger_provider=logger_provider))
    logging.getLogger().setLevel(logging.INFO)

    return tracer_provider, meter_provider, logger_provider
```

The exporters take no arguments because they read the endpoint, protocol, and headers from the environment. To keep configuration in code instead, pass the **full** signal path to each one — `OTLPSpanExporter(endpoint="https://ingestion.<REGION>.bronto.io/v1/traces", headers={"x-bronto-api-key": ...})`, and likewise for logs and metrics.

Enable instrumentation after the providers are registered, and build your client after that:

```python app.py theme={"dark"}
import os

from otel_bronto import configure_bronto_telemetry

tracer_provider, meter_provider, logger_provider = configure_bronto_telemetry()

# Instrumentation attaches to the registered providers.
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor

OpenAIInstrumentor().instrument()

from openai import AzureOpenAI

client = AzureOpenAI(
    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
    api_key=os.environ["AZURE_OPENAI_API_KEY"],
    api_version="<api-version>",
)

response = client.chat.completions.create(
    model="<your-deployment-name>",
    messages=[{"role": "user", "content": "Summarise today's incidents."}],
)
```

Any log emitted inside an active span automatically carries that span's `trace_id` and `span_id`, so you can jump from a log line to the model call that produced it with no manual context propagation.

<Tip>
  **Short-lived processes.** The batch processors and the metric reader export on a timer, so a script, job, or evaluation run that exits immediately can drop its last batch. Shut the providers down before exit:

  ```python theme={"dark"}
  tracer_provider.shutdown()
  meter_provider.shutdown()
  logger_provider.shutdown()
  ```
</Tip>

<Tip>
  **Framework spans without code changes.** Install the `opentelemetry-instrumentation-<framework>` packages for your web framework and HTTP client and run your app with `opentelemetry-instrument python app.py`. The CLI configures the providers itself from the same `OTEL_*` variables above, so use it *instead of* `otel_bronto.py` — and add `opentelemetry-instrumentation-logging` so standard-library logs are still bridged. See [Python zero-code instrumentation](https://opentelemetry.io/docs/zero-code/python/).
</Tip>

## Capturing prompts and responses

Two environment variables control whether the text of prompts and completions is captured, and in what shape:

```bash theme={"dark"}
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
```

* `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` turns content capture on. It is **off by default** in the official instrumentations. `span_only` writes content to `gen_ai.input.messages` and `gen_ai.output.messages` as span attributes, which Bronto surfaces as searchable trace fields. The legacy `true` setting emits content as separate log records correlated by `trace_id`, where it is not queryable on the span itself — prefer `span_only` where your instrumentation supports it. Either way the content reaches Bronto, because the setup above exports logs as well as traces.
* `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` opts into the current GenAI convention shape. Instrumentation libraries keep emitting an older schema by default so they don't break existing dashboards, and support for this opt-in is **instrumentation-specific** — some libraries already emit the current fields, some need a different setting, some ignore it entirely.

<Note>
  Exercise caution when capturing prompts and responses — depending on the nature of your application they may include sensitive data.
</Note>

Semantic Kernel and Microsoft Agent Framework do not use `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` — each has its own switch for sensitive-data capture. See their observability documentation, linked in the table above, for the exact variable names.

## What you'll see in Bronto

### Traces

One app or agent invocation becomes one trace with nested spans:

| Span                 | Source                                    | Notes                                                                       |
| -------------------- | ----------------------------------------- | --------------------------------------------------------------------------- |
| Request / invocation | Your app or web framework instrumentation | Root span for the request                                                   |
| Agent or task span   | Agent framework                           | One per agent step; multi-agent runs nest handoffs beneath the orchestrator |
| `chat`               | GenAI instrumentation                     | One per model call, carrying `gen_ai.*` attributes                          |
| Tool call            | Agent framework                           | One child span per tool the agent invokes                                   |

The attributes worth knowing on the model spans:

| Attribute                                                  | Example       | Notes                                                   |
| ---------------------------------------------------------- | ------------- | ------------------------------------------------------- |
| `gen_ai.provider.name`                                     | `openai`      | Provider — filter and group by this                     |
| `gen_ai.operation.name`                                    | `chat`        | Operation type                                          |
| `gen_ai.request.model` / `gen_ai.response.model`           | `gpt-4o-mini` | Requested model / model actually served                 |
| `gen_ai.usage.input_tokens` / `gen_ai.usage.output_tokens` | `412`, `173`  | Numeric — sum, average, and chart these                 |
| `gen_ai.response.finish_reasons.0`                         | `stop`        | Bronto indexes the first element of the array attribute |
| `gen_ai.tool.name` / `gen_ai.tool.call.id`                 | `get_weather` | On tool-call spans, where the framework emits them      |
| `gen_ai.input.messages` / `gen_ai.output.messages`         | —             | Only present when content capture is enabled            |

Open [Explore Traces](/tracing/explore-traces) and filter by the `service.name` you set. Open the newest trace and confirm the nesting above: the invocation at the root, a `chat` span per model call, and tool-call spans beneath it.

### Metrics

Where your instrumentation implements the [GenAI metric conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-metrics.md), the meter provider exports aggregate instruments alongside the spans:

| Metric                                        | What it measures                                                |
| --------------------------------------------- | --------------------------------------------------------------- |
| `gen_ai.client.operation.duration`            | Total time from issuing a request until the operation completes |
| `gen_ai.client.token.usage`                   | Token usage per call, as a histogram                            |
| `gen_ai.client.operation.time_to_first_chunk` | Client-observed streaming latency                               |
| `gen_ai.server.time_to_first_token`           | Model-server time to first token, where the server reports it   |

Open **Metric Explorer** and select a recently emitted metric to confirm it carries the same `service.name`. Coverage of these instruments varies by library and version — spans are the reliable source of token counts, metrics the cheaper source of aggregate latency. See [Explore Metrics](/metrics/explore-metrics) for building views.

### Logs

Application logs from the bridged `logging` module arrive with `trace_id` and `span_id` attached, so they line up with the trace that produced them. Log structured GenAI fields with `extra=` and they become queryable attributes:

```python theme={"dark"}
import logging

log = logging.getLogger(__name__)

log.info(
    "agent.result",
    extra={
        "event.name": "agent.result",
        "gen_ai.request.model": "<your-deployment-name>",
        "session.id": session_id,
    },
)
```

Avoid printf-style logging (`log.info("tokens=%s", n)`) for data you want to query — that produces a flat message string rather than structured fields.

### Search and aggregate

Bronto indexes attributes with a `$` prefix, so query them in [Log Search](https://app.bronto.io/search) as field predicates:

```sql theme={"dark"}
"$gen_ai.provider.name" = 'openai'
AND "$gen_ai.usage.input_tokens" > 0
```

```sql theme={"dark"}
"$gen_ai.output.messages" ILIKE '%rate limit%'
```

Token usage is numeric, so it aggregates: **Sum** of `$gen_ai.usage.output_tokens` grouped by `$gen_ai.request.model` gives per-model consumption; the **P95** of the same field finds unusually large responses. See [LLM Observability](/ai-features/llm-observability) for the full set of aggregations and [Visualizations](/Search-and-Visualize/Log-Visualization) for building them into a dashboard.

## Troubleshooting

| Symptom                                       | Likely cause                                                                                                                                                                                                                                   |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` / `403` from the exporter               | Header name must be exactly `x-bronto-api-key`, and the key needs ingestion permissions. In `OTEL_EXPORTER_OTLP_HEADERS`, use `key=value` with no quotes or spaces around the `=`.                                                             |
| `404` from the exporter                       | Path mismatch. `OTEL_EXPORTER_OTLP_ENDPOINT` takes the **base** URL and the SDK appends `/v1/logs`, `/v1/metrics`, `/v1/traces`; an explicit `endpoint=` argument takes the **full** signal path.                                              |
| Connection or protocol errors                 | Use the `opentelemetry-exporter-otlp-proto-http` exporters with `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`. Bronto's ingestion endpoints are OTLP/HTTP on port `443`; the gRPC exporter will not reach them.                                  |
| Exports succeed but nothing appears           | Wrong region — an EU key against `ingestion.us.bronto.io` (or the reverse) will not land in your account. Check the region of the endpoint against the region of the key.                                                                      |
| Traces arrive, metrics don't                  | Exponential histograms are rejected. Leave the default explicit-bucket aggregation, and set `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta`. See [Send Metrics to Bronto](/metrics/send-metrics).                                    |
| No spans at all                               | Instrumentation was enabled before the providers were registered, the instrumented client was constructed before either, or the instrumentation package isn't installed. Register the providers first, instrument second, build clients third. |
| Signals appear in long runs but not in jobs   | The process exits before the batch processors flush. Call `shutdown()` on each provider before exit.                                                                                                                                           |
| Spans arrive, prompts and responses are empty | Content capture is off by default. See [Capturing prompts and responses](#capturing-prompts-and-responses) — and confirm your framework honours that variable.                                                                                 |
| Attribute names don't match this page         | Your instrumentation is emitting an older convention. Try `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`, and inspect a real trace to see what the installed version actually emits.                                               |

## Node.js and TypeScript

The pattern is the same: build the three providers with the OTLP/HTTP exporters from `@opentelemetry/exporter-trace-otlp-proto`, `@opentelemetry/exporter-metrics-otlp-proto`, and `@opentelemetry/exporter-logs-otlp-proto`, pointed at the same base endpoint with the same `x-bronto-api-key` header, then register your framework's instrumentation. See [Send Node.js logs, metrics, and traces to Bronto](/opentelemetry/nodejs) for the full setup.

## References

* [LLM Observability](/ai-features/llm-observability)
* [OpenTelemetry on Azure](/integrations/azure-otel)
* [Send Python logs, metrics, and traces to Bronto](/opentelemetry/python)
* [Send Metrics to Bronto](/metrics/send-metrics)
* [Microsoft Foundry documentation](https://learn.microsoft.com/en-us/azure/foundry/)
* [Observability in generative AI](https://learn.microsoft.com/en-us/azure/foundry/concepts/observability)
* [OpenTelemetry GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md)
* [OpenTelemetry GenAI metric conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-metrics.md)

***

For assistance or questions, contact [support@bronto.io](mailto:support@bronto.io).
