Skip to content

Commit 4916c2e

Browse files
nikhilNavanikhilc-microsoftCopilotCopilot
authored
Add exporter options and other functionality (#63)
* updates to baggage and scope * add tests for new functionality * fix tests * Update libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_scope.py Co-authored-by: Copilot <[email protected]> * Update libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/baggage_builder.py Co-authored-by: Copilot <[email protected]> * Update libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter_options.py Co-authored-by: Copilot <[email protected]> * Update libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter_options.py Co-authored-by: Copilot <[email protected]> * Update libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py Co-authored-by: Copilot <[email protected]> * Update libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py Co-authored-by: Copilot <[email protected]> * Update libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/baggage_builder.py Co-authored-by: Copilot <[email protected]> * Remove redundant null check in conversation ID extraction (#64) * Initial plan * Remove redundant condition in _iter_conversation_pairs Co-authored-by: nikhilNava <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: nikhilNava <[email protected]> * Fix copyright header in utils.py to use standard Microsoft format (#65) * Initial plan * Fix copyright header in utils.py to use standard Microsoft format Co-authored-by: nikhilNava <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: nikhilNava <[email protected]> * Update libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/baggage_builder.py Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Nikhil Chitlur Navakiran (from Dev Box) <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: nikhilNava <[email protected]>
1 parent 870905d commit 4916c2e

File tree

17 files changed

+539
-302
lines changed

17 files changed

+539
-302
lines changed

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

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
1212

1313
from .exporters.agent365_exporter import Agent365Exporter
14+
from .exporters.agent365_exporter_options import Agent365ExporterOptions
1415
from .exporters.utils import is_agent365_exporter_enabled
1516
from .trace_processor.span_processor import SpanProcessor
1617

@@ -51,6 +52,7 @@ def configure(
5152
logger_name: str = DEFAULT_LOGGER_NAME,
5253
token_resolver: Callable[[str, str], str | None] | None = None,
5354
cluster_category: str = "prod",
55+
exporter_options: Optional[Agent365ExporterOptions] = None,
5456
**kwargs: Any,
5557
) -> bool:
5658
"""
@@ -59,8 +61,12 @@ def configure(
5961
:param service_name: The name of the service.
6062
:param service_namespace: The namespace of the service.
6163
:param logger_name: The name of the logger to collect telemetry from.
62-
:param token_resolver: Callable that returns an auth token for a given agent + tenant.
63-
:param cluster_category: Environment / cluster category (e.g., "preprod", "prod").
64+
:param token_resolver: (Deprecated) Callable that returns an auth token for a given agent + tenant.
65+
Use exporter_options instead.
66+
:param cluster_category: (Deprecated) Environment / cluster category (e.g. "prod").
67+
Use exporter_options instead.
68+
:param exporter_options: Agent365ExporterOptions instance for configuring the exporter.
69+
If provided, exporter_options takes precedence. If exporter_options is None, the token_resolver and cluster_category parameters are used as fallback/legacy support to construct a default Agent365ExporterOptions instance.
6470
:return: True if configuration succeeded, False otherwise.
6571
"""
6672
try:
@@ -71,6 +77,7 @@ def configure(
7177
logger_name,
7278
token_resolver,
7379
cluster_category,
80+
exporter_options,
7481
**kwargs,
7582
)
7683
except Exception as e:
@@ -84,6 +91,7 @@ def _configure_internal(
8491
logger_name: str,
8592
token_resolver: Callable[[str, str], str | None] | None = None,
8693
cluster_category: str = "prod",
94+
exporter_options: Optional[Agent365ExporterOptions] = None,
8795
**kwargs: Any,
8896
) -> bool:
8997
"""Internal configuration method - not thread-safe, must be called with lock."""
@@ -115,11 +123,26 @@ def _configure_internal(
115123
trace.set_tracer_provider(tracer_provider)
116124
self._tracer_provider = tracer_provider
117125

118-
if is_agent365_exporter_enabled() and token_resolver is not None:
119-
exporter = Agent365Exporter(
120-
token_resolver=token_resolver,
126+
# Use exporter_options if provided, otherwise create default options with legacy parameters
127+
if exporter_options is None:
128+
exporter_options = Agent365ExporterOptions(
121129
cluster_category=cluster_category,
122-
**kwargs,
130+
token_resolver=token_resolver,
131+
)
132+
133+
# Extract configuration for BatchSpanProcessor
134+
batch_processor_kwargs = {
135+
"max_queue_size": exporter_options.max_queue_size,
136+
"schedule_delay_millis": exporter_options.scheduled_delay_ms,
137+
"export_timeout_millis": exporter_options.exporter_timeout_ms,
138+
"max_export_batch_size": exporter_options.max_export_batch_size,
139+
}
140+
141+
if is_agent365_exporter_enabled() and exporter_options.token_resolver is not None:
142+
exporter = Agent365Exporter(
143+
token_resolver=exporter_options.token_resolver,
144+
cluster_category=exporter_options.cluster_category,
145+
use_s2s_endpoint=exporter_options.use_s2s_endpoint,
123146
)
124147
else:
125148
exporter = ConsoleSpanExporter()
@@ -130,7 +153,7 @@ def _configure_internal(
130153
# Add span processors
131154

132155
# Create BatchSpanProcessor with optimized settings
133-
batch_processor = BatchSpanProcessor(exporter)
156+
batch_processor = BatchSpanProcessor(exporter, **batch_processor_kwargs)
134157
agent_processor = SpanProcessor()
135158

136159
tracer_provider.add_span_processor(batch_processor)
@@ -197,6 +220,7 @@ def configure(
197220
logger_name: str = DEFAULT_LOGGER_NAME,
198221
token_resolver: Callable[[str, str], str | None] | None = None,
199222
cluster_category: str = "prod",
223+
exporter_options: Optional[Agent365ExporterOptions] = None,
200224
**kwargs: Any,
201225
) -> bool:
202226
"""
@@ -205,6 +229,12 @@ def configure(
205229
:param service_name: The name of the service.
206230
:param service_namespace: The namespace of the service.
207231
:param logger_name: The name of the logger to collect telemetry from.
232+
:param token_resolver: (Deprecated) Callable that returns an auth token for a given agent + tenant.
233+
Use exporter_options instead.
234+
:param cluster_category: (Deprecated) Environment / cluster category (e.g. "prod").
235+
Use exporter_options instead.
236+
:param exporter_options: Agent365ExporterOptions instance for configuring the exporter.
237+
If provided, exporter_options takes precedence. If exporter_options is None, the token_resolver and cluster_category parameters are used as fallback/legacy support to construct a default Agent365ExporterOptions instance.
208238
:return: True if configuration succeeded, False otherwise.
209239
"""
210240
return _telemetry_manager.configure(
@@ -213,6 +243,7 @@ def configure(
213243
logger_name,
214244
token_resolver,
215245
cluster_category,
246+
exporter_options,
216247
**kwargs,
217248
)
218249

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,13 @@
9292
GEN_AI_AGENT_BLUEPRINT_ID_KEY = "gen_ai.agent.applicationid"
9393
CORRELATION_ID_KEY = "correlation.id"
9494
HIRING_MANAGER_ID_KEY = "hiring.manager.id"
95+
SESSION_DESCRIPTION_KEY = "session.description"
9596

9697
# Execution context dimensions
9798
GEN_AI_EXECUTION_TYPE_KEY = "gen_ai.execution.type"
9899
GEN_AI_EXECUTION_PAYLOAD_KEY = "gen_ai.execution.payload"
99100

100101
# Source metadata dimensions
101-
GEN_AI_EXECUTION_SOURCE_ID_KEY = "gen_ai.execution.sourceMetadata.id"
102102
GEN_AI_EXECUTION_SOURCE_NAME_KEY = "gen_ai.channel.name"
103103
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY = "gen_ai.channel.link"
104104

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
from typing import Awaitable, Callable, Optional
5+
6+
7+
class Agent365ExporterOptions:
8+
"""
9+
Configuration for Agent365Exporter.
10+
Only cluster_category and token_resolver are required for core operation.
11+
"""
12+
13+
def __init__(
14+
self,
15+
cluster_category: str = "prod",
16+
token_resolver: Optional[Callable[[str, str], Awaitable[Optional[str]]]] = None,
17+
use_s2s_endpoint: bool = False,
18+
max_queue_size: int = 2048,
19+
scheduled_delay_ms: int = 5000,
20+
exporter_timeout_ms: int = 30000,
21+
max_export_batch_size: int = 512,
22+
):
23+
"""
24+
Args:
25+
cluster_category: Cluster region argument. Defaults to 'prod'.
26+
token_resolver: Async callable that resolves the auth token (REQUIRED).
27+
use_s2s_endpoint: Use the S2S endpoint instead of standard endpoint.
28+
max_queue_size: Maximum queue size for the batch processor. Default is 2048.
29+
scheduled_delay_ms: Delay between export batches (ms). Default is 5000.
30+
exporter_timeout_ms: Timeout for the export operation (ms). Default is 30000.
31+
max_export_batch_size: Maximum batch size for export operations. Default is 512.
32+
"""
33+
self.cluster_category = cluster_category
34+
self.token_resolver = token_resolver
35+
self.use_s2s_endpoint = use_s2s_endpoint
36+
self.max_queue_size = max_queue_size
37+
self.scheduled_delay_ms = scheduled_delay_ms
38+
self.exporter_timeout_ms = exporter_timeout_ms
39+
self.max_export_batch_size = max_export_batch_size

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
GEN_AI_CALLER_UPN_KEY,
1818
GEN_AI_CALLER_USER_ID_KEY,
1919
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
20-
GEN_AI_EXECUTION_SOURCE_ID_KEY,
2120
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
2221
GEN_AI_EXECUTION_TYPE_KEY,
2322
GEN_AI_INPUT_MESSAGES_KEY,
@@ -111,7 +110,6 @@ def __init__(
111110
# Set request metadata if provided
112111
if request:
113112
if request.source_metadata:
114-
self.set_tag_maybe(GEN_AI_EXECUTION_SOURCE_ID_KEY, request.source_metadata.id)
115113
self.set_tag_maybe(GEN_AI_EXECUTION_SOURCE_NAME_KEY, request.source_metadata.name)
116114
self.set_tag_maybe(
117115
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, request.source_metadata.description
@@ -121,6 +119,7 @@ def __init__(
121119
GEN_AI_EXECUTION_TYPE_KEY,
122120
request.execution_type.value if request.execution_type else None,
123121
)
122+
self.set_tag_maybe(GEN_AI_INPUT_MESSAGES_KEY, safe_json_dumps([request.content]))
124123

125124
# Set caller details tags
126125
if caller_details:

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/baggage_builder.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
GEN_AI_CONVERSATION_ID_KEY,
2121
GEN_AI_CONVERSATION_ITEM_LINK_KEY,
2222
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
23-
GEN_AI_EXECUTION_SOURCE_ID_KEY,
2423
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
2524
HIRING_MANAGER_ID_KEY,
2625
OPERATION_SOURCE_KEY,
26+
SESSION_DESCRIPTION_KEY,
27+
SESSION_ID_KEY,
2728
TENANT_ID_KEY,
2829
)
30+
from ..models.operation_source import OperationSource
31+
from ..utils import deprecated
2932
from .turn_context_baggage import from_turn_context
3033

3134

@@ -50,16 +53,18 @@ def __init__(self):
5053
"""Initialize the baggage builder."""
5154
self._pairs: dict[str, str] = {}
5255

53-
def operation_source(self, value: str | None) -> "BaggageBuilder":
56+
def operation_source(self, value: OperationSource | None) -> "BaggageBuilder":
5457
"""Set the operation source baggage value.
5558
5659
Args:
57-
value: The operation source value
60+
value: The operation source enum value
5861
5962
Returns:
6063
Self for method chaining
6164
"""
62-
self._set(OPERATION_SOURCE_KEY, value)
65+
# Convert enum to string value for baggage storage
66+
str_value = value.value if value is not None else None
67+
self._set(OPERATION_SOURCE_KEY, str_value)
6368
return self
6469

6570
def tenant_id(self, value: str | None) -> "BaggageBuilder":
@@ -188,18 +193,38 @@ def conversation_item_link(self, value: str | None) -> "BaggageBuilder":
188193
self._set(GEN_AI_CONVERSATION_ITEM_LINK_KEY, value)
189194
return self
190195

196+
@deprecated("This is a no-op. Use channel_name() or channel_links() instead.")
191197
def source_metadata_id(self, value: str | None) -> "BaggageBuilder":
192198
"""Set the execution source metadata ID (e.g., channel ID)."""
193-
self._set(GEN_AI_EXECUTION_SOURCE_ID_KEY, value)
194199
return self
195200

201+
@deprecated("Use channel_name() instead")
196202
def source_metadata_name(self, value: str | None) -> "BaggageBuilder":
197203
"""Set the execution source metadata name (e.g., channel name)."""
198-
self._set(GEN_AI_EXECUTION_SOURCE_NAME_KEY, value)
199-
return self
204+
return self.channel_name(value)
200205

206+
@deprecated("Use channel_links() instead")
201207
def source_metadata_description(self, value: str | None) -> "BaggageBuilder":
202208
"""Set the execution source metadata description (e.g., channel description)."""
209+
return self.channel_links(value)
210+
211+
def session_id(self, value: str | None) -> "BaggageBuilder":
212+
"""Set the session ID baggage value."""
213+
self._set(SESSION_ID_KEY, value)
214+
return self
215+
216+
def session_description(self, value: str | None) -> "BaggageBuilder":
217+
"""Set the session description baggage value."""
218+
self._set(SESSION_DESCRIPTION_KEY, value)
219+
return self
220+
221+
def channel_name(self, value: str | None) -> "BaggageBuilder":
222+
"""Sets the channel name baggage value (e.g., 'Teams', 'msteams')."""
223+
self._set(GEN_AI_EXECUTION_SOURCE_NAME_KEY, value)
224+
return self
225+
226+
def channel_links(self, value: str | None) -> "BaggageBuilder":
227+
"""Sets the channel link baggage value. (e.g., channel links or description)."""
203228
self._set(GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, value)
204229
return self
205230

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/middleware/turn_context_baggage.py

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import json
4-
from typing import Any, Iterable, Iterator, Mapping
4+
from typing import Any, Iterator, Mapping
55

66
from ..constants import (
77
GEN_AI_AGENT_AUID_KEY,
@@ -17,7 +17,6 @@
1717
GEN_AI_CONVERSATION_ID_KEY,
1818
GEN_AI_CONVERSATION_ITEM_LINK_KEY,
1919
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
20-
GEN_AI_EXECUTION_SOURCE_ID_KEY,
2120
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
2221
GEN_AI_EXECUTION_TYPE_KEY,
2322
TENANT_ID_KEY,
@@ -26,9 +25,6 @@
2625

2726
AGENT_ROLE = "agenticUser"
2827
CHANNEL_ID_AGENTS = "agents"
29-
ENTITY_TYPE_WPX_COMMENT = "wpxcomment"
30-
ENTITY_TYPE_EMAIL_NOTIFICATION = "emailNotification"
31-
WPX_CONVERSATION_ID_FORMAT = "{document_id}_{parent_comment_id}"
3228

3329

3430
def _safe_get(obj: Any, *names: str) -> Any:
@@ -135,37 +131,40 @@ def _iter_tenant_id_pair(activity: Any) -> Iterator[tuple[str, Any]]:
135131

136132

137133
def _iter_source_metadata_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
134+
"""
135+
Generate source metadata pairs from activity, handling both string and ChannelId object cases.
136+
137+
:param activity: The activity object (Activity instance or dict)
138+
:return: Iterator of (key, value) tuples for source metadata
139+
"""
140+
# Handle channel_id (can be string or ChannelId object)
138141
channel_id = _safe_get(activity, "channel_id")
139-
yield GEN_AI_EXECUTION_SOURCE_ID_KEY, channel_id
140-
yield GEN_AI_EXECUTION_SOURCE_NAME_KEY, channel_id
141-
yield GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, _safe_get(activity, "type", "Type")
142+
143+
# Extract channel name from either string or ChannelId object
144+
channel_name = None
145+
sub_channel = None
146+
147+
if channel_id is not None:
148+
if isinstance(channel_id, str):
149+
# Direct string value
150+
channel_name = channel_id
151+
elif hasattr(channel_id, "channel"):
152+
# ChannelId object
153+
channel_name = channel_id.channel
154+
sub_channel = getattr(channel_id, "sub_channel", None)
155+
elif isinstance(channel_id, dict):
156+
# Serialized ChannelId as dict
157+
channel_name = channel_id.get("channel")
158+
sub_channel = channel_id.get("sub_channel")
159+
160+
# Yield channel name as source name
161+
yield GEN_AI_EXECUTION_SOURCE_NAME_KEY, channel_name
162+
yield GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, sub_channel
142163

143164

144165
def _iter_conversation_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
145-
channel_id = _safe_get(activity, "channel_id")
146-
entities = _safe_get(activity, "entities") or []
147-
conversation_id = None
148-
149-
if channel_id == CHANNEL_ID_AGENTS and isinstance(entities, Iterable):
150-
# search entities for wpxcomment or emailNotification
151-
for e in entities:
152-
etype = _safe_get(e, "type", "Type")
153-
if etype == ENTITY_TYPE_WPX_COMMENT:
154-
document_id = _safe_get(e, "documentId", "document_id")
155-
parent_comment_id = _safe_get(e, "parentCommentId", "parent_comment_id")
156-
if document_id and parent_comment_id:
157-
conversation_id = WPX_CONVERSATION_ID_FORMAT.format(
158-
document_id=document_id,
159-
parent_comment_id=parent_comment_id,
160-
)
161-
break
162-
elif etype == ENTITY_TYPE_EMAIL_NOTIFICATION:
163-
conversation_id = _safe_get(e, "conversationId", "conversation_id")
164-
if conversation_id:
165-
break
166-
if not conversation_id:
167-
conv = _safe_get(activity, "conversation")
168-
conversation_id = _safe_get(conv, "id", "Id")
166+
conv = _safe_get(activity, "conversation")
167+
conversation_id = _safe_get(conv, "id")
169168

170169
item_link = _safe_get(activity, "service_url")
171170

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Operation source enumeration for Agent365 SDK."""
5+
6+
from enum import Enum
7+
8+
9+
class OperationSource(Enum):
10+
"""
11+
Enumeration representing the source of an operation.
12+
"""
13+
14+
SDK = "SDK"
15+
"""Operation executed by SDK."""
16+
17+
GATEWAY = "Gateway"
18+
"""Operation executed by Gateway."""
19+
20+
MCP_SERVER = "MCPServer"
21+
"""Operation executed by MCP Server."""

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,3 @@ class Request:
1616
execution_type: ExecutionType
1717
session_id: str | None = None
1818
source_metadata: SourceMetadata | None = None
19-
payload: str | None = None

0 commit comments

Comments
 (0)