Skip to content

Commit c858b8b

Browse files
CopilotnikhilNava
andcommitted
Add BaggageMiddleware, OutputLoggingMiddleware, and ObservabilityHostingManager
Implement Python equivalents of the Node.js PR #210 middleware: - BaggageMiddleware: propagates OpenTelemetry baggage from TurnContext - OutputLoggingMiddleware: creates OutputScope spans for outgoing messages - ObservabilityHostingManager: singleton to configure hosting middleware - 19 unit tests covering all three middleware components Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com>
1 parent 4a36531 commit c858b8b

File tree

9 files changed

+850
-0
lines changed

9 files changed

+850
-0
lines changed

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,47 @@
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+
import logging
9+
from collections.abc import Awaitable, Callable
10+
11+
from microsoft_agents.activity import ActivityEventNames, ActivityTypes
12+
from microsoft_agents.hosting.core.turn_context import TurnContext
13+
from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder
14+
15+
from ..scope_helpers.populate_baggage import populate
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class BaggageMiddleware:
21+
"""Middleware that propagates OpenTelemetry baggage context derived from TurnContext.
22+
23+
Async replies (ContinueConversation) are passed through without baggage setup.
24+
"""
25+
26+
async def on_turn(
27+
self,
28+
context: TurnContext,
29+
logic: Callable[[TurnContext], Awaitable],
30+
) -> None:
31+
activity = context.activity
32+
is_async_reply = (
33+
activity is not None
34+
and activity.type == ActivityTypes.event
35+
and activity.name == ActivityEventNames.continue_conversation
36+
)
37+
38+
if is_async_reply:
39+
await logic()
40+
return
41+
42+
builder = BaggageBuilder()
43+
populate(builder, context)
44+
baggage_scope = builder.build()
45+
46+
with baggage_scope:
47+
await logic()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
from typing import Protocol
11+
12+
from microsoft_agents.hosting.core import Middleware
13+
14+
from .baggage_middleware import BaggageMiddleware
15+
from .output_logging_middleware import OutputLoggingMiddleware
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class _AdapterLike(Protocol):
21+
"""Protocol for adapter objects that support middleware registration."""
22+
23+
def use(self, middleware: Middleware) -> object: ...
24+
25+
26+
@dataclass
27+
class ObservabilityHostingOptions:
28+
"""Configuration options for the hosting observability layer."""
29+
30+
enable_baggage: bool = True
31+
"""Enable baggage propagation middleware. Defaults to ``True``."""
32+
33+
enable_output_logging: bool = False
34+
"""Enable output logging middleware for tracing outgoing messages. Defaults to ``False``."""
35+
36+
37+
class ObservabilityHostingManager:
38+
"""Singleton manager for configuring hosting-layer observability middleware.
39+
40+
Example:
41+
.. code-block:: python
42+
43+
ObservabilityHostingManager.configure(adapter, ObservabilityHostingOptions(
44+
enable_output_logging=True,
45+
))
46+
"""
47+
48+
_instance: ObservabilityHostingManager | None = None
49+
50+
def __init__(self) -> None:
51+
"""Private constructor — use :meth:`configure` instead."""
52+
53+
@classmethod
54+
def configure(
55+
cls,
56+
adapter: _AdapterLike | None = None,
57+
options: ObservabilityHostingOptions | None = None,
58+
) -> ObservabilityHostingManager:
59+
"""Configure the singleton instance and register middleware on the adapter.
60+
61+
Subsequent calls after the first are no-ops and return the existing instance.
62+
63+
Args:
64+
adapter: An adapter that supports ``.use()`` for middleware registration.
65+
options: Configuration options. Defaults are used when ``None``.
66+
67+
Returns:
68+
The singleton :class:`ObservabilityHostingManager` instance.
69+
"""
70+
if cls._instance is not None:
71+
logger.warning(
72+
"[ObservabilityHostingManager] Already configured. "
73+
"Subsequent configure() calls are ignored."
74+
)
75+
return cls._instance
76+
77+
instance = cls()
78+
79+
if adapter is not None:
80+
opts = options or ObservabilityHostingOptions()
81+
82+
if opts.enable_baggage:
83+
adapter.use(BaggageMiddleware())
84+
logger.info("[ObservabilityHostingManager] BaggageMiddleware registered.")
85+
86+
if opts.enable_output_logging:
87+
adapter.use(OutputLoggingMiddleware())
88+
logger.info("[ObservabilityHostingManager] OutputLoggingMiddleware registered.")
89+
90+
logger.info(
91+
"[ObservabilityHostingManager] Configured. Baggage: %s, OutputLogging: %s.",
92+
opts.enable_baggage,
93+
opts.enable_output_logging,
94+
)
95+
else:
96+
logger.warning(
97+
"[ObservabilityHostingManager] No adapter provided. No middleware registered."
98+
)
99+
100+
cls._instance = instance
101+
return instance
102+
103+
@classmethod
104+
def reset(cls) -> None:
105+
"""Reset the singleton instance. Intended for testing only."""
106+
cls._instance = None

0 commit comments

Comments
 (0)