Skip to content

Commit 73bd51c

Browse files
fix(sdk): honor API key header for observability
1 parent daca1e4 commit 73bd51c

3 files changed

Lines changed: 73 additions & 7 deletions

File tree

sdks/python/src/agent_control/observability.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
await shutdown_observability()
2727
2828
Configuration (Environment Variables):
29+
# Server connection
30+
AGENT_CONTROL_API_KEY_HEADER: API key header name (default: X-API-Key)
31+
2932
# Observability (event batching)
3033
AGENT_CONTROL_OBSERVABILITY_ENABLED: Enable observability (default: true)
3134
AGENT_CONTROL_BATCH_SIZE: Max events per batch (default: 100)
@@ -275,6 +278,7 @@ class EventBatcher:
275278
Attributes:
276279
server_url: Base URL of the Agent Control server
277280
api_key: API key for authentication
281+
api_key_header: HTTP header used to send the API key
278282
batch_size: Maximum events per batch
279283
flush_interval: Seconds between automatic flushes
280284
"""
@@ -283,6 +287,7 @@ def __init__(
283287
self,
284288
server_url: str | None = None,
285289
api_key: str | None = None,
290+
api_key_header: str | None = None,
286291
batch_size: int | None = None,
287292
flush_interval: float | None = None,
288293
):
@@ -292,11 +297,13 @@ def __init__(
292297
Args:
293298
server_url: Server URL (defaults to get_settings().url)
294299
api_key: API key (defaults to get_settings().api_key)
300+
api_key_header: API key header (defaults to get_settings().api_key_header)
295301
batch_size: Max events per batch (defaults to get_settings().batch_size)
296302
flush_interval: Seconds between flushes (defaults to get_settings().flush_interval)
297303
"""
298304
self.server_url = server_url or get_settings().url
299305
self.api_key = api_key or get_settings().api_key
306+
self.api_key_header = api_key_header or get_settings().api_key_header
300307
self.batch_size = batch_size if batch_size is not None else get_settings().batch_size
301308
if flush_interval is not None:
302309
self.flush_interval = flush_interval
@@ -413,7 +420,7 @@ def _build_batch_request(
413420
url = f"{self.server_url}/api/v1/observability/events"
414421
headers = {"Content-Type": "application/json"}
415422
if self.api_key:
416-
headers["X-API-Key"] = self.api_key
423+
headers[self.api_key_header] = self.api_key
417424
payload = {"events": [event.model_dump(mode="json") for event in events]}
418425
return url, headers, payload
419426

@@ -1023,6 +1030,7 @@ async def _run_awaitable_during_shutdown(result: Awaitable[Any]) -> None:
10231030
def init_observability(
10241031
server_url: str | None = None,
10251032
api_key: str | None = None,
1033+
api_key_header: str | None = None,
10261034
enabled: bool | None = None,
10271035
sink_name: str | None = None,
10281036
sink_config: JSONObject | None = None,
@@ -1035,6 +1043,7 @@ def init_observability(
10351043
Args:
10361044
server_url: Server URL for sending events
10371045
api_key: API key for authentication
1046+
api_key_header: HTTP header used to send the API key
10381047
enabled: Override AGENT_CONTROL_OBSERVABILITY_ENABLED
10391048
sink_name: Override AGENT_CONTROL_OBSERVABILITY_SINK_NAME
10401049
sink_config: Override AGENT_CONTROL_OBSERVABILITY_SINK_CONFIG
@@ -1079,7 +1088,11 @@ def init_observability(
10791088
return _batcher
10801089

10811090
# Create batcher
1082-
_batcher = EventBatcher(server_url=server_url, api_key=api_key)
1091+
_batcher = EventBatcher(
1092+
server_url=server_url,
1093+
api_key=api_key,
1094+
api_key_header=api_key_header,
1095+
)
10831096
_batcher.start()
10841097
_event_sink = _BatcherControlEventSink(_batcher)
10851098

sdks/python/src/agent_control/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ class SDKSettings(BaseSettings):
5656
default="",
5757
description="API key for server authentication",
5858
)
59+
api_key_header: str = Field(
60+
default="X-API-Key",
61+
description="HTTP header used to send the API key",
62+
)
5963

6064
# Observability (event batching)
6165
observability_enabled: bool = Field(

sdks/python/tests/test_observability.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def reset_observability_state() -> None:
113113
observability_enabled=True,
114114
observability_sink_name=DEFAULT_CONTROL_EVENT_SINK_NAME,
115115
observability_sink_config={},
116+
api_key_header="X-API-Key",
116117
)
117118
with obs._external_event_sinks_lock:
118119
obs._external_event_sinks.clear()
@@ -126,6 +127,7 @@ class TestEventBatcherInit:
126127
def test_init_default_values(self):
127128
"""Test EventBatcher initializes with default values."""
128129
batcher = EventBatcher()
130+
assert batcher.api_key_header == get_settings().api_key_header
129131
assert batcher.batch_size == get_settings().batch_size
130132
assert batcher.flush_interval == get_settings().flush_interval
131133
assert batcher.shutdown_join_timeout == get_settings().shutdown_join_timeout
@@ -139,32 +141,37 @@ def test_init_custom_values(self):
139141
batcher = EventBatcher(
140142
server_url="http://custom:9000",
141143
api_key="test-key",
144+
api_key_header="X-Custom-API-Key",
142145
batch_size=50,
143146
flush_interval=5.0,
144147
)
145148
assert batcher.server_url == "http://custom:9000"
146149
assert batcher.api_key == "test-key"
150+
assert batcher.api_key_header == "X-Custom-API-Key"
147151
assert batcher.batch_size == 50
148152
assert batcher.flush_interval == 5.0
149153

150154
def test_init_from_settings(self):
151155
"""Test EventBatcher reads from settings."""
152156
from agent_control.settings import configure_settings
153157

154-
# Save original values
155-
original_url = get_settings().url
156-
original_api_key = get_settings().api_key
158+
original_settings = get_settings().model_dump()
157159

158160
try:
159161
# Configure settings programmatically
160-
configure_settings(url="http://configured-server:8080", api_key="configured-api-key")
162+
configure_settings(
163+
url="http://configured-server:8080",
164+
api_key="configured-api-key",
165+
api_key_header="X-Custom-API-Key",
166+
)
161167

162168
batcher = EventBatcher()
163169
assert batcher.server_url == "http://configured-server:8080"
164170
assert batcher.api_key == "configured-api-key"
171+
assert batcher.api_key_header == "X-Custom-API-Key"
165172
finally:
166173
# Restore original settings
167-
configure_settings(url=original_url, api_key=original_api_key)
174+
configure_settings(**original_settings)
168175

169176

170177
class TestEventBatcherStartStop:
@@ -547,6 +554,46 @@ def test_send_batch_sync_returns_true_on_202(self):
547554
assert result is True
548555
client_ctor.assert_called_once_with(timeout=30.0)
549556
client.post.assert_called_once()
557+
assert client.post.call_args.kwargs["headers"]["X-API-Key"] == "test-key"
558+
559+
def test_send_batch_sync_uses_configured_api_key_header(self):
560+
batcher = EventBatcher(
561+
server_url="http://test:8000",
562+
api_key="test-key",
563+
api_key_header="X-Custom-API-Key",
564+
)
565+
response = MagicMock(status_code=202, text="accepted")
566+
client = MagicMock()
567+
client.post.return_value = response
568+
client_context = MagicMock()
569+
client_context.__enter__.return_value = client
570+
571+
with patch(
572+
"agent_control.observability.httpx.Client",
573+
return_value=client_context,
574+
):
575+
result = batcher._send_batch_sync([create_mock_event()])
576+
577+
assert result is True
578+
headers = client.post.call_args.kwargs["headers"]
579+
assert headers["X-Custom-API-Key"] == "test-key"
580+
assert "X-API-Key" not in headers
581+
582+
def test_build_batch_request_uses_settings_api_key_header(self):
583+
original_settings = get_settings().model_dump()
584+
try:
585+
configure_settings(
586+
api_key="settings-key",
587+
api_key_header="X-Custom-API-Key",
588+
)
589+
batcher = EventBatcher()
590+
591+
_, headers, _ = batcher._build_batch_request([create_mock_event()])
592+
593+
assert headers["X-Custom-API-Key"] == "settings-key"
594+
assert "X-API-Key" not in headers
595+
finally:
596+
configure_settings(**original_settings)
550597

551598
def test_send_batch_sync_returns_false_on_401_without_retry(self):
552599
batcher = EventBatcher()
@@ -1056,10 +1103,12 @@ def test_init_enabled_creates_batcher(self):
10561103
result = init_observability(
10571104
server_url="http://test:8000",
10581105
api_key="test-key",
1106+
api_key_header="X-Custom-API-Key",
10591107
enabled=True,
10601108
)
10611109
assert result is not None
10621110
assert isinstance(result, EventBatcher)
1111+
assert result.api_key_header == "X-Custom-API-Key"
10631112
assert result._running is True
10641113
assert get_event_sink() is not None
10651114

0 commit comments

Comments
 (0)