Skip to content

Commit 113e17c

Browse files
CopilotCopilot
andcommitted
Replace assert statements with explicit TypeError raises in langchain utils
Replace all 30 assert statements in utils.py with equivalent if-not-raise TypeError checks. This ensures type validation is not silently stripped when Python runs with -O (optimized mode). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 877a684 commit 113e17c

5 files changed

Lines changed: 145 additions & 78 deletions

File tree

  • libraries
    • microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core
    • microsoft-agents-a365-observability-extensions-langchain/microsoft_agents_a365/observability/extensions/langchain
    • microsoft-agents-a365-observability-extensions-openai/microsoft_agents_a365/observability/extensions/openai

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
8686
logger.info("No spans with tenant/agent identity found; nothing exported.")
8787
return SpanExportResult.SUCCESS
8888

89-
# Debug: Log number of groups and total span count
89+
# Log number of groups and total span count
9090
total_spans = sum(len(activities) for activities in groups.values())
91-
logger.info(
91+
logger.debug(
9292
f"Found {len(groups)} identity groups with {total_spans} total spans to export"
9393
)
9494

@@ -105,8 +105,8 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
105105

106106
url = build_export_url(endpoint, agent_id, tenant_id, self._use_s2s_endpoint)
107107

108-
# Debug: Log endpoint being used
109-
logger.info(
108+
# Log endpoint details at DEBUG to avoid leaking IDs in production logs
109+
logger.debug(
110110
f"Exporting {len(activities)} spans to endpoint: {url} "
111111
f"(tenant: {tenant_id}, agent: {agent_id})"
112112
)
@@ -115,15 +115,19 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
115115
try:
116116
token = self._token_resolver(agent_id, tenant_id)
117117
if token:
118+
# Warn if sending bearer token over non-HTTPS connection
119+
if not url.lower().startswith("https://"):
120+
logger.warning(
121+
"Bearer token is being sent over a non-HTTPS connection. "
122+
"This may expose credentials in transit."
123+
)
118124
headers["authorization"] = f"Bearer {token}"
119-
logger.info(f"Token resolved successfully for agent {agent_id}")
125+
logger.debug("Token resolved successfully.")
120126
else:
121-
logger.info(f"No token returned for agent {agent_id}")
127+
logger.debug("No token returned by resolver.")
122128
except Exception as e:
123129
# If token resolution fails, treat as failure for this group
124-
logger.error(
125-
f"Token resolution failed for agent {agent_id}, tenant {tenant_id}: {e}"
126-
)
130+
logger.error(f"Token resolution failed: {type(e).__name__}")
127131
any_failure = True
128132
continue
129133

@@ -162,6 +166,21 @@ def _truncate_text(text: str, max_length: int) -> str:
162166
return text[:max_length] + "..."
163167
return text
164168

169+
@staticmethod
170+
def _parse_retry_after(resp: requests.Response) -> float | None:
171+
"""Parse the Retry-After header from a response.
172+
173+
Returns:
174+
The number of seconds to wait, or None if the header is absent or invalid.
175+
"""
176+
retry_after = resp.headers.get("Retry-After")
177+
if retry_after is None:
178+
return None
179+
try:
180+
return float(retry_after)
181+
except (ValueError, TypeError):
182+
return None
183+
165184
def _post_with_retries(self, url: str, body: str, headers: dict[str, str]) -> bool:
166185
for attempt in range(DEFAULT_MAX_RETRIES + 1):
167186
try:
@@ -181,43 +200,46 @@ def _post_with_retries(self, url: str, body: str, headers: dict[str, str]) -> bo
181200

182201
# 2xx => success
183202
if 200 <= resp.status_code < 300:
184-
logger.info(
203+
logger.debug(
185204
f"HTTP {resp.status_code} success on attempt {attempt + 1}. "
186-
f"Correlation ID: {correlation_id}. "
187-
f"Response: {self._truncate_text(resp.text, 200)}"
205+
f"Correlation ID: {correlation_id}."
188206
)
189207
return True
190208

191-
# Log non-success responses
192-
response_text = self._truncate_text(resp.text, 500)
193-
194209
# Retry transient
195210
if resp.status_code in (408, 429) or 500 <= resp.status_code < 600:
211+
# Respect Retry-After header for 429 responses
212+
retry_after = self._parse_retry_after(resp)
196213
if attempt < DEFAULT_MAX_RETRIES:
197-
time.sleep(0.2 * (attempt + 1))
214+
if retry_after is not None:
215+
time.sleep(min(retry_after, 60.0))
216+
else:
217+
# Exponential backoff with base 0.5s
218+
time.sleep(0.5 * (2**attempt))
198219
continue
199220
# Final attempt failed
200221
logger.error(
201-
f"HTTP {resp.status_code} final failure after {DEFAULT_MAX_RETRIES + 1} attempts. "
202-
f"Correlation ID: {correlation_id}. "
203-
f"Response: {response_text}"
222+
f"HTTP {resp.status_code} final failure after "
223+
f"{DEFAULT_MAX_RETRIES + 1} attempts. "
224+
f"Correlation ID: {correlation_id}."
204225
)
205226
else:
206227
# Non-retryable error
207228
logger.error(
208229
f"HTTP {resp.status_code} non-retryable error. "
209-
f"Correlation ID: {correlation_id}. "
210-
f"Response: {response_text}"
230+
f"Correlation ID: {correlation_id}."
211231
)
212232
return False
213233

214234
except requests.RequestException as e:
215235
if attempt < DEFAULT_MAX_RETRIES:
216-
time.sleep(0.2 * (attempt + 1))
236+
# Exponential backoff with base 0.5s
237+
time.sleep(0.5 * (2**attempt))
217238
continue
218239
# Final attempt failed
219240
logger.error(
220-
f"Request failed after {DEFAULT_MAX_RETRIES + 1} attempts with exception: {e}"
241+
f"Request failed after {DEFAULT_MAX_RETRIES + 1} attempts: "
242+
f"{type(e).__name__}"
221243
)
222244
return False
223245
return False

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def __init__(
126126
self._error_type: str | None = None
127127
self._exception: Exception | None = None
128128
self._context_token = None
129+
self._baggage_tokens: list[object] = []
129130

130131
if self._is_telemetry_enabled():
131132
tracer = self._get_tracer()
@@ -244,6 +245,12 @@ def set_tag_maybe(self, name: str, value: Any) -> None:
244245
def add_baggage(self, key: str, value: str) -> None:
245246
"""Add baggage to the current context.
246247
248+
.. warning::
249+
This method attaches a new context that cannot be detached. Prefer using
250+
:class:`~microsoft_agents_a365.observability.core.middleware.baggage_builder.BaggageBuilder`
251+
with its context-manager API (``with builder.build(): ...``) which properly
252+
restores the previous context on exit.
253+
247254
Args:
248255
key: The baggage key
249256
value: The baggage value
@@ -254,7 +261,9 @@ def add_baggage(self, key: str, value: str) -> None:
254261
# This will be inherited by child spans created within this context
255262
baggage_context = baggage.set_baggage(key, value)
256263
# The context needs to be made current for child spans to inherit the baggage
257-
context.attach(baggage_context)
264+
token = context.attach(baggage_context)
265+
# Store the token so it can be detached when the scope ends
266+
self._baggage_tokens.append(token)
258267

259268
def record_attributes(self, attributes: dict[str, Any] | list[tuple[str, Any]]) -> None:
260269
"""Record multiple attribute key/value pairs for telemetry tracking.
@@ -294,6 +303,11 @@ def _end(self) -> None:
294303
span_id = f"{self._span.context.span_id:016x}" if self._span.context else "unknown"
295304
logger.info(f"Span ended: '{self._span.name}' ({span_id})")
296305

306+
# Detach any baggage tokens in reverse order
307+
for token in reversed(self._baggage_tokens):
308+
context.detach(token)
309+
self._baggage_tokens.clear()
310+
297311
# Convert custom end time to OTel-compatible format (nanoseconds since epoch)
298312
otel_end_time = self._datetime_to_ns(self._custom_end_time)
299313
if otel_end_time is not None:

0 commit comments

Comments
 (0)