Skip to content

Commit 7ac987c

Browse files
fix(sdk): honor API key header for observability
1 parent 5d080e8 commit 7ac987c

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_OBSERVABILITY_SINK_NAME: Selected control-event sink (default: default)
@@ -286,6 +289,7 @@ class EventBatcher:
286289
Attributes:
287290
server_url: Base URL of the Agent Control server
288291
api_key: API key for authentication
292+
api_key_header: HTTP header used to send the API key
289293
batch_size: Maximum events per batch
290294
flush_interval: Seconds between automatic flushes
291295
"""
@@ -294,6 +298,7 @@ def __init__(
294298
self,
295299
server_url: str | None = None,
296300
api_key: str | None = None,
301+
api_key_header: str | None = None,
297302
batch_size: int | None = None,
298303
flush_interval: float | None = None,
299304
):
@@ -303,11 +308,13 @@ def __init__(
303308
Args:
304309
server_url: Server URL (defaults to get_settings().url)
305310
api_key: API key (defaults to get_settings().api_key)
311+
api_key_header: API key header (defaults to get_settings().api_key_header)
306312
batch_size: Max events per batch (defaults to get_settings().batch_size)
307313
flush_interval: Seconds between flushes (defaults to get_settings().flush_interval)
308314
"""
309315
self.server_url = server_url or get_settings().url
310316
self.api_key = api_key or get_settings().api_key
317+
self.api_key_header = api_key_header or get_settings().api_key_header
311318
self.batch_size = batch_size if batch_size is not None else get_settings().batch_size
312319
if flush_interval is not None:
313320
self.flush_interval = flush_interval
@@ -424,7 +431,7 @@ def _build_batch_request(
424431
url = f"{self.server_url}/api/v1/observability/events"
425432
headers = {"Content-Type": "application/json"}
426433
if self.api_key:
427-
headers["X-API-Key"] = self.api_key
434+
headers[self.api_key_header] = self.api_key
428435
payload = {"events": [event.model_dump(mode="json") for event in events]}
429436
return url, headers, payload
430437

@@ -1098,6 +1105,7 @@ def _get_custom_control_event_sinks_to_shutdown() -> tuple[ControlEventSink, ...
10981105
def init_observability(
10991106
server_url: str | None = None,
11001107
api_key: str | None = None,
1108+
api_key_header: str | None = None,
11011109
enabled: bool | None = None,
11021110
sink_name: str | None = None,
11031111
sink_config: JSONObject | None = None,
@@ -1110,6 +1118,7 @@ def init_observability(
11101118
Args:
11111119
server_url: Server URL for sending events
11121120
api_key: API key for authentication
1121+
api_key_header: HTTP header used to send the API key
11131122
enabled: Override AGENT_CONTROL_OBSERVABILITY_ENABLED
11141123
sink_name: Override AGENT_CONTROL_OBSERVABILITY_SINK_NAME
11151124
sink_config: Override AGENT_CONTROL_OBSERVABILITY_SINK_CONFIG
@@ -1157,7 +1166,11 @@ def init_observability(
11571166
return _batcher
11581167

11591168
# Create batcher
1160-
_batcher = EventBatcher(server_url=server_url, api_key=api_key)
1169+
_batcher = EventBatcher(
1170+
server_url=server_url,
1171+
api_key=api_key,
1172+
api_key_header=api_key_header,
1173+
)
11611174
_batcher.start()
11621175
_event_sink = _BatcherControlEventSink(_batcher)
11631176

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
@@ -120,6 +120,7 @@ def reset_observability_state() -> None:
120120
observability_enabled=True,
121121
observability_sink_name=DEFAULT_CONTROL_EVENT_SINK_NAME,
122122
observability_sink_config={},
123+
api_key_header="X-API-Key",
123124
)
124125
with obs._used_custom_event_sinks_lock:
125126
obs._used_custom_event_sinks.clear()
@@ -136,6 +137,7 @@ class TestEventBatcherInit:
136137
def test_init_default_values(self):
137138
"""Test EventBatcher initializes with default values."""
138139
batcher = EventBatcher()
140+
assert batcher.api_key_header == get_settings().api_key_header
139141
assert batcher.batch_size == get_settings().batch_size
140142
assert batcher.flush_interval == get_settings().flush_interval
141143
assert batcher.shutdown_join_timeout == get_settings().shutdown_join_timeout
@@ -149,32 +151,37 @@ def test_init_custom_values(self):
149151
batcher = EventBatcher(
150152
server_url="http://custom:9000",
151153
api_key="test-key",
154+
api_key_header="X-Custom-API-Key",
152155
batch_size=50,
153156
flush_interval=5.0,
154157
)
155158
assert batcher.server_url == "http://custom:9000"
156159
assert batcher.api_key == "test-key"
160+
assert batcher.api_key_header == "X-Custom-API-Key"
157161
assert batcher.batch_size == 50
158162
assert batcher.flush_interval == 5.0
159163

160164
def test_init_from_settings(self):
161165
"""Test EventBatcher reads from settings."""
162166
from agent_control.settings import configure_settings
163167

164-
# Save original values
165-
original_url = get_settings().url
166-
original_api_key = get_settings().api_key
168+
original_settings = get_settings().model_dump()
167169

168170
try:
169171
# Configure settings programmatically
170-
configure_settings(url="http://configured-server:8080", api_key="configured-api-key")
172+
configure_settings(
173+
url="http://configured-server:8080",
174+
api_key="configured-api-key",
175+
api_key_header="X-Custom-API-Key",
176+
)
171177

172178
batcher = EventBatcher()
173179
assert batcher.server_url == "http://configured-server:8080"
174180
assert batcher.api_key == "configured-api-key"
181+
assert batcher.api_key_header == "X-Custom-API-Key"
175182
finally:
176183
# Restore original settings
177-
configure_settings(url=original_url, api_key=original_api_key)
184+
configure_settings(**original_settings)
178185

179186

180187
class TestEventBatcherStartStop:
@@ -557,6 +564,46 @@ def test_send_batch_sync_returns_true_on_202(self):
557564
assert result is True
558565
client_ctor.assert_called_once_with(timeout=30.0)
559566
client.post.assert_called_once()
567+
assert client.post.call_args.kwargs["headers"]["X-API-Key"] == "test-key"
568+
569+
def test_send_batch_sync_uses_configured_api_key_header(self):
570+
batcher = EventBatcher(
571+
server_url="http://test:8000",
572+
api_key="test-key",
573+
api_key_header="X-Custom-API-Key",
574+
)
575+
response = MagicMock(status_code=202, text="accepted")
576+
client = MagicMock()
577+
client.post.return_value = response
578+
client_context = MagicMock()
579+
client_context.__enter__.return_value = client
580+
581+
with patch(
582+
"agent_control.observability.httpx.Client",
583+
return_value=client_context,
584+
):
585+
result = batcher._send_batch_sync([create_mock_event()])
586+
587+
assert result is True
588+
headers = client.post.call_args.kwargs["headers"]
589+
assert headers["X-Custom-API-Key"] == "test-key"
590+
assert "X-API-Key" not in headers
591+
592+
def test_build_batch_request_uses_settings_api_key_header(self):
593+
original_settings = get_settings().model_dump()
594+
try:
595+
configure_settings(
596+
api_key="settings-key",
597+
api_key_header="X-Custom-API-Key",
598+
)
599+
batcher = EventBatcher()
600+
601+
_, headers, _ = batcher._build_batch_request([create_mock_event()])
602+
603+
assert headers["X-Custom-API-Key"] == "settings-key"
604+
assert "X-API-Key" not in headers
605+
finally:
606+
configure_settings(**original_settings)
560607

561608
def test_send_batch_sync_returns_false_on_401_without_retry(self):
562609
batcher = EventBatcher()
@@ -1069,10 +1116,12 @@ def test_init_enabled_creates_batcher(self):
10691116
result = init_observability(
10701117
server_url="http://test:8000",
10711118
api_key="test-key",
1119+
api_key_header="X-Custom-API-Key",
10721120
enabled=True,
10731121
)
10741122
assert result is not None
10751123
assert isinstance(result, EventBatcher)
1124+
assert result.api_key_header == "X-Custom-API-Key"
10761125
assert result._running is True
10771126
assert get_event_sink() is not None
10781127

0 commit comments

Comments
 (0)