Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,18 @@
"""
Microsoft Agent 365 Observability Hosting Library.
"""

from .middleware.baggage_middleware import BaggageMiddleware
from .middleware.observability_hosting_manager import (
ObservabilityHostingManager,
ObservabilityHostingOptions,
)
from .middleware.output_logging_middleware import A365_PARENT_SPAN_KEY, OutputLoggingMiddleware

__all__ = [
"BaggageMiddleware",
"OutputLoggingMiddleware",
"A365_PARENT_SPAN_KEY",
"ObservabilityHostingManager",
"ObservabilityHostingOptions",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from .baggage_middleware import BaggageMiddleware
from .observability_hosting_manager import ObservabilityHostingManager, ObservabilityHostingOptions
from .output_logging_middleware import A365_PARENT_SPAN_KEY, OutputLoggingMiddleware

__all__ = [
"BaggageMiddleware",
"OutputLoggingMiddleware",
"A365_PARENT_SPAN_KEY",
"ObservabilityHostingManager",
"ObservabilityHostingOptions",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Middleware that propagates OpenTelemetry baggage context derived from TurnContext."""

from __future__ import annotations

from collections.abc import Awaitable, Callable

from microsoft_agents.activity import ActivityEventNames, ActivityTypes
from microsoft_agents.hosting.core.turn_context import TurnContext
from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder

from ..scope_helpers.populate_baggage import populate


class BaggageMiddleware:
"""Middleware that propagates OpenTelemetry baggage context derived from TurnContext.

Async replies (ContinueConversation) are passed through without baggage setup.
"""

async def on_turn(
self,
context: TurnContext,
logic: Callable[[TurnContext], Awaitable],
) -> None:
activity = context.activity
is_async_reply = (
activity is not None
and activity.type == ActivityTypes.event
and activity.name == ActivityEventNames.continue_conversation
)

if is_async_reply:
await logic()
return

builder = BaggageBuilder()
populate(builder, context)
baggage_scope = builder.build()

with baggage_scope:
await logic()
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Singleton manager for configuring hosting-layer observability middleware."""

from __future__ import annotations

import logging
from dataclasses import dataclass

from microsoft_agents.hosting.core.middleware_set import MiddlewareSet

from .baggage_middleware import BaggageMiddleware
from .output_logging_middleware import OutputLoggingMiddleware

logger = logging.getLogger(__name__)


@dataclass
class ObservabilityHostingOptions:
"""Configuration options for the hosting observability layer."""

enable_baggage: bool = False
"""Enable baggage propagation middleware. Defaults to ``False``."""

enable_output_logging: bool = False
"""Enable output logging middleware for tracing outgoing messages. Defaults to ``False``."""


class ObservabilityHostingManager:
"""Singleton manager for configuring hosting-layer observability middleware.

Example:
.. code-block:: python

ObservabilityHostingManager.configure(adapter.middleware_set, ObservabilityHostingOptions(
enable_output_logging=True,
))
"""

_instance: ObservabilityHostingManager | None = None

def __init__(self) -> None:
"""Private constructor — use :meth:`configure` instead."""

@classmethod
def configure(
cls,
middleware_set: MiddlewareSet,
options: ObservabilityHostingOptions,
) -> ObservabilityHostingManager:
"""Configure the singleton instance and register middleware.

Subsequent calls after the first are no-ops and return the existing instance.

Args:
middleware_set: The middleware set to register middleware on
(e.g., ``adapter.middleware_set``).
options: Configuration options controlling which middleware to enable.

Returns:
The singleton :class:`ObservabilityHostingManager` instance.

Raises:
TypeError: If *middleware_set* or *options* is ``None``.
"""
if middleware_set is None:
raise TypeError("middleware_set must not be None")
if options is None:
raise TypeError("options must not be None")

if cls._instance is not None:
logger.warning(
"[ObservabilityHostingManager] Already configured. "
"Subsequent configure() calls are ignored."
)
return cls._instance

instance = cls()

if options.enable_baggage:
middleware_set.use(BaggageMiddleware())
logger.info("[ObservabilityHostingManager] BaggageMiddleware registered.")

if options.enable_output_logging:
middleware_set.use(OutputLoggingMiddleware())
logger.info("[ObservabilityHostingManager] OutputLoggingMiddleware registered.")

logger.info(
"[ObservabilityHostingManager] Configured. Baggage: %s, OutputLogging: %s.",
options.enable_baggage,
options.enable_output_logging,
)

cls._instance = instance
return instance

@classmethod
def reset(cls) -> None:
"""Reset the singleton instance. Intended for testing only."""
cls._instance = None
Loading