Skip to content

Commit 486b85e

Browse files
Merge branch 'main' into dependabot/github_actions/github/codeql-action-4.32.5
2 parents eec4b0c + fd22e97 commit 486b85e

13 files changed

Lines changed: 835 additions & 5 deletions

File tree

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4+
import os
45
import logging
56
import threading
67
from collections.abc import Callable
78
from typing import Any, Optional
89

910
from opentelemetry import trace
11+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
1012
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_NAMESPACE, Resource
1113
from opentelemetry.sdk.trace import TracerProvider
1214
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
@@ -161,10 +163,11 @@ def _configure_internal(
161163
use_s2s_endpoint=exporter_options.use_s2s_endpoint,
162164
suppress_invoke_agent_input=suppress_invoke_agent_input,
163165
)
166+
164167
else:
165168
exporter = ConsoleSpanExporter()
166169
self._logger.warning(
167-
"is_agent365_exporter_enabled() not enabled or token_resolver not set.Falling back to console exporter."
170+
"is_agent365_exporter_enabled() not enabled or token_resolver not set. Falling back to console exporter."
168171
)
169172

170173
# Add span processors
@@ -181,6 +184,13 @@ def _configure_internal(
181184
self._span_processors["batch"] = batch_processor
182185
self._span_processors["agent"] = agent_processor
183186

187+
if os.environ.get("ENABLE_OTLP_EXPORTER", "").lower() == "true":
188+
# The OTLPSpanExporter is auto configured from the environment variables
189+
otlp_exporter = OTLPSpanExporter()
190+
tracer_provider.add_span_processor(
191+
_EnrichingBatchSpanProcessor(otlp_exporter, **batch_processor_kwargs)
192+
)
193+
184194
# Configure logging if logger_name is provided
185195
if logger_name:
186196
target_logger = logging.getLogger(logger_name)

libraries/microsoft-agents-a365-observability-hosting/microsoft_agents_a365/observability/hosting/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,18 @@
44
"""
55
Microsoft Agent 365 Observability Hosting Library.
66
"""
7+
8+
from .middleware.baggage_middleware import BaggageMiddleware
9+
from .middleware.observability_hosting_manager import (
10+
ObservabilityHostingManager,
11+
ObservabilityHostingOptions,
12+
)
13+
from .middleware.output_logging_middleware import A365_PARENT_SPAN_KEY, OutputLoggingMiddleware
14+
15+
__all__ = [
16+
"BaggageMiddleware",
17+
"OutputLoggingMiddleware",
18+
"A365_PARENT_SPAN_KEY",
19+
"ObservabilityHostingManager",
20+
"ObservabilityHostingOptions",
21+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
from .baggage_middleware import BaggageMiddleware
5+
from .observability_hosting_manager import ObservabilityHostingManager, ObservabilityHostingOptions
6+
from .output_logging_middleware import A365_PARENT_SPAN_KEY, OutputLoggingMiddleware
7+
8+
__all__ = [
9+
"BaggageMiddleware",
10+
"OutputLoggingMiddleware",
11+
"A365_PARENT_SPAN_KEY",
12+
"ObservabilityHostingManager",
13+
"ObservabilityHostingOptions",
14+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Middleware that propagates OpenTelemetry baggage context derived from TurnContext."""
5+
6+
from __future__ import annotations
7+
8+
from collections.abc import Awaitable, Callable
9+
10+
from microsoft_agents.activity import ActivityEventNames, ActivityTypes
11+
from microsoft_agents.hosting.core.turn_context import TurnContext
12+
from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder
13+
14+
from ..scope_helpers.populate_baggage import populate
15+
16+
17+
class BaggageMiddleware:
18+
"""Middleware that propagates OpenTelemetry baggage context derived from TurnContext.
19+
20+
Async replies (ContinueConversation) are passed through without baggage setup.
21+
"""
22+
23+
async def on_turn(
24+
self,
25+
context: TurnContext,
26+
logic: Callable[[TurnContext], Awaitable],
27+
) -> None:
28+
activity = context.activity
29+
is_async_reply = (
30+
activity is not None
31+
and activity.type == ActivityTypes.event
32+
and activity.name == ActivityEventNames.continue_conversation
33+
)
34+
35+
if is_async_reply:
36+
await logic()
37+
return
38+
39+
builder = BaggageBuilder()
40+
populate(builder, context)
41+
baggage_scope = builder.build()
42+
43+
with baggage_scope:
44+
await logic()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Singleton manager for configuring hosting-layer observability middleware."""
5+
6+
from __future__ import annotations
7+
8+
import logging
9+
from dataclasses import dataclass
10+
11+
from microsoft_agents.hosting.core.middleware_set import MiddlewareSet
12+
13+
from .baggage_middleware import BaggageMiddleware
14+
from .output_logging_middleware import OutputLoggingMiddleware
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
@dataclass
20+
class ObservabilityHostingOptions:
21+
"""Configuration options for the hosting observability layer."""
22+
23+
enable_baggage: bool = False
24+
"""Enable baggage propagation middleware. Defaults to ``False``."""
25+
26+
enable_output_logging: bool = False
27+
"""Enable output logging middleware for tracing outgoing messages. Defaults to ``False``."""
28+
29+
30+
class ObservabilityHostingManager:
31+
"""Singleton manager for configuring hosting-layer observability middleware.
32+
33+
Example:
34+
.. code-block:: python
35+
36+
ObservabilityHostingManager.configure(adapter.middleware_set, ObservabilityHostingOptions(
37+
enable_output_logging=True,
38+
))
39+
"""
40+
41+
_instance: ObservabilityHostingManager | None = None
42+
43+
def __init__(self) -> None:
44+
"""Private constructor — use :meth:`configure` instead."""
45+
46+
@classmethod
47+
def configure(
48+
cls,
49+
middleware_set: MiddlewareSet,
50+
options: ObservabilityHostingOptions,
51+
) -> ObservabilityHostingManager:
52+
"""Configure the singleton instance and register middleware.
53+
54+
Subsequent calls after the first are no-ops and return the existing instance.
55+
56+
Args:
57+
middleware_set: The middleware set to register middleware on
58+
(e.g., ``adapter.middleware_set``).
59+
options: Configuration options controlling which middleware to enable.
60+
61+
Returns:
62+
The singleton :class:`ObservabilityHostingManager` instance.
63+
64+
Raises:
65+
TypeError: If *middleware_set* or *options* is ``None``.
66+
"""
67+
if middleware_set is None:
68+
raise TypeError("middleware_set must not be None")
69+
if options is None:
70+
raise TypeError("options must not be None")
71+
72+
if cls._instance is not None:
73+
logger.warning(
74+
"[ObservabilityHostingManager] Already configured. "
75+
"Subsequent configure() calls are ignored."
76+
)
77+
return cls._instance
78+
79+
instance = cls()
80+
81+
if options.enable_baggage:
82+
middleware_set.use(BaggageMiddleware())
83+
logger.info("[ObservabilityHostingManager] BaggageMiddleware registered.")
84+
85+
if options.enable_output_logging:
86+
middleware_set.use(OutputLoggingMiddleware())
87+
logger.info("[ObservabilityHostingManager] OutputLoggingMiddleware registered.")
88+
89+
logger.info(
90+
"[ObservabilityHostingManager] Configured. Baggage: %s, OutputLogging: %s.",
91+
options.enable_baggage,
92+
options.enable_output_logging,
93+
)
94+
95+
cls._instance = instance
96+
return instance
97+
98+
@classmethod
99+
def reset(cls) -> None:
100+
"""Reset the singleton instance. Intended for testing only."""
101+
cls._instance = None

0 commit comments

Comments
 (0)