Skip to content

Commit 83a443d

Browse files
authored
Added credential authentication support for customer sdkstats (#46143)
* Added credential authentication support for customer sdkstats * Update CHANGELOG * Address feedback * Fix test * Update docstring
1 parent a54e4bc commit 83a443d

5 files changed

Lines changed: 92 additions & 11 deletions

File tree

sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
### Breaking Changes
88

99
### Bugs Fixed
10+
- Added credential authentication support for customer sdkstats
11+
([#46143](https://github.com/Azure/azure-sdk-for-python/pull/46143))
1012

1113
### Other Changes
1214

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/customer/_customer_sdkstats.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ def collect_customer_sdkstats(exporter: "BaseExporter") -> None: # type: ignore
1919
# Check if already initialized (thread-safe check)
2020
if not customer_stats.is_initialized:
2121
# The initialize method is thread-safe and handles double-initialization
22-
customer_stats.initialize(connection_string=exporter._connection_string) # type: ignore
22+
customer_stats.initialize(
23+
connection_string=exporter._connection_string, # type: ignore
24+
credential=exporter._credential, # type: ignore
25+
)
2326

2427

2528
def shutdown_customer_sdkstats_metrics() -> None:

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/customer/_manager.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,14 @@ def is_shutdown(self) -> bool:
128128
"""
129129
return self._status == CustomerSdkStatsStatus.SHUTDOWN # type: ignore
130130

131-
def initialize(self, connection_string: str) -> bool:
131+
def initialize(self, connection_string: str, credential: Optional[Any] = None) -> bool:
132132
"""Initialize Customer SDKStats collection with the provided connection string.
133133
134134
:param connection_string: Azure Monitor connection string
135135
:type connection_string: str
136+
:param credential: Token credential for AAD authentication. Defaults to None.
137+
:type credential: ~azure.core.credentials.TokenCredential or None
138+
136139
:return: True if initialization was successful, False otherwise
137140
:rtype: bool
138141
"""
@@ -147,24 +150,30 @@ def initialize(self, connection_string: str) -> bool:
147150
# Already initialized, return True
148151
return True
149152

150-
return self._do_initialize(connection_string)
153+
return self._do_initialize(connection_string, credential=credential)
151154

152-
def _do_initialize(self, connection_string: str) -> bool:
155+
def _do_initialize(self, connection_string: str, credential: Optional[Any] = None) -> bool:
153156
"""Internal initialization method.
154157
155158
:param connection_string: Azure Monitor connection string
156159
:type connection_string: str
160+
:param credential: Token credential for AAD authentication. Defaults to None.
161+
:type credential: ~azure.core.credentials.TokenCredential or None
162+
157163
:return: True if initialization was successful, False otherwise
158164
:rtype: bool
159165
"""
160166
try:
161167
# Use delayed import to avoid circular import
162168
from azure.monitor.opentelemetry.exporter.export.metrics._exporter import AzureMonitorMetricExporter
163169

164-
self._customer_sdkstats_exporter = AzureMonitorMetricExporter(
165-
connection_string=connection_string,
166-
is_customer_sdkstats=True,
167-
)
170+
exporter_kwargs: Dict[str, Any] = {
171+
"connection_string": connection_string,
172+
"is_customer_sdkstats": True,
173+
}
174+
if credential is not None:
175+
exporter_kwargs["credential"] = credential
176+
self._customer_sdkstats_exporter = AzureMonitorMetricExporter(**exporter_kwargs)
168177
metric_reader_options = {
169178
"exporter": self._customer_sdkstats_exporter,
170179
"export_interval_millis": get_customer_sdkstats_export_interval() * 1000, # Default 15m

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/customer_sdk_stats/test_customer_sdkstats.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def test_collect_customer_sdkstats(self):
4343
# Create a mock exporter
4444
mock_exporter = mock.Mock()
4545
mock_exporter._connection_string = "InstrumentationKey=12345678-1234-5678-abcd-12345678abcd"
46+
mock_exporter._credential = None
4647

4748
# Collect customer SDK stats
4849
collect_customer_sdkstats(mock_exporter)
@@ -51,6 +52,24 @@ def test_collect_customer_sdkstats(self):
5152
manager = get_customer_stats_manager()
5253
self.assertTrue(manager.is_initialized)
5354

55+
@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat.customer._customer_sdkstats.get_customer_stats_manager")
56+
def test_collect_customer_sdkstats_passes_credential(self, mock_get_manager):
57+
"""Test that credential from exporter is passed to manager.initialize()."""
58+
mock_manager = mock.Mock()
59+
mock_manager.is_initialized = False
60+
mock_get_manager.return_value = mock_manager
61+
62+
mock_exporter = mock.Mock()
63+
mock_exporter._connection_string = "InstrumentationKey=12345678-1234-5678-abcd-12345678abcd"
64+
mock_exporter._credential = mock.Mock()
65+
66+
collect_customer_sdkstats(mock_exporter)
67+
68+
mock_manager.initialize.assert_called_once_with(
69+
connection_string=mock_exporter._connection_string,
70+
credential=mock_exporter._credential,
71+
)
72+
5473
def test_collect_customer_sdkstats_multiple_calls(self):
5574
"""Test that multiple calls to collect_customer_sdkstats don't cause issues."""
5675
# Create a mock exporter

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/customer_sdk_stats/test_manager.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def setUp(self):
3636

3737
# Get a fresh manager instance
3838
self.manager = CustomerSdkStatsManager()
39+
self.mock_credential = Mock()
3940

4041
def tearDown(self):
4142
"""Clean up test environment."""
@@ -92,16 +93,20 @@ def test_initialize_success(self, mock_meter_provider, mock_metric_reader, mock_
9293

9394
connection_string = "InstrumentationKey=12345678-1234-5678-abcd-12345678abcd"
9495

95-
# Test initialization
96-
result = self.manager.initialize(connection_string)
96+
# Test initialization with credential
97+
result = self.manager.initialize(connection_string, credential=self.mock_credential)
9798

9899
self.assertTrue(result)
99100
self.assertEqual(self.manager.status, CustomerSdkStatsStatus.ACTIVE)
100101
self.assertTrue(self.manager.is_initialized)
101102
self.assertFalse(self.manager.is_shutdown)
102103

103104
# Verify mocks were called
104-
mock_exporter.assert_called_once()
105+
mock_exporter.assert_called_once_with(
106+
connection_string=connection_string,
107+
is_customer_sdkstats=True,
108+
credential=self.mock_credential,
109+
)
105110
mock_metric_reader.assert_called_once()
106111
mock_meter_provider.assert_called_once()
107112
self.assertEqual(mock_meter.create_observable_gauge.call_count, 3)
@@ -129,6 +134,49 @@ def test_initialize_empty_connection_string(self):
129134
self.assertFalse(result)
130135
self.assertEqual(self.manager.status, CustomerSdkStatsStatus.UNINITIALIZED)
131136

137+
@patch("azure.monitor.opentelemetry.exporter.export.metrics._exporter.AzureMonitorMetricExporter")
138+
@patch("azure.monitor.opentelemetry.exporter.statsbeat.customer._manager.PeriodicExportingMetricReader")
139+
@patch("azure.monitor.opentelemetry.exporter.statsbeat.customer._manager.MeterProvider")
140+
def test_initialize_with_credential(self, mock_meter_provider, mock_metric_reader, mock_exporter):
141+
"""Test that credential is passed through to the exporter during initialization."""
142+
mock_meter = Mock()
143+
mock_meter_provider_instance = Mock()
144+
mock_meter_provider_instance.get_meter.return_value = mock_meter
145+
mock_meter_provider.return_value = mock_meter_provider_instance
146+
147+
connection_string = "InstrumentationKey=12345678-1234-5678-abcd-12345678abcd"
148+
mock_credential = Mock()
149+
150+
result = self.manager.initialize(connection_string, credential=mock_credential)
151+
152+
self.assertTrue(result)
153+
self.assertTrue(self.manager.is_initialized)
154+
mock_exporter.assert_called_once_with(
155+
connection_string=connection_string,
156+
is_customer_sdkstats=True,
157+
credential=mock_credential,
158+
)
159+
160+
@patch("azure.monitor.opentelemetry.exporter.export.metrics._exporter.AzureMonitorMetricExporter")
161+
@patch("azure.monitor.opentelemetry.exporter.statsbeat.customer._manager.PeriodicExportingMetricReader")
162+
@patch("azure.monitor.opentelemetry.exporter.statsbeat.customer._manager.MeterProvider")
163+
def test_initialize_without_credential(self, mock_meter_provider, mock_metric_reader, mock_exporter):
164+
"""Test that credential is not passed to the exporter when not provided."""
165+
mock_meter = Mock()
166+
mock_meter_provider_instance = Mock()
167+
mock_meter_provider_instance.get_meter.return_value = mock_meter
168+
mock_meter_provider.return_value = mock_meter_provider_instance
169+
170+
connection_string = "InstrumentationKey=12345678-1234-5678-abcd-12345678abcd"
171+
172+
result = self.manager.initialize(connection_string)
173+
174+
self.assertTrue(result)
175+
mock_exporter.assert_called_once_with(
176+
connection_string=connection_string,
177+
is_customer_sdkstats=True,
178+
)
179+
132180
def test_initialize_multiple_calls(self):
133181
"""Test that multiple initialization calls are handled correctly."""
134182
with patch("azure.monitor.opentelemetry.exporter.export.metrics._exporter.AzureMonitorMetricExporter"), patch(

0 commit comments

Comments
 (0)