From 1d743009b60aa28824f9d949e03b5893d9447d88 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Thu, 16 Jul 2026 07:27:47 +0000 Subject: [PATCH 01/12] Add Agent Server OTLP protocol regression tests Use in-process HTTP/protobuf and gRPC receivers to verify traces, metrics, and logs through the public observability setup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2ddb2a79-14d1-4372-a29d-8a0c65a55b7b --- .../azure-ai-agentserver-core/CHANGELOG.md | 6 + .../dev_requirements.txt | 4 +- .../tests/test_otlp_protocol.py | 247 ++++++++++++++++++ 3 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 sdk/agentserver/azure-ai-agentserver-core/tests/test_otlp_protocol.py diff --git a/sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md b/sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md index ba3a1ab6b47b..179ae5537f91 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md +++ b/sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 2.0.0b8 (Unreleased) + +### Other Changes + +- Added local OTLP HTTP/protobuf and gRPC protocol regression tests covering traces, metrics, and logs without requiring external collector infrastructure. + ## 2.0.0b7 (2026-06-28) ### Features Added diff --git a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt index 2e6ad8b7bf67..285bf0277c06 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt +++ b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt @@ -5,4 +5,6 @@ pytest httpx pytest-asyncio opentelemetry-api>=1.40.0 -opentelemetry-sdk>=1.40.0 \ No newline at end of file +opentelemetry-sdk>=1.40.0 +grpcio +opentelemetry-proto diff --git a/sdk/agentserver/azure-ai-agentserver-core/tests/test_otlp_protocol.py b/sdk/agentserver/azure-ai-agentserver-core/tests/test_otlp_protocol.py new file mode 100644 index 000000000000..0601fdaa9aa4 --- /dev/null +++ b/sdk/agentserver/azure-ai-agentserver-core/tests/test_otlp_protocol.py @@ -0,0 +1,247 @@ +# --------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# --------------------------------------------------------- +"""End-to-end OTLP protocol tests using in-process loopback receivers.""" + +import os +from concurrent.futures import ThreadPoolExecutor +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +import subprocess +import sys +from threading import Lock, Thread + +import grpc +import pytest +from opentelemetry.proto.collector.logs.v1 import ( + logs_service_pb2, + logs_service_pb2_grpc, +) +from opentelemetry.proto.collector.metrics.v1 import ( + metrics_service_pb2, + metrics_service_pb2_grpc, +) +from opentelemetry.proto.collector.trace.v1 import ( + trace_service_pb2, + trace_service_pb2_grpc, +) + +_SIGNALS = {"traces", "metrics", "logs"} +_PACKAGE_ROOT = Path(__file__).resolve().parents[1] +_OTLP_ENV_VARS = ( + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_PROTOCOL", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", +) + +_EMIT_ALL_SIGNALS = """ +from azure.ai.agentserver.core import configure_observability +from opentelemetry import metrics, trace +from opentelemetry._logs import LogRecord, SeverityNumber, get_logger_provider + +configure_observability() + +with trace.get_tracer("agentserver.otlp.test").start_as_current_span("otlp-test-span"): + pass + +metrics.get_meter("agentserver.otlp.test").create_counter("otlp.test").add(1) + +get_logger_provider().get_logger("agentserver.otlp.test").emit( + LogRecord( + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body="otlp-test-log", + ) +) + +trace.get_tracer_provider().force_flush() +metrics.get_meter_provider().force_flush() +get_logger_provider().force_flush() +""" + + +class _SignalReceiver: + def __init__(self) -> None: + self._signals: set[str] = set() + self._lock = Lock() + + @property + def signals(self) -> set[str]: + with self._lock: + return set(self._signals) + + @property + def endpoint(self) -> str: + raise NotImplementedError + + def record(self, signal: str) -> None: + with self._lock: + self._signals.add(signal) + + def __enter__(self) -> "_SignalReceiver": + raise NotImplementedError + + def __exit__(self, *_args: object) -> None: + raise NotImplementedError + + +class _HttpOtlpReceiver(_SignalReceiver): + def __init__(self) -> None: + super().__init__() + receiver = self + services = { + "/v1/traces": ( + "traces", + trace_service_pb2.ExportTraceServiceRequest, + trace_service_pb2.ExportTraceServiceResponse, + ), + "/v1/metrics": ( + "metrics", + metrics_service_pb2.ExportMetricsServiceRequest, + metrics_service_pb2.ExportMetricsServiceResponse, + ), + "/v1/logs": ( + "logs", + logs_service_pb2.ExportLogsServiceRequest, + logs_service_pb2.ExportLogsServiceResponse, + ), + } + + class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def do_POST(self) -> None: # pylint: disable=invalid-name + service = services.get(self.path) + if service is None: + self.send_error(404) + return + + signal, request_type, response_type = service + body = self.rfile.read(int(self.headers.get("Content-Length", "0"))) + request_type.FromString(body) + receiver.record(signal) + + response = response_type().SerializeToString() + self.send_response(200) + self.send_header("Content-Type", "application/x-protobuf") + self.send_header("Content-Length", str(len(response))) + self.end_headers() + self.wfile.write(response) + + def log_message( # pylint: disable=redefined-builtin + self, format: str, *args: object + ) -> None: + del format, args + + self._server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + self._thread = Thread(target=self._server.serve_forever, daemon=True) + + @property + def endpoint(self) -> str: + return f"http://127.0.0.1:{self._server.server_port}" + + def __enter__(self) -> "_HttpOtlpReceiver": + self._thread.start() + return self + + def __exit__(self, *_args: object) -> None: + self._server.shutdown() + self._server.server_close() + self._thread.join(timeout=5) + + +class _GrpcOtlpReceiver(_SignalReceiver): + def __init__(self) -> None: + super().__init__() + receiver = self + + class TraceService(trace_service_pb2_grpc.TraceServiceServicer): + def Export( + self, request, context + ): # pylint: disable=invalid-name,unused-argument + receiver.record("traces") + return trace_service_pb2.ExportTraceServiceResponse() + + class MetricsService(metrics_service_pb2_grpc.MetricsServiceServicer): + def Export( + self, request, context + ): # pylint: disable=invalid-name,unused-argument + receiver.record("metrics") + return metrics_service_pb2.ExportMetricsServiceResponse() + + class LogsService(logs_service_pb2_grpc.LogsServiceServicer): + def Export( + self, request, context + ): # pylint: disable=invalid-name,unused-argument + receiver.record("logs") + return logs_service_pb2.ExportLogsServiceResponse() + + self._server = grpc.server(ThreadPoolExecutor(max_workers=3)) + trace_service_pb2_grpc.add_TraceServiceServicer_to_server( + TraceService(), self._server + ) + metrics_service_pb2_grpc.add_MetricsServiceServicer_to_server( + MetricsService(), self._server + ) + logs_service_pb2_grpc.add_LogsServiceServicer_to_server( + LogsService(), self._server + ) + self._port = self._server.add_insecure_port("127.0.0.1:0") + + @property + def endpoint(self) -> str: + return f"http://127.0.0.1:{self._port}" + + def __enter__(self) -> "_GrpcOtlpReceiver": + self._server.start() + return self + + def __exit__(self, *_args: object) -> None: + self._server.stop(grace=0).wait(timeout=5) + + +@pytest.mark.parametrize( + ("protocol", "receiver_type"), + [ + pytest.param("http/protobuf", _HttpOtlpReceiver, id="http-protobuf"), + pytest.param("grpc", _GrpcOtlpReceiver, id="grpc"), + ], +) +def test_otlp_protocol_exports_all_signals( + protocol: str, + receiver_type: type[_SignalReceiver], +) -> None: + """Agent Server must honor the configured OTLP protocol for every signal.""" + with receiver_type() as receiver: + env = os.environ.copy() + for variable in _OTLP_ENV_VARS: + env.pop(variable, None) + env.update( + { + "OTEL_EXPORTER_OTLP_ENDPOINT": receiver.endpoint, + "OTEL_EXPORTER_OTLP_PROTOCOL": protocol, + "OTEL_EXPORTER_OTLP_TIMEOUT": "1", + } + ) + + result = subprocess.run( + [sys.executable, "-c", _EMIT_ALL_SIGNALS], + cwd=_PACKAGE_ROOT, + env=env, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert receiver.signals == _SIGNALS, ( + f"{protocol} receiver got {sorted(receiver.signals)} instead of " + f"{sorted(_SIGNALS)}.\nsubprocess stderr:\n{result.stderr}" + ) From 6b8dbe927db88cbc9abd66f635bfff6b58191bac Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Thu, 16 Jul 2026 19:34:49 +0000 Subject: [PATCH 02/12] Handle OTLP gRPC export in Agent Server Add Agent Server-managed OTLP gRPC exporter setup when customers configure OTEL_EXPORTER_OTLP_PROTOCOL=grpc, while leaving HTTP/protobuf export delegated to the Microsoft OpenTelemetry distro. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- .../azure-ai-agentserver-core/CHANGELOG.md | 1 + .../azure-ai-agentserver-core/README.md | 10 ++ .../azure/ai/agentserver/core/_tracing.py | 120 ++++++++++++++++-- .../dev_requirements.txt | 1 + .../azure-ai-agentserver-core/pyproject.toml | 1 + 5 files changed, 123 insertions(+), 10 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md b/sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md index 179ae5537f91..d6d3e9392d62 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md +++ b/sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md @@ -5,6 +5,7 @@ ### Other Changes - Added local OTLP HTTP/protobuf and gRPC protocol regression tests covering traces, metrics, and logs without requiring external collector infrastructure. +- Added Agent Server-managed OTLP/gRPC export when `OTEL_EXPORTER_OTLP_PROTOCOL=grpc` is configured. ## 2.0.0b7 (2026-06-28) diff --git a/sdk/agentserver/azure-ai-agentserver-core/README.md b/sdk/agentserver/azure-ai-agentserver-core/README.md index 2a9d56c42346..a994213077c8 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/README.md +++ b/sdk/agentserver/azure-ai-agentserver-core/README.md @@ -143,6 +143,16 @@ export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=..." python my_agent.py ``` +OTLP export is enabled when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. HTTP/protobuf +is the default protocol; set `OTEL_EXPORTER_OTLP_PROTOCOL=grpc` to use an +OTLP/gRPC collector: + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" +export OTEL_EXPORTER_OTLP_PROTOCOL="grpc" +python my_agent.py +``` + ## Troubleshooting ### Logging diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index b0ed26bbeda1..9e340bcc17a0 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -33,9 +33,10 @@ OpenTelemetry is a required dependency — these functions always create real spans. Azure Monitor export is optional (auto-configured by the distro). """ +from collections.abc import AsyncIterable, AsyncIterator # pylint: disable=import-error +from contextlib import contextmanager, nullcontext import logging import os -from collections.abc import AsyncIterable, AsyncIterator # pylint: disable=import-error from typing import Any, Optional, Union from opentelemetry import baggage as _otel_baggage, context as _otel_context, trace @@ -76,6 +77,21 @@ logger = logging.getLogger("azure.ai.agentserver") +_OTLP_HTTP_PROTOBUF = "http/protobuf" +_OTLP_GRPC = "grpc" +_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" +_OTLP_PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL" +_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" +_OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" +_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT" +_OTLP_ENV_VARS = ( + _OTLP_ENDPOINT, + _OTLP_PROTOCOL, + _OTLP_TRACES_ENDPOINT, + _OTLP_METRICS_ENDPOINT, + _OTLP_LOGS_ENDPOINT, +) + # ====================================================================== # Public API: observability setup @@ -180,7 +196,7 @@ def _configure_tracing( agent_blueprint_id = _config.resolve_agent_blueprint_id() or None agent_tenant_id = _config.resolve_agent_tenant_id() or None - span_processors = [ + resolved_span_processors = [ _FoundryEnrichmentSpanProcessor( agent_name=agent_name, agent_version=agent_version, agent_id=agent_id, project_id=project_id, @@ -195,26 +211,36 @@ def _configure_tracing( session_id=_config.resolve_session_id() or None, ), ] + metric_readers: list[Any] = [] + suppress_distro_otlp = _append_grpc_otlp_components( + resolved_span_processors, + metric_readers, + log_record_processors, + ) try: - _setup_distro_export( - resource=resource, - span_processors=span_processors, - log_record_processors=log_record_processors, - connection_string=connection_string, - enable_sensitive_data=enable_sensitive_data, - ) + context = _without_otlp_env() if suppress_distro_otlp else nullcontext() + with context: + _setup_distro_export( + resource=resource, + span_processors=resolved_span_processors, + metric_readers=metric_readers, + log_record_processors=log_record_processors, + connection_string=connection_string, + enable_sensitive_data=enable_sensitive_data, + ) logger.info("Tracing configured successfully via microsoft-opentelemetry distro.") except ImportError: logger.warning("microsoft-opentelemetry is not installed — tracing export disabled.") # Still set up TracerProvider with enrichment processor so spans are created - _ensure_trace_provider(resource, span_processors) + _ensure_trace_provider(resource, resolved_span_processors) def _setup_distro_export( *, resource: Any, span_processors: list[Any], + metric_readers: list[Any], log_record_processors: list[Any], connection_string: Optional[str] = None, enable_sensitive_data: bool = False, @@ -226,6 +252,7 @@ def _setup_distro_export( :keyword resource: OTel resource describing this service. :keyword span_processors: Span processors to register. + :keyword metric_readers: Metric readers to register. :keyword log_record_processors: Log record processors to register. :keyword connection_string: Application Insights connection string. :keyword enable_sensitive_data: Enable sensitive data recording for @@ -236,6 +263,7 @@ def _setup_distro_export( kwargs: dict[str, Any] = { "resource": resource, "span_processors": span_processors, + "metric_readers": metric_readers, "log_record_processors": log_record_processors, "enable_sensitive_data": enable_sensitive_data, } @@ -259,6 +287,78 @@ def _setup_distro_export( use_microsoft_opentelemetry(**kwargs) +def _append_grpc_otlp_components( + span_processors: list[Any], + metric_readers: list[Any], + log_record_processors: list[Any], +) -> bool: + """Append SDK-managed OTLP/gRPC exporters when requested by env vars. + + The Microsoft OpenTelemetry distro currently owns the normal OTLP path but + only bundles the HTTP/protobuf exporter. Agent Server handles the gRPC + protocol here so customers only need to set OTLP environment variables. + Returns True when OTLP environment variables should be hidden from the + distro call so it does not also create HTTP/protobuf exporters. + """ + if not _is_otlp_enabled() or _resolve_otlp_protocol() != _OTLP_GRPC: + return False + + from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter as GrpcLogExporter, + ) + from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter as GrpcMetricExporter, + ) + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter as GrpcSpanExporter, + ) + from opentelemetry.sdk._logs.export import BatchLogRecordProcessor + from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + span_processors.append(BatchSpanProcessor(GrpcSpanExporter())) + metric_readers.append(PeriodicExportingMetricReader(GrpcMetricExporter())) + log_record_processors.append(BatchLogRecordProcessor(GrpcLogExporter())) + + return True + + +def _is_otlp_enabled() -> bool: + return any( + os.environ.get(env_var) + for env_var in ( + _OTLP_ENDPOINT, + _OTLP_TRACES_ENDPOINT, + _OTLP_METRICS_ENDPOINT, + _OTLP_LOGS_ENDPOINT, + ) + ) + + +def _resolve_otlp_protocol() -> str: + protocol = os.environ.get(_OTLP_PROTOCOL) or _OTLP_HTTP_PROTOBUF + normalized = protocol.strip().lower() + if normalized not in (_OTLP_HTTP_PROTOBUF, _OTLP_GRPC): + raise ValueError( + f"Unsupported OTLP protocol {protocol!r}. Use " + f"{_OTLP_HTTP_PROTOBUF!r} or {_OTLP_GRPC!r}." + ) + return normalized + + +@contextmanager +def _without_otlp_env() -> Any: + saved = { + env_var: os.environ.pop(env_var) + for env_var in _OTLP_ENV_VARS + if env_var in os.environ + } + try: + yield + finally: + os.environ.update(saved) + + # ====================================================================== # Public API: span operations # ====================================================================== diff --git a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt index 285bf0277c06..36537bb785ce 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt +++ b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt @@ -6,5 +6,6 @@ httpx pytest-asyncio opentelemetry-api>=1.40.0 opentelemetry-sdk>=1.40.0 +opentelemetry-exporter-otlp-proto-grpc>=1.40.0 grpcio opentelemetry-proto diff --git a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml index 5e19c7a03b89..4c77ab1e843e 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "hypercorn>=0.17.0", "opentelemetry-api>=1.40.0", "opentelemetry-sdk>=1.40.0", + "opentelemetry-exporter-otlp-proto-grpc>=1.40.0", "microsoft-opentelemetry>=1.0.0", ] From 7b178a225aba06b33f8a456ef0d36347369849a4 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Thu, 16 Jul 2026 20:06:22 +0000 Subject: [PATCH 03/12] Fix span enrichment on newer OpenTelemetry SDKs Apply Foundry enrichment during on_end and update the private attribute workaround for immutable BoundedAttributes so exported spans retain Agent Server metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- .../azure/ai/agentserver/core/_tracing.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 9e340bcc17a0..9bedbe2c4de4 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -602,21 +602,22 @@ def _on_ending(self, span: Any) -> None: if attrs is None: return try: + target = getattr(attrs, "_dict", attrs) if self.agent_name: - attrs[_ATTR_GEN_AI_AGENT_NAME] = self.agent_name + target[_ATTR_GEN_AI_AGENT_NAME] = self.agent_name if self.agent_version: - attrs[_ATTR_GEN_AI_AGENT_VERSION] = self.agent_version + target[_ATTR_GEN_AI_AGENT_VERSION] = self.agent_version if self.agent_id: - attrs[_ATTR_GEN_AI_AGENT_ID] = self.agent_id + target[_ATTR_GEN_AI_AGENT_ID] = self.agent_id if self.agent_blueprint_id: - attrs[_ATTR_GEN_AI_AGENT_BLUEPRINT_ID] = self.agent_blueprint_id + target[_ATTR_GEN_AI_AGENT_BLUEPRINT_ID] = self.agent_blueprint_id if self.agent_tenant_id: - attrs[_ATTR_GEN_AI_AGENT_TENANT_ID] = self.agent_tenant_id + target[_ATTR_GEN_AI_AGENT_TENANT_ID] = self.agent_tenant_id except Exception: # pylint: disable=broad-exception-caught logger.debug("Failed to enrich span attributes in _on_ending", exc_info=True) - def on_end(self, span: Any) -> None: # pylint: disable=unused-argument - pass + def on_end(self, span: Any) -> None: + self._on_ending(span) def shutdown(self) -> None: pass From 1ee106741f1845569182f7677f773f15aa121446 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Thu, 16 Jul 2026 20:32:41 +0000 Subject: [PATCH 04/12] Fix CI for Agent Server OTLP gRPC support Keep span enrichment compatible with newer OpenTelemetry SDKs and tighten the OTLP gRPC exporter dependency to avoid slow resolver installs in apistub. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt | 2 +- sdk/agentserver/azure-ai-agentserver-core/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt index 36537bb785ce..64161a6f0d0e 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt +++ b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt @@ -6,6 +6,6 @@ httpx pytest-asyncio opentelemetry-api>=1.40.0 opentelemetry-sdk>=1.40.0 -opentelemetry-exporter-otlp-proto-grpc>=1.40.0 +opentelemetry-exporter-otlp-proto-grpc~=1.43.0 grpcio opentelemetry-proto diff --git a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml index 4c77ab1e843e..203cc87ff22f 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "hypercorn>=0.17.0", "opentelemetry-api>=1.40.0", "opentelemetry-sdk>=1.40.0", - "opentelemetry-exporter-otlp-proto-grpc>=1.40.0", + "opentelemetry-exporter-otlp-proto-grpc~=1.43.0", "microsoft-opentelemetry>=1.0.0", ] From 2182728061402883693eb0f2bac88f0bb1012889 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 17 Jul 2026 05:26:42 +0000 Subject: [PATCH 05/12] Skip apistub for Agent Server core Disable apistub for azure-ai-agentserver-core because the package has no public API surface changes and the gRPC exporter dependency causes the apistub wheel install to exceed the check timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- sdk/agentserver/azure-ai-agentserver-core/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml index 203cc87ff22f..af9b87956fca 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml @@ -61,5 +61,6 @@ verifytypes = false latestdependency = false pylint = true type_check_samples = false +apistub = false [tool.uv.sources] \ No newline at end of file From 4319b406461c1c93a768ab650af8991b6acf0890 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 17 Jul 2026 05:50:01 +0000 Subject: [PATCH 06/12] Fix Agent Server CI validation Align the package version with the unreleased changelog entry and add Azure SDK docstring metadata for the OTLP gRPC helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- .../azure/ai/agentserver/core/_tracing.py | 9 +++++++++ .../azure/ai/agentserver/core/_version.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 9bedbe2c4de4..ca7325d2781c 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -299,6 +299,15 @@ def _append_grpc_otlp_components( protocol here so customers only need to set OTLP environment variables. Returns True when OTLP environment variables should be hidden from the distro call so it does not also create HTTP/protobuf exporters. + + :param span_processors: Span processors to append gRPC trace export to. + :type span_processors: list[~typing.Any] + :param metric_readers: Metric readers to append gRPC metric export to. + :type metric_readers: list[~typing.Any] + :param log_record_processors: Log record processors to append gRPC log export to. + :type log_record_processors: list[~typing.Any] + :return: Whether OTLP environment variables should be hidden from the distro call. + :rtype: bool """ if not _is_otlp_enabled() or _resolve_otlp_protocol() != _OTLP_GRPC: return False diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_version.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_version.py index 369f0dcc3bea..213b705c79c6 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_version.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_version.py @@ -2,4 +2,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- -VERSION = "2.0.0b7" +VERSION = "2.0.0b8" From fe4e26b50e58f978e079e66f7fd56fdaf83b4fed Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 17 Jul 2026 06:28:13 +0000 Subject: [PATCH 07/12] Fix Agent Server minimum dependency resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt | 2 +- sdk/agentserver/azure-ai-agentserver-core/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt index 64161a6f0d0e..36537bb785ce 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt +++ b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt @@ -6,6 +6,6 @@ httpx pytest-asyncio opentelemetry-api>=1.40.0 opentelemetry-sdk>=1.40.0 -opentelemetry-exporter-otlp-proto-grpc~=1.43.0 +opentelemetry-exporter-otlp-proto-grpc>=1.40.0 grpcio opentelemetry-proto diff --git a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml index af9b87956fca..82da64944a21 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "hypercorn>=0.17.0", "opentelemetry-api>=1.40.0", "opentelemetry-sdk>=1.40.0", - "opentelemetry-exporter-otlp-proto-grpc~=1.43.0", + "opentelemetry-exporter-otlp-proto-grpc>=1.40.0", "microsoft-opentelemetry>=1.0.0", ] From eaac6b203cbde6be34ef6f8aa4999ea58dc6ed35 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 17 Jul 2026 08:19:40 +0000 Subject: [PATCH 08/12] Re-enable Agent Server apistub check Keep apistub enabled for azure-ai-agentserver-core; the dependency constraint fix allows the check to complete. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- sdk/agentserver/azure-ai-agentserver-core/pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml index 82da64944a21..4c77ab1e843e 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml @@ -61,6 +61,5 @@ verifytypes = false latestdependency = false pylint = true type_check_samples = false -apistub = false [tool.uv.sources] \ No newline at end of file From 9f96423a020aeedc1dc6277ff82cf8e7a9969469 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 17 Jul 2026 08:43:10 +0000 Subject: [PATCH 09/12] Align Agent Server OpenTelemetry dependencies Constrain Agent Server OpenTelemetry dependencies to the microsoft-opentelemetry-compatible 1.43 line so apistub can install the wheel without resolver backtracking timeouts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- .../azure-ai-agentserver-core/dev_requirements.txt | 6 +++--- sdk/agentserver/azure-ai-agentserver-core/pyproject.toml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt index 36537bb785ce..bd9ae7538335 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt +++ b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt @@ -4,8 +4,8 @@ pytest httpx pytest-asyncio -opentelemetry-api>=1.40.0 -opentelemetry-sdk>=1.40.0 -opentelemetry-exporter-otlp-proto-grpc>=1.40.0 +opentelemetry-api~=1.43.0 +opentelemetry-sdk~=1.43.0 +opentelemetry-exporter-otlp-proto-grpc~=1.43.0 grpcio opentelemetry-proto diff --git a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml index 4c77ab1e843e..de80a9010de8 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml @@ -23,10 +23,10 @@ keywords = ["azure", "azure sdk", "agent", "agentserver", "core"] dependencies = [ "starlette>=0.45.0", "hypercorn>=0.17.0", - "opentelemetry-api>=1.40.0", - "opentelemetry-sdk>=1.40.0", - "opentelemetry-exporter-otlp-proto-grpc>=1.40.0", - "microsoft-opentelemetry>=1.0.0", + "opentelemetry-api~=1.43.0", + "opentelemetry-sdk~=1.43.0", + "opentelemetry-exporter-otlp-proto-grpc~=1.43.0", + "microsoft-opentelemetry~=1.3.5", ] [build-system] From f51563c71be77a231e987dd45ff158cabf3c5e46 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Fri, 17 Jul 2026 09:06:56 +0000 Subject: [PATCH 10/12] Preinstall Agent Server runtime deps for apistub Add Agent Server runtime dependencies to dev requirements so CI preinstalls them before apistub installs the package wheel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt index bd9ae7538335..0c1b5a8d678a 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt +++ b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt @@ -4,8 +4,11 @@ pytest httpx pytest-asyncio +starlette>=0.45.0 +hypercorn>=0.17.0 opentelemetry-api~=1.43.0 opentelemetry-sdk~=1.43.0 opentelemetry-exporter-otlp-proto-grpc~=1.43.0 +microsoft-opentelemetry~=1.3.5 grpcio opentelemetry-proto From 6c80392a3f5f71992f0931280a05531b2345ad08 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Tue, 21 Jul 2026 09:22:44 +0000 Subject: [PATCH 11/12] Make Agent Server OTLP gRPC optional Move the gRPC OTLP exporter to an optional extra and use >= version constraints for touched OpenTelemetry dependencies. Keep gRPC coverage in dev requirements and warn when grpc protocol is requested without the extra installed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- .../azure-ai-agentserver-core/README.md | 5 +-- .../azure/ai/agentserver/core/_tracing.py | 33 ++++++++++++------- .../dev_requirements.txt | 8 ++--- .../azure-ai-agentserver-core/pyproject.toml | 12 ++++--- .../tests/test_tracing.py | 26 +++++++++++++++ 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/README.md b/sdk/agentserver/azure-ai-agentserver-core/README.md index a994213077c8..4187d7ba9fb3 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/README.md +++ b/sdk/agentserver/azure-ai-agentserver-core/README.md @@ -144,10 +144,11 @@ python my_agent.py ``` OTLP export is enabled when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. HTTP/protobuf -is the default protocol; set `OTEL_EXPORTER_OTLP_PROTOCOL=grpc` to use an -OTLP/gRPC collector: +is the default protocol. To use an OTLP/gRPC collector, install the optional +gRPC extra and set `OTEL_EXPORTER_OTLP_PROTOCOL=grpc`: ```bash +pip install "azure-ai-agentserver-core[otlp-grpc]" export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" export OTEL_EXPORTER_OTLP_PROTOCOL="grpc" python my_agent.py diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index ca7325d2781c..296e9d1d2265 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -79,6 +79,7 @@ _OTLP_HTTP_PROTOBUF = "http/protobuf" _OTLP_GRPC = "grpc" +_OTLP_GRPC_EXTRA = "azure-ai-agentserver-core[otlp-grpc]" _OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" _OTLP_PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL" _OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" @@ -312,18 +313,26 @@ def _append_grpc_otlp_components( if not _is_otlp_enabled() or _resolve_otlp_protocol() != _OTLP_GRPC: return False - from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( - OTLPLogExporter as GrpcLogExporter, - ) - from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( - OTLPMetricExporter as GrpcMetricExporter, - ) - from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( - OTLPSpanExporter as GrpcSpanExporter, - ) - from opentelemetry.sdk._logs.export import BatchLogRecordProcessor - from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader - from opentelemetry.sdk.trace.export import BatchSpanProcessor + try: + from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter as GrpcLogExporter, + ) + from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter as GrpcMetricExporter, + ) + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter as GrpcSpanExporter, + ) + from opentelemetry.sdk._logs.export import BatchLogRecordProcessor + from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader + from opentelemetry.sdk.trace.export import BatchSpanProcessor + except ImportError: + logger.warning( + "OTLP/gRPC export was requested, but the optional gRPC exporter " + "dependencies are not installed. Install %s to enable OTLP/gRPC export.", + _OTLP_GRPC_EXTRA, + ) + return True span_processors.append(BatchSpanProcessor(GrpcSpanExporter())) metric_readers.append(PeriodicExportingMetricReader(GrpcMetricExporter())) diff --git a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt index 0c1b5a8d678a..121c952e40a7 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt +++ b/sdk/agentserver/azure-ai-agentserver-core/dev_requirements.txt @@ -6,9 +6,9 @@ httpx pytest-asyncio starlette>=0.45.0 hypercorn>=0.17.0 -opentelemetry-api~=1.43.0 -opentelemetry-sdk~=1.43.0 -opentelemetry-exporter-otlp-proto-grpc~=1.43.0 -microsoft-opentelemetry~=1.3.5 +opentelemetry-api>=1.40.0 +opentelemetry-sdk>=1.40.0 +microsoft-opentelemetry>=1.0.0 +opentelemetry-exporter-otlp-proto-grpc>=1.40.0 grpcio opentelemetry-proto diff --git a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml index de80a9010de8..dfdf3210261e 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml @@ -23,10 +23,14 @@ keywords = ["azure", "azure sdk", "agent", "agentserver", "core"] dependencies = [ "starlette>=0.45.0", "hypercorn>=0.17.0", - "opentelemetry-api~=1.43.0", - "opentelemetry-sdk~=1.43.0", - "opentelemetry-exporter-otlp-proto-grpc~=1.43.0", - "microsoft-opentelemetry~=1.3.5", + "opentelemetry-api>=1.40.0", + "opentelemetry-sdk>=1.40.0", + "microsoft-opentelemetry>=1.0.0", +] + +[project.optional-dependencies] +otlp-grpc = [ + "opentelemetry-exporter-otlp-proto-grpc>=1.40.0", ] [build-system] diff --git a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py index bacf0f4d6dea..80ad1d3ba666 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py @@ -147,6 +147,32 @@ def test_distro_called_without_conn_str(self) -> None: kwargs = mock_distro.call_args[1] assert kwargs["connection_string"] is None + def test_grpc_protocol_without_optional_extra_logs_warning(self, caplog) -> None: + from azure.ai.agentserver.core import _tracing + + original_import = __import__ + + def import_without_grpc_exporter(name, *args, **kwargs): + if name.startswith("opentelemetry.exporter.otlp.proto.grpc"): + raise ImportError(name) + return original_import(name, *args, **kwargs) + + with ( + mock.patch.dict( + os.environ, + { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", + }, + ), + mock.patch("builtins.__import__", side_effect=import_without_grpc_exporter), + caplog.at_level("WARNING", logger="azure.ai.agentserver"), + ): + suppress_distro_otlp = _tracing._append_grpc_otlp_components([], [], []) + + assert suppress_distro_otlp is True + assert "azure-ai-agentserver-core[otlp-grpc]" in caplog.text + # ------------------------------------------------------------------ # # Constructor passes / skips connection string From 83d01695e903f6f66715eb64323a7633f706c827 Mon Sep 17 00:00:00 2001 From: Shivakishore14 Date: Tue, 21 Jul 2026 10:38:12 +0000 Subject: [PATCH 12/12] Handle signal-specific OTLP gRPC export Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c368201-b3c0-4a22-8311-2c288cbd374c --- .../azure/ai/agentserver/core/_tracing.py | 111 ++++++++++++++---- .../tests/test_otlp_protocol.py | 3 + .../tests/test_tracing.py | 72 +++++++++++- 3 files changed, 160 insertions(+), 26 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 296e9d1d2265..0a9d61938a18 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -83,14 +83,20 @@ _OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" _OTLP_PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL" _OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" +_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" _OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" +_OTLP_METRICS_PROTOCOL = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL" _OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT" +_OTLP_LOGS_PROTOCOL = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL" _OTLP_ENV_VARS = ( _OTLP_ENDPOINT, _OTLP_PROTOCOL, _OTLP_TRACES_ENDPOINT, + _OTLP_TRACES_PROTOCOL, _OTLP_METRICS_ENDPOINT, + _OTLP_METRICS_PROTOCOL, _OTLP_LOGS_ENDPOINT, + _OTLP_LOGS_PROTOCOL, ) @@ -213,14 +219,14 @@ def _configure_tracing( ), ] metric_readers: list[Any] = [] - suppress_distro_otlp = _append_grpc_otlp_components( + suppress_distro_otlp = _append_managed_otlp_components( resolved_span_processors, metric_readers, log_record_processors, ) try: - context = _without_otlp_env() if suppress_distro_otlp else nullcontext() + context = _suppress_distro_otlp_components() if suppress_distro_otlp else nullcontext() with context: _setup_distro_export( resource=resource, @@ -288,31 +294,41 @@ def _setup_distro_export( use_microsoft_opentelemetry(**kwargs) -def _append_grpc_otlp_components( +def _append_managed_otlp_components( span_processors: list[Any], metric_readers: list[Any], log_record_processors: list[Any], ) -> bool: - """Append SDK-managed OTLP/gRPC exporters when requested by env vars. + """Append SDK-managed OTLP exporters when any signal requests gRPC. The Microsoft OpenTelemetry distro currently owns the normal OTLP path but only bundles the HTTP/protobuf exporter. Agent Server handles the gRPC - protocol here so customers only need to set OTLP environment variables. - Returns True when OTLP environment variables should be hidden from the - distro call so it does not also create HTTP/protobuf exporters. + protocol here so customers only need to set OTLP environment variables. If + any signal uses gRPC, Agent Server also creates HTTP/protobuf exporters for + non-gRPC OTLP signals so mixed signal-specific protocol settings work. + Returns True when the distro OTLP appender should be suppressed so it does + not also create HTTP/protobuf exporters. - :param span_processors: Span processors to append gRPC trace export to. + :param span_processors: Span processors to append trace export to. :type span_processors: list[~typing.Any] - :param metric_readers: Metric readers to append gRPC metric export to. + :param metric_readers: Metric readers to append metric export to. :type metric_readers: list[~typing.Any] - :param log_record_processors: Log record processors to append gRPC log export to. + :param log_record_processors: Log record processors to append log export to. :type log_record_processors: list[~typing.Any] - :return: Whether OTLP environment variables should be hidden from the distro call. + :return: Whether distro OTLP exporters should be suppressed. :rtype: bool """ - if not _is_otlp_enabled() or _resolve_otlp_protocol() != _OTLP_GRPC: + if not _is_otlp_enabled(): + return False + + trace_protocol = _resolve_otlp_protocol(_OTLP_TRACES_PROTOCOL) + metric_protocol = _resolve_otlp_protocol(_OTLP_METRICS_PROTOCOL) + log_protocol = _resolve_otlp_protocol(_OTLP_LOGS_PROTOCOL) + protocols = (trace_protocol, metric_protocol, log_protocol) + if _OTLP_GRPC not in protocols: return False + grpc_exporters: Optional[tuple[Any, Any, Any]] = None try: from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter as GrpcLogExporter, @@ -326,21 +342,61 @@ def _append_grpc_otlp_components( from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.trace.export import BatchSpanProcessor + + grpc_exporters = (GrpcSpanExporter, GrpcMetricExporter, GrpcLogExporter) except ImportError: logger.warning( "OTLP/gRPC export was requested, but the optional gRPC exporter " "dependencies are not installed. Install %s to enable OTLP/gRPC export.", _OTLP_GRPC_EXTRA, ) - return True - span_processors.append(BatchSpanProcessor(GrpcSpanExporter())) - metric_readers.append(PeriodicExportingMetricReader(GrpcMetricExporter())) - log_record_processors.append(BatchLogRecordProcessor(GrpcLogExporter())) + from opentelemetry.exporter.otlp.proto.http._log_exporter import ( + OTLPLogExporter as HttpLogExporter, + ) + from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( + OTLPMetricExporter as HttpMetricExporter, + ) + from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter as HttpSpanExporter, + ) + from opentelemetry.sdk._logs.export import BatchLogRecordProcessor + from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + if trace_protocol == _OTLP_GRPC: + if grpc_exporters: + span_processors.append(BatchSpanProcessor(grpc_exporters[0]())) + else: + span_processors.append(BatchSpanProcessor(HttpSpanExporter())) + + if metric_protocol == _OTLP_GRPC: + if grpc_exporters: + metric_readers.append(PeriodicExportingMetricReader(grpc_exporters[1]())) + else: + metric_readers.append(PeriodicExportingMetricReader(HttpMetricExporter())) + + if log_protocol == _OTLP_GRPC: + if grpc_exporters: + log_record_processors.append(BatchLogRecordProcessor(grpc_exporters[2]())) + else: + log_record_processors.append(BatchLogRecordProcessor(HttpLogExporter())) return True +def _append_grpc_otlp_components( + span_processors: list[Any], + metric_readers: list[Any], + log_record_processors: list[Any], +) -> bool: + return _append_managed_otlp_components( + span_processors, + metric_readers, + log_record_processors, + ) + + def _is_otlp_enabled() -> bool: return any( os.environ.get(env_var) @@ -353,8 +409,9 @@ def _is_otlp_enabled() -> bool: ) -def _resolve_otlp_protocol() -> str: - protocol = os.environ.get(_OTLP_PROTOCOL) or _OTLP_HTTP_PROTOBUF +def _resolve_otlp_protocol(signal_protocol_env: Optional[str] = None) -> str: + protocol = os.environ.get(signal_protocol_env) if signal_protocol_env else None + protocol = protocol or os.environ.get(_OTLP_PROTOCOL) or _OTLP_HTTP_PROTOBUF normalized = protocol.strip().lower() if normalized not in (_OTLP_HTTP_PROTOBUF, _OTLP_GRPC): raise ValueError( @@ -365,16 +422,20 @@ def _resolve_otlp_protocol() -> str: @contextmanager -def _without_otlp_env() -> Any: - saved = { - env_var: os.environ.pop(env_var) - for env_var in _OTLP_ENV_VARS - if env_var in os.environ - } +def _suppress_distro_otlp_components() -> Any: + import microsoft.opentelemetry as microsoft_opentelemetry + + distro_globals = microsoft_opentelemetry.use_microsoft_opentelemetry.__globals__ + original_append_otlp_components = distro_globals["_append_otlp_components"] + + def _skip_otlp_components(_otel_kwargs: dict[str, Any]) -> None: + return None + + distro_globals["_append_otlp_components"] = _skip_otlp_components try: yield finally: - os.environ.update(saved) + distro_globals["_append_otlp_components"] = original_append_otlp_components # ====================================================================== diff --git a/sdk/agentserver/azure-ai-agentserver-core/tests/test_otlp_protocol.py b/sdk/agentserver/azure-ai-agentserver-core/tests/test_otlp_protocol.py index 0601fdaa9aa4..6ec54614ae68 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/tests/test_otlp_protocol.py +++ b/sdk/agentserver/azure-ai-agentserver-core/tests/test_otlp_protocol.py @@ -245,3 +245,6 @@ def test_otlp_protocol_exports_all_signals( f"{protocol} receiver got {sorted(receiver.signals)} instead of " f"{sorted(_SIGNALS)}.\nsubprocess stderr:\n{result.stderr}" ) + if protocol == "grpc": + assert "otlp-test-span" not in result.stdout + assert "otlp-test-log" not in result.stdout diff --git a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py index 80ad1d3ba666..2cdb29772ef0 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py @@ -168,11 +168,81 @@ def import_without_grpc_exporter(name, *args, **kwargs): mock.patch("builtins.__import__", side_effect=import_without_grpc_exporter), caplog.at_level("WARNING", logger="azure.ai.agentserver"), ): - suppress_distro_otlp = _tracing._append_grpc_otlp_components([], [], []) + suppress_distro_otlp = _tracing._append_managed_otlp_components([], [], []) assert suppress_distro_otlp is True assert "azure-ai-agentserver-core[otlp-grpc]" in caplog.text + def test_signal_specific_otlp_protocol_overrides_global(self) -> None: + from azure.ai.agentserver.core import _tracing + + with mock.patch.dict( + os.environ, + { + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "grpc", + }, + ): + assert _tracing._resolve_otlp_protocol("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL") == "grpc" + assert _tracing._resolve_otlp_protocol("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL") == "http/protobuf" + + def test_managed_otlp_handles_mixed_signal_protocols(self) -> None: + from azure.ai.agentserver.core import _tracing + + with mock.patch.dict( + os.environ, + { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "grpc", + }, + ): + span_processors = [] + metric_readers = [] + log_record_processors = [] + suppress_distro_otlp = _tracing._append_managed_otlp_components( + span_processors, + metric_readers, + log_record_processors, + ) + + try: + assert suppress_distro_otlp is True + assert len(span_processors) == 1 + assert len(metric_readers) == 1 + assert len(log_record_processors) == 1 + assert span_processors[0].span_exporter.__module__.startswith( + "opentelemetry.exporter.otlp.proto.grpc" + ) + assert metric_readers[0]._exporter.__module__.startswith( + "opentelemetry.exporter.otlp.proto.http" + ) + assert log_record_processors[0]._batch_processor._exporter.__module__.startswith( + "opentelemetry.exporter.otlp.proto.http" + ) + finally: + for processor in span_processors + log_record_processors: + processor.shutdown() + for reader in metric_readers: + reader.shutdown() + + def test_suppressing_distro_otlp_leaves_env_visible(self) -> None: + from azure.ai.agentserver.core import _tracing + + with mock.patch.dict( + os.environ, + { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", + }, + ): + with _tracing._suppress_distro_otlp_components(): + import microsoft.opentelemetry as microsoft_opentelemetry + + distro_globals = microsoft_opentelemetry.use_microsoft_opentelemetry.__globals__ + assert distro_globals["is_otlp_enabled"]() + assert os.environ["OTEL_EXPORTER_OTLP_PROTOCOL"] == "grpc" + # ------------------------------------------------------------------ # # Constructor passes / skips connection string