Skip to content

Commit fac5708

Browse files
authored
Emit OpenAI Agents SDK integration usage telemetry (#14)
1 parent 1244b42 commit fac5708

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed

azure/durable_functions/openai_agents/orchestrator_generator.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .runner import DurableOpenAIRunner
1010
from .context import DurableAIAgentContext
1111
from .event_loop import ensure_event_loop
12+
from .usage_telemetry import UsageTelemetry
1213

1314

1415
async def durable_openai_agent_activity(input: str, model_provider: ModelProvider):
@@ -29,6 +30,9 @@ def durable_openai_agent_orchestrator_generator(
2930
activity_name: str,
3031
):
3132
"""Adapts the synchronous OpenAI Agents function to an Durable orchestrator generator."""
33+
# Log versions the first time this generator is invoked
34+
UsageTelemetry.log_usage_once()
35+
3236
ensure_event_loop()
3337
task_tracker = TaskTracker(durable_orchestration_context)
3438
durable_ai_agent_context = DurableAIAgentContext(
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
5+
class UsageTelemetry:
6+
"""Handles telemetry logging for OpenAI Agents SDK integration usage."""
7+
8+
# Class-level flag to ensure logging happens only once across all instances
9+
_usage_logged = False
10+
11+
@classmethod
12+
def log_usage_once(cls):
13+
"""Log OpenAI Agents SDK integration usage exactly once.
14+
15+
Fails gracefully if metadata cannot be retrieved.
16+
"""
17+
if cls._usage_logged:
18+
return
19+
20+
# NOTE: Any log line beginning with the special prefix defined below will be
21+
# captured by the Azure Functions host as a Language Worker console log and
22+
# forwarded to internal telemetry pipelines.
23+
# Do not change this constant value without coordinating with the Functions
24+
# host team.
25+
LANGUAGE_WORKER_CONSOLE_LOG_PREFIX = "LanguageWorkerConsoleLog"
26+
27+
package_versions = cls._collect_openai_agent_package_versions()
28+
msg = (
29+
f"{LANGUAGE_WORKER_CONSOLE_LOG_PREFIX}" # Prefix captured by Azure Functions host
30+
"Detected OpenAI Agents SDK integration with Durable Functions. "
31+
f"Package versions: {package_versions}"
32+
)
33+
print(msg)
34+
35+
cls._usage_logged = True
36+
37+
@classmethod
38+
def _collect_openai_agent_package_versions(cls) -> str:
39+
"""Collect versions of relevant packages for telemetry logging.
40+
41+
Returns
42+
-------
43+
str
44+
Comma-separated list of name=version entries or "(unavailable)" if
45+
versions could not be determined.
46+
"""
47+
try:
48+
try:
49+
from importlib import metadata # Python 3.8+
50+
except ImportError: # pragma: no cover - legacy fallback
51+
import importlib_metadata as metadata # type: ignore
52+
53+
package_names = [
54+
"azure-functions-durable",
55+
"openai",
56+
"openai-agents",
57+
]
58+
59+
versions = []
60+
for package_name in package_names:
61+
try:
62+
ver = metadata.version(package_name)
63+
versions.append(f"{package_name}={ver}")
64+
except Exception: # noqa: BLE001 - swallow and continue
65+
versions.append(f"{package_name}=(not installed)")
66+
67+
return ", ".join(versions) if versions else "(unavailable)"
68+
except Exception: # noqa: BLE001 - never let version gathering break user code
69+
return "(unavailable)"
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import unittest.mock
2+
3+
4+
class TestUsageTelemetry:
5+
"""Test cases for the UsageTelemetry class."""
6+
7+
def test_log_usage_once_logs_message_on_first_call(self, capsys):
8+
"""Test that log_usage_once logs the telemetry message."""
9+
# Reset any previous state by creating a fresh import
10+
import importlib
11+
from azure.durable_functions.openai_agents import usage_telemetry
12+
importlib.reload(usage_telemetry)
13+
UsageTelemetryFresh = usage_telemetry.UsageTelemetry
14+
15+
def mock_version(package_name):
16+
if package_name == "azure-functions-durable":
17+
return "1.3.4"
18+
elif package_name == "openai":
19+
return "1.98.0"
20+
elif package_name == "openai-agents":
21+
return "0.2.5"
22+
return "unknown"
23+
24+
with unittest.mock.patch('importlib.metadata.version', side_effect=mock_version):
25+
UsageTelemetryFresh.log_usage_once()
26+
27+
captured = capsys.readouterr()
28+
assert captured.out.startswith("LanguageWorkerConsoleLog")
29+
assert "Detected OpenAI Agents SDK integration with Durable Functions." in captured.out
30+
assert "azure-functions-durable=1.3.4" in captured.out
31+
assert "openai=1.98.0" in captured.out
32+
assert "openai-agents=0.2.5" in captured.out
33+
34+
def test_log_usage_handles_package_version_errors(self, capsys):
35+
"""Test that log_usage_once handles package version lookup errors gracefully."""
36+
# Reset any previous state by creating a fresh import
37+
import importlib
38+
from azure.durable_functions.openai_agents import usage_telemetry
39+
importlib.reload(usage_telemetry)
40+
UsageTelemetryFresh = usage_telemetry.UsageTelemetry
41+
42+
# Test with mixed success/failure scenario: some packages work, others fail
43+
def mock_version(package_name):
44+
if package_name == "azure-functions-durable":
45+
return "1.3.4"
46+
elif package_name == "openai":
47+
raise Exception("Package not found")
48+
elif package_name == "openai-agents":
49+
return "0.2.5"
50+
return "unknown"
51+
52+
with unittest.mock.patch('importlib.metadata.version', side_effect=mock_version):
53+
UsageTelemetryFresh.log_usage_once()
54+
55+
captured = capsys.readouterr()
56+
assert captured.out.startswith("LanguageWorkerConsoleLog")
57+
assert "Detected OpenAI Agents SDK integration with Durable Functions." in captured.out
58+
# Should handle errors gracefully: successful packages show versions, failed ones show "(not installed)"
59+
assert "azure-functions-durable=1.3.4" in captured.out
60+
assert "openai=(not installed)" in captured.out
61+
assert "openai-agents=0.2.5" in captured.out
62+
63+
def test_log_usage_works_with_real_packages(self, capsys):
64+
"""Test that log_usage_once works with real package versions."""
65+
# Reset any previous state by creating a fresh import
66+
import importlib
67+
from azure.durable_functions.openai_agents import usage_telemetry
68+
importlib.reload(usage_telemetry)
69+
UsageTelemetryFresh = usage_telemetry.UsageTelemetry
70+
71+
# Test without mocking to see the real behavior
72+
UsageTelemetryFresh.log_usage_once()
73+
74+
captured = capsys.readouterr()
75+
assert captured.out.startswith("LanguageWorkerConsoleLog")
76+
assert "Detected OpenAI Agents SDK integration with Durable Functions." in captured.out
77+
# Should contain some version information or (unavailable)
78+
assert ("azure-functions-durable=" in captured.out or "(unavailable)" in captured.out)
79+
80+
def test_log_usage_once_is_idempotent(self, capsys):
81+
"""Test that multiple calls to log_usage_once only log once."""
82+
# Reset any previous state by creating a fresh import
83+
import importlib
84+
from azure.durable_functions.openai_agents import usage_telemetry
85+
importlib.reload(usage_telemetry)
86+
UsageTelemetryFresh = usage_telemetry.UsageTelemetry
87+
88+
with unittest.mock.patch('importlib.metadata.version', return_value="1.0.0"):
89+
# Call multiple times
90+
UsageTelemetryFresh.log_usage_once()
91+
UsageTelemetryFresh.log_usage_once()
92+
UsageTelemetryFresh.log_usage_once()
93+
94+
captured = capsys.readouterr()
95+
# Should only see one log message despite multiple calls
96+
log_count = captured.out.count("LanguageWorkerConsoleLogDetected OpenAI Agents SDK integration")
97+
assert log_count == 1

0 commit comments

Comments
 (0)