Skip to content

Commit df9272b

Browse files
committed
updated telemetry event dependency to opentelemetry LoggingHandler
1 parent af5de3a commit df9272b

3 files changed

Lines changed: 116 additions & 59 deletions

File tree

featuremanagement/azuremonitor/_send_telemetry.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,38 @@
1212

1313
logger = logging.getLogger(__name__)
1414

15+
# Set up a separate logger for feature-evaluation telemetry events to avoid propagating to the root logger
16+
_event_logger = logging.getLogger(__name__ + ".events")
17+
_event_logger.propagate = False
18+
1519
try:
16-
from azure.monitor.events.extension import track_event as azure_monitor_track_event # type: ignore
17-
from opentelemetry.context.context import Context
18-
from opentelemetry.sdk.trace import Span, SpanProcessor
20+
from logging import INFO
21+
from opentelemetry.instrumentation.logging.handler import LoggingHandler
22+
from opentelemetry.sdk.trace import SpanProcessor
1923

20-
HAS_AZURE_MONITOR_EVENTS_EXTENSION = True
24+
HAS_OPENTELEMETRY_LOGGING = True
2125
except ImportError:
22-
HAS_AZURE_MONITOR_EVENTS_EXTENSION = False
23-
logger.warning(
24-
"azure-monitor-events-extension is not installed. Telemetry will not be sent to Application Insights."
25-
)
26+
HAS_OPENTELEMETRY_LOGGING = False
27+
LoggingHandler = object # type: ignore
2628
SpanProcessor = object # type: ignore
27-
Span = object # type: ignore
28-
Context = object # type: ignore
29+
30+
31+
_events_logger_initialized = False
32+
33+
def _initialize_event_logger() -> None:
34+
global _events_logger_initialized
35+
if _events_logger_initialized:
36+
return
37+
38+
if not HAS_OPENTELEMETRY_LOGGING:
39+
logger.warning(
40+
"OpenTelemetry logging handler is not installed. Telemetry will not be sent to Application Insights."
41+
)
42+
return
43+
44+
_event_logger.addHandler(LoggingHandler())
45+
_event_logger.setLevel(INFO)
46+
_events_logger_initialized = True
2947

3048
FEATURE_NAME = "FeatureName"
3149
ENABLED = "Enabled"
@@ -51,15 +69,22 @@ def track_event(event_name: str, user: str, event_properties: Optional[Dict[str,
5169
:param str user: The user ID to associate with the event.
5270
:param dict[str, str] event_properties: A dictionary of named string properties.
5371
"""
54-
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
72+
if not HAS_OPENTELEMETRY_LOGGING:
5573
return
5674

75+
_initialize_event_logger()
76+
5777
event_properties = event_properties or {}
5878

5979
if user:
6080
event_properties[TARGETING_ID] = user
6181

62-
azure_monitor_track_event(event_name, event_properties)
82+
# Azure Monitor exporter maps this attribute to customEvent telemetry name.
83+
custom_event_attributes = {
84+
**event_properties,
85+
"microsoft.custom_event.name": event_name,
86+
}
87+
_event_logger.info(event_name, extra=custom_event_attributes)
6388

6489

6590
def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
@@ -68,7 +93,7 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
6893
6994
:param EvaluationEvent evaluation_event: The evaluation event to publish telemetry for.
7095
"""
71-
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
96+
if not HAS_OPENTELEMETRY_LOGGING:
7297
return
7398

7499
feature = evaluation_event.feature
@@ -129,15 +154,15 @@ def __init__(self, **kwargs: Any) -> None:
129154
"targeting_context_accessor", None
130155
)
131156

132-
def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None: # pylint: disable=unused-argument
157+
def on_start(self, span: Any, parent_context: Optional[Any] = None) -> None: # pylint: disable=unused-argument
133158
"""
134159
Attaches the targeting ID to the span and baggage when a new span is started.
135160
136161
:param Span span: The span that was started.
137162
:param parent_context: The parent context of the span.
138163
"""
139-
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
140-
logger.warning("Azure Monitor Events Extension is not installed.")
164+
if not HAS_OPENTELEMETRY_LOGGING:
165+
logger.warning("OpenTelemetry logging handler is not installed.")
141166
return
142167
if self._targeting_context_accessor and callable(self._targeting_context_accessor):
143168
if inspect.iscoroutinefunction(self._targeting_context_accessor):

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,9 @@ Homepage = "https://github.com/microsoft/FeatureManagement-Python"
5858
Issues = "https://github.com/microsoft/FeatureManagement-Python/issues"
5959

6060
[project.optional-dependencies]
61-
AzureMonitor = ["azure-monitor-events-extension<2.0.0"]
61+
AzureMonitor = ["azure-monitor-opentelemetry < 2.0.0"]
6262
test = [
6363
"azure-monitor-opentelemetry < 2.0.0",
64-
"azure-monitor-events-extension < 2.0.0",
6564
]
6665
dev = [
6766
"pytest >= 7.0.0, < 10.0.0",
@@ -76,4 +75,5 @@ dev = [
7675
"myst_parser >= 2.0.0, < 6.0.0",
7776
"opentelemetry-api >= 1.20.0, < 2.0.0",
7877
"opentelemetry-sdk >= 1.20.0, < 2.0.0",
78+
"opentelemetry-instrumentation-logging >= 0.40b0, < 1.0.0",
7979
]

tests/test_send_telemetry_appinsights.py

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
from featuremanagement.azuremonitor import TargetingSpanProcessor
1414

1515

16+
def _event_properties(mock_track_event):
17+
return mock_track_event.call_args.kwargs["event_properties"]
18+
19+
1620
@pytest.mark.usefixtures("caplog")
1721
class TestSendTelemetryAppinsights:
1822

@@ -40,23 +44,45 @@ def test_send_telemetry_appinsights(self):
4044
evaluation_event.variant = variant
4145
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_DISABLED
4246

43-
with patch("featuremanagement.azuremonitor._send_telemetry.azure_monitor_track_event") as mock_track_event:
47+
with patch("featuremanagement.azuremonitor._send_telemetry.track_event") as mock_track_event:
4448
# This is called like this so we can override the track_event function
4549
featuremanagement.azuremonitor._send_telemetry.publish_telemetry( # pylint: disable=protected-access
4650
evaluation_event
4751
)
4852
mock_track_event.assert_called_once()
4953
assert mock_track_event.call_args[0][0] == "FeatureEvaluation"
50-
assert mock_track_event.call_args[0][1]["FeatureName"] == "TestFeature"
51-
assert mock_track_event.call_args[0][1]["Enabled"] == "True"
52-
assert mock_track_event.call_args[0][1]["TargetingId"] == "test_user"
53-
assert mock_track_event.call_args[0][1]["Variant"] == "TestVariant"
54-
assert mock_track_event.call_args[0][1]["ETag"] == "cmwBRcIAq1jUyKL3Kj8bvf9jtxBrFg-R-ayExStMC90"
54+
assert mock_track_event.call_args[0][1] == "test_user"
55+
event_properties = _event_properties(mock_track_event)
56+
assert event_properties["FeatureName"] == "TestFeature"
57+
assert event_properties["Enabled"] == "True"
58+
assert event_properties["Variant"] == "TestVariant"
59+
assert event_properties["ETag"] == "cmwBRcIAq1jUyKL3Kj8bvf9jtxBrFg-R-ayExStMC90"
5560
assert (
56-
mock_track_event.call_args[0][1]["FeatureFlagReference"]
61+
event_properties["FeatureFlagReference"]
5762
== "fake-store-uri/kv/.appconfig.featureflag/TestFeature"
5863
)
59-
assert mock_track_event.call_args[0][1]["FeatureFlagId"] == "fake-feature-flag-id"
64+
assert event_properties["FeatureFlagId"] == "fake-feature-flag-id"
65+
66+
def test_track_event_preserves_reserved_custom_event_name(self):
67+
with patch("featuremanagement.azuremonitor._send_telemetry._initialize_event_logger"), patch(
68+
"featuremanagement.azuremonitor._send_telemetry._event_logger.info"
69+
) as mock_event_logger_info, patch(
70+
"featuremanagement.azuremonitor._send_telemetry.HAS_OPENTELEMETRY_LOGGING", True
71+
):
72+
featuremanagement.azuremonitor._send_telemetry.track_event(
73+
"FeatureEvaluation",
74+
"test_user",
75+
{
76+
"microsoft.custom_event.name": "override_attempt",
77+
"CustomProperty": "custom_value",
78+
},
79+
)
80+
81+
mock_event_logger_info.assert_called_once()
82+
assert mock_event_logger_info.call_args[0][0] == "FeatureEvaluation"
83+
assert mock_event_logger_info.call_args.kwargs["extra"]["microsoft.custom_event.name"] == "FeatureEvaluation"
84+
assert mock_event_logger_info.call_args.kwargs["extra"]["CustomProperty"] == "custom_value"
85+
assert mock_event_logger_info.call_args.kwargs["extra"]["TargetingId"] == "test_user"
6086

6187
def test_send_telemetry_appinsights_no_user(self):
6288
feature_flag = FeatureFlag.convert_from_json({"id": "TestFeature"})
@@ -67,18 +93,20 @@ def test_send_telemetry_appinsights_no_user(self):
6793
evaluation_event.variant = variant
6894
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_DISABLED
6995

70-
with patch("featuremanagement.azuremonitor._send_telemetry.azure_monitor_track_event") as mock_track_event:
96+
with patch("featuremanagement.azuremonitor._send_telemetry.track_event") as mock_track_event:
7197
# This is called like this so we can override the track_event function
7298
featuremanagement.azuremonitor._send_telemetry.publish_telemetry( # pylint: disable=protected-access
7399
evaluation_event
74100
)
75101
mock_track_event.assert_called_once()
76102
assert mock_track_event.call_args[0][0] == "FeatureEvaluation"
77-
assert mock_track_event.call_args[0][1]["FeatureName"] == "TestFeature"
78-
assert mock_track_event.call_args[0][1]["Enabled"] == "False"
79-
assert "TargetingId" not in mock_track_event.call_args[0][1]
80-
assert mock_track_event.call_args[0][1]["Variant"] == "TestVariant"
81-
assert mock_track_event.call_args[0][1]["VariantAssignmentReason"] == "DefaultWhenDisabled"
103+
assert mock_track_event.call_args[0][1] == ""
104+
event_properties = _event_properties(mock_track_event)
105+
assert event_properties["FeatureName"] == "TestFeature"
106+
assert event_properties["Enabled"] == "False"
107+
assert "TargetingId" not in event_properties
108+
assert event_properties["Variant"] == "TestVariant"
109+
assert event_properties["VariantAssignmentReason"] == "DefaultWhenDisabled"
82110

83111
def test_send_telemetry_appinsights_no_variant(self):
84112
feature_flag = FeatureFlag.convert_from_json({"id": "TestFeature"})
@@ -87,25 +115,26 @@ def test_send_telemetry_appinsights_no_variant(self):
87115
evaluation_event.enabled = True
88116
evaluation_event.user = "test_user"
89117

90-
with patch("featuremanagement.azuremonitor._send_telemetry.azure_monitor_track_event") as mock_track_event:
118+
with patch("featuremanagement.azuremonitor._send_telemetry.track_event") as mock_track_event:
91119
# This is called like this so we can override the track_event function
92120
featuremanagement.azuremonitor._send_telemetry.publish_telemetry( # pylint: disable=protected-access
93121
evaluation_event
94122
)
95123
mock_track_event.assert_called_once()
96124
assert mock_track_event.call_args[0][0] == "FeatureEvaluation"
97-
assert mock_track_event.call_args[0][1]["FeatureName"] == "TestFeature"
98-
assert mock_track_event.call_args[0][1]["Enabled"] == "True"
99-
assert mock_track_event.call_args[0][1]["TargetingId"] == "test_user"
100-
assert "Variant" not in mock_track_event.call_args[0][1]
101-
assert "Reason" not in mock_track_event.call_args[0][1]
125+
assert mock_track_event.call_args[0][1] == "test_user"
126+
event_properties = _event_properties(mock_track_event)
127+
assert event_properties["FeatureName"] == "TestFeature"
128+
assert event_properties["Enabled"] == "True"
129+
assert "Variant" not in event_properties
130+
assert "Reason" not in event_properties
102131

103132
def test_send_telemetry_appinsights_no_feature_flag(self):
104133
evaluation_event = EvaluationEvent(None)
105134
evaluation_event.enabled = True
106135
evaluation_event.user = "test_user"
107136

108-
with patch("featuremanagement.azuremonitor._send_telemetry.azure_monitor_track_event") as mock_track_event:
137+
with patch("featuremanagement.azuremonitor._send_telemetry.track_event") as mock_track_event:
109138
# This is called like this so we can override the track_event function
110139
featuremanagement.azuremonitor._send_telemetry.publish_telemetry( # pylint: disable=protected-access
111140
evaluation_event
@@ -130,18 +159,19 @@ def test_send_telemetry_appinsights_default_when_enabled(self):
130159
evaluation_event.variant = variant
131160
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_ENABLED
132161

133-
with patch("featuremanagement.azuremonitor._send_telemetry.azure_monitor_track_event") as mock_track_event:
162+
with patch("featuremanagement.azuremonitor._send_telemetry.track_event") as mock_track_event:
134163
# This is called like this so we can override the track_event function
135164
featuremanagement.azuremonitor._send_telemetry.publish_telemetry( # pylint: disable=protected-access
136165
evaluation_event
137166
)
138167
mock_track_event.assert_called_once()
139168
assert mock_track_event.call_args[0][0] == "FeatureEvaluation"
140-
assert mock_track_event.call_args[0][1]["FeatureName"] == "TestFeature"
141-
assert mock_track_event.call_args[0][1]["Enabled"] == "True"
142-
assert mock_track_event.call_args[0][1]["TargetingId"] == "test_user"
143-
assert mock_track_event.call_args[0][1]["Variant"] == "big"
144-
assert mock_track_event.call_args[0][1]["VariantAssignmentReason"] == "DefaultWhenEnabled"
169+
assert mock_track_event.call_args[0][1] == "test_user"
170+
event_properties = _event_properties(mock_track_event)
171+
assert event_properties["FeatureName"] == "TestFeature"
172+
assert event_properties["Enabled"] == "True"
173+
assert event_properties["Variant"] == "big"
174+
assert event_properties["VariantAssignmentReason"] == "DefaultWhenEnabled"
145175

146176
def test_send_telemetry_appinsights_default_when_enabled_no_percentile(self):
147177
feature_flag = FeatureFlag.convert_from_json(
@@ -160,18 +190,19 @@ def test_send_telemetry_appinsights_default_when_enabled_no_percentile(self):
160190
evaluation_event.variant = variant
161191
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_ENABLED
162192

163-
with patch("featuremanagement.azuremonitor._send_telemetry.azure_monitor_track_event") as mock_track_event:
193+
with patch("featuremanagement.azuremonitor._send_telemetry.track_event") as mock_track_event:
164194
# This is called like this so we can override the track_event function
165195
featuremanagement.azuremonitor._send_telemetry.publish_telemetry( # pylint: disable=protected-access
166196
evaluation_event
167197
)
168198
mock_track_event.assert_called_once()
169199
assert mock_track_event.call_args[0][0] == "FeatureEvaluation"
170-
assert mock_track_event.call_args[0][1]["FeatureName"] == "TestFeature"
171-
assert mock_track_event.call_args[0][1]["Enabled"] == "True"
172-
assert mock_track_event.call_args[0][1]["TargetingId"] == "test_user"
173-
assert mock_track_event.call_args[0][1]["Variant"] == "big"
174-
assert mock_track_event.call_args[0][1]["VariantAssignmentReason"] == "DefaultWhenEnabled"
200+
assert mock_track_event.call_args[0][1] == "test_user"
201+
event_properties = _event_properties(mock_track_event)
202+
assert event_properties["FeatureName"] == "TestFeature"
203+
assert event_properties["Enabled"] == "True"
204+
assert event_properties["Variant"] == "big"
205+
assert event_properties["VariantAssignmentReason"] == "DefaultWhenEnabled"
175206

176207
def test_send_telemetry_appinsights_allocation(self):
177208
feature_flag = FeatureFlag.convert_from_json(
@@ -190,20 +221,21 @@ def test_send_telemetry_appinsights_allocation(self):
190221
evaluation_event.variant = variant
191222
evaluation_event.reason = VariantAssignmentReason.PERCENTILE
192223

193-
with patch("featuremanagement.azuremonitor._send_telemetry.azure_monitor_track_event") as mock_track_event:
224+
with patch("featuremanagement.azuremonitor._send_telemetry.track_event") as mock_track_event:
194225
# This is called like this so we can override the track_event function
195226
featuremanagement.azuremonitor._send_telemetry.publish_telemetry( # pylint: disable=protected-access
196227
evaluation_event
197228
)
198229
mock_track_event.assert_called_once()
199230
assert mock_track_event.call_args[0][0] == "FeatureEvaluation"
200-
assert mock_track_event.call_args[0][1]["FeatureName"] == "TestFeature"
201-
assert mock_track_event.call_args[0][1]["Enabled"] == "True"
202-
assert mock_track_event.call_args[0][1]["TargetingId"] == "test_user"
203-
assert mock_track_event.call_args[0][1]["Variant"] == "big"
204-
assert mock_track_event.call_args[0][1]["VariantAssignmentReason"] == "Percentile"
205-
assert mock_track_event.call_args[0][1]["VariantAssignmentPercentage"] == "25"
206-
assert "DefaultWhenEnabled" not in mock_track_event.call_args[0][1]
231+
assert mock_track_event.call_args[0][1] == "test_user"
232+
event_properties = _event_properties(mock_track_event)
233+
assert event_properties["FeatureName"] == "TestFeature"
234+
assert event_properties["Enabled"] == "True"
235+
assert event_properties["Variant"] == "big"
236+
assert event_properties["VariantAssignmentReason"] == "Percentile"
237+
assert event_properties["VariantAssignmentPercentage"] == "25"
238+
assert "DefaultWhenEnabled" not in event_properties
207239

208240
def test_targeting_span_processor(self, caplog):
209241
processor = TargetingSpanProcessor()

0 commit comments

Comments
 (0)