Skip to content

Commit 31280a4

Browse files
committed
Addressing comments
1 parent 2350667 commit 31280a4

7 files changed

Lines changed: 53 additions & 4 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ def _configure_internal(
180180
token_resolver=exporter_options.token_resolver,
181181
cluster_category=exporter_options.cluster_category,
182182
use_s2s_endpoint=exporter_options.use_s2s_endpoint,
183+
max_payload_bytes=exporter_options.max_payload_bytes,
183184
)
184185

185186
else:

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
104104
)
105105

106106
if len(chunks) > 1:
107-
logger.info(
107+
# Logged at DEBUG to avoid leaking tenant/agent IDs in production logs.
108+
logger.debug(
108109
f"Split {len(activities)} spans into {len(chunks)} chunks "
109110
f"for tenantId: {tenant_id}, agentId: {agent_id}"
110111
)
@@ -151,10 +152,21 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
151152
payload = self._build_envelope(chunk, resource_attrs)
152153
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
153154
body_bytes = len(body.encode("utf-8"))
154-
logger.info(
155+
logger.debug(
155156
f"Sending chunk {i + 1} of {len(chunks)} "
156157
f"({len(chunk)} spans, {body_bytes} bytes)"
157158
)
159+
# Defensive check: the estimator covers per-span content but not
160+
# envelope overhead (resource attributes, scope wrappers). Warn if
161+
# the assembled body exceeds the configured limit so operators can
162+
# observe estimator drift before the server starts rejecting requests.
163+
if body_bytes > self._max_payload_bytes:
164+
logger.warning(
165+
f"Chunk {i + 1} of {len(chunks)} body size ({body_bytes} bytes) "
166+
f"exceeds max_payload_bytes ({self._max_payload_bytes}); "
167+
"estimator may be under-counting envelope overhead. "
168+
f"Tenant: {tenant_id}, agent: {agent_id}, spans: {len(chunk)}."
169+
)
158170

159171
ok = self._post_with_retries(url, body, headers)
160172
if not ok:

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from typing import Awaitable, Callable, Optional
55

6+
from .utils import DEFAULT_MAX_PAYLOAD_BYTES
7+
68

79
class Agent365ExporterOptions:
810
"""
@@ -19,6 +21,7 @@ def __init__(
1921
scheduled_delay_ms: int = 5000,
2022
exporter_timeout_ms: int = 30000,
2123
max_export_batch_size: int = 512,
24+
max_payload_bytes: int = DEFAULT_MAX_PAYLOAD_BYTES,
2225
):
2326
"""
2427
Args:
@@ -29,6 +32,10 @@ def __init__(
2932
scheduled_delay_ms: Delay between export batches (ms). Default is 5000.
3033
exporter_timeout_ms: Timeout for the export operation (ms). Default is 30000.
3134
max_export_batch_size: Maximum batch size for export operations. Default is 512.
35+
max_payload_bytes: Upper bound on HTTP request body size in bytes. The exporter
36+
splits per-identity batches into sub-batches whose estimated size stays under
37+
this limit, providing headroom under the A365 1 MB server limit. Default is
38+
900_000 (~100 KB headroom for estimator error and JSON envelope overhead).
3239
"""
3340
self.cluster_category = cluster_category
3441
self.token_resolver = token_resolver
@@ -37,3 +44,4 @@ def __init__(
3744
self.scheduled_delay_ms = scheduled_delay_ms
3845
self.exporter_timeout_ms = exporter_timeout_ms
3946
self.max_export_batch_size = max_export_batch_size
47+
self.max_payload_bytes = max_payload_bytes

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,19 +354,33 @@ def chunk_by_size(
354354
) -> list[list[T]]:
355355
"""Split items into sub-batches whose cumulative estimated size stays under ``max_chunk_bytes``.
356356
357+
Multi-item chunks are guaranteed to stay within the limit. A single item
358+
whose estimated size exceeds ``max_chunk_bytes`` forms its own one-item
359+
chunk (never silently dropped) even though that chunk exceeds the limit.
360+
357361
Invariants:
358362
- Input order is preserved across chunks.
359363
- Empty input produces empty output.
360-
- A single item whose size exceeds ``max_chunk_bytes`` forms its own
361-
single-item chunk (never silently dropped).
364+
- No item is ever dropped.
362365
- No chunk is ever empty.
366+
367+
Raises:
368+
ValueError: If ``max_chunk_bytes`` is not positive, or if ``get_size``
369+
returns a negative value for any item.
363370
"""
371+
if max_chunk_bytes <= 0:
372+
raise ValueError(f"max_chunk_bytes must be positive, got {max_chunk_bytes}")
373+
364374
chunks: list[list[T]] = []
365375
current: list[T] = []
366376
current_bytes = 0
367377

368378
for item in items:
369379
item_bytes = get_size(item)
380+
if item_bytes < 0:
381+
raise ValueError(
382+
f"get_size returned a negative value ({item_bytes}); sizes must be non-negative"
383+
)
370384
if current and current_bytes + item_bytes > max_chunk_bytes:
371385
chunks.append(current)
372386
current = []

tests/observability/core/exporters/test_payload_chunking.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ def test_multi_item_chunks_respect_limit_no_chunk_is_empty(self) -> None:
107107
if len(chunk) > 1:
108108
self.assertLessEqual(sum(item["size"] for item in chunk), 500_000)
109109

110+
def test_rejects_zero_max_chunk_bytes(self) -> None:
111+
with self.assertRaises(ValueError):
112+
chunk_by_size([{"id": "s", "size": 1}], self._get_size, 0)
113+
114+
def test_rejects_negative_max_chunk_bytes(self) -> None:
115+
with self.assertRaises(ValueError):
116+
chunk_by_size([{"id": "s", "size": 1}], self._get_size, -1)
117+
118+
def test_rejects_negative_item_size(self) -> None:
119+
with self.assertRaises(ValueError):
120+
chunk_by_size([{"id": "s", "size": -1}], self._get_size, 100)
121+
110122

111123
class TestTruncateSpanVerification(unittest.TestCase):
112124
MAX = 250 * 1024

tests/observability/core/test_agent365.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def test_batch_span_processor_and_exporter_called_with_correct_values(
120120
token_resolver=self.mock_token_resolver,
121121
cluster_category="staging",
122122
use_s2s_endpoint=True,
123+
max_payload_bytes=900_000,
123124
)
124125

125126
# Verify BatchSpanProcessor was called with correct parameters from exporter_options

tests/observability/core/test_spectra_exporter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ def test_configure_with_agent365_options_unchanged(self, mock_is_enabled, mock_e
188188
token_resolver=mock_token_resolver,
189189
cluster_category="staging",
190190
use_s2s_endpoint=True,
191+
max_payload_bytes=900_000,
191192
)
192193

193194
@patch("microsoft_agents_a365.observability.core.config.OTLPSpanExporter")

0 commit comments

Comments
 (0)