4
4
# license information.
5
5
# --------------------------------------------------------------------------
6
6
import logging
7
- from typing import Dict , Optional
8
- from .._models import EvaluationEvent
7
+ import inspect
8
+ from typing import Any , Callable , Dict , Optional
9
+ from .._models import VariantAssignmentReason , EvaluationEvent , TargetingContext
10
+
11
+ logger = logging .getLogger (__name__ )
9
12
10
13
try :
11
14
from azure .monitor .events .extension import track_event as azure_monitor_track_event # type: ignore
15
+ from opentelemetry .context .context import Context
16
+ from opentelemetry .sdk .trace import Span , SpanProcessor
12
17
13
18
HAS_AZURE_MONITOR_EVENTS_EXTENSION = True
14
19
except ImportError :
15
20
HAS_AZURE_MONITOR_EVENTS_EXTENSION = False
16
- logging .warning (
21
+ logger .warning (
17
22
"azure-monitor-events-extension is not installed. Telemetry will not be sent to Application Insights."
18
23
)
24
+ SpanProcessor = object # type: ignore
25
+ Span = object # type: ignore
26
+ Context = object # type: ignore
19
27
20
28
FEATURE_NAME = "FeatureName"
21
29
ENABLED = "Enabled"
22
30
TARGETING_ID = "TargetingId"
23
31
VARIANT = "Variant"
24
32
REASON = "VariantAssignmentReason"
25
33
34
+ DEFAULT_WHEN_ENABLED = "DefaultWhenEnabled"
35
+ VERSION = "Version"
36
+ VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage"
37
+ MICROSOFT_TARGETING_ID = "Microsoft.TargetingId"
38
+
26
39
EVENT_NAME = "FeatureEvaluation"
27
40
28
41
EVALUATION_EVENT_VERSION = "1.0.0"
@@ -64,7 +77,7 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
64
77
event : Dict [str , Optional [str ]] = {
65
78
FEATURE_NAME : feature .name ,
66
79
ENABLED : str (evaluation_event .enabled ),
67
- "Version" : EVALUATION_EVENT_VERSION ,
80
+ VERSION : EVALUATION_EVENT_VERSION ,
68
81
}
69
82
70
83
reason = evaluation_event .reason
@@ -75,9 +88,67 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
75
88
if variant :
76
89
event [VARIANT ] = variant .name
77
90
91
+ # VariantAllocationPercentage
92
+ allocation_percentage = 0
93
+ if reason == VariantAssignmentReason .DEFAULT_WHEN_ENABLED :
94
+ event [VARIANT_ASSIGNMENT_PERCENTAGE ] = str (100 )
95
+ if feature .allocation :
96
+ for allocation in feature .allocation .percentile :
97
+ allocation_percentage += allocation .percentile_to - allocation .percentile_from
98
+ event [VARIANT_ASSIGNMENT_PERCENTAGE ] = str (100 - allocation_percentage )
99
+ elif reason == VariantAssignmentReason .PERCENTILE :
100
+ if feature .allocation and feature .allocation .percentile :
101
+ for allocation in feature .allocation .percentile :
102
+ if variant and allocation .variant == variant .name :
103
+ allocation_percentage += allocation .percentile_to - allocation .percentile_from
104
+ event [VARIANT_ASSIGNMENT_PERCENTAGE ] = str (allocation_percentage )
105
+
106
+ # DefaultWhenEnabled
107
+ if feature .allocation and feature .allocation .default_when_enabled :
108
+ event [DEFAULT_WHEN_ENABLED ] = feature .allocation .default_when_enabled
109
+
78
110
if feature .telemetry :
79
111
for metadata_key , metadata_value in feature .telemetry .metadata .items ():
80
112
if metadata_key not in event :
81
113
event [metadata_key ] = metadata_value
82
114
83
115
track_event (EVENT_NAME , evaluation_event .user , event_properties = event )
116
+
117
+
118
+ class TargetingSpanProcessor (SpanProcessor ):
119
+ """
120
+ A custom SpanProcessor that attaches the targeting ID to the span and baggage when a new span is started.
121
+ :keyword Callable[[], TargetingContext] targeting_context_accessor: Callback function to get the current targeting
122
+ context if one isn't provided.
123
+ """
124
+
125
+ def __init__ (self , ** kwargs : Any ) -> None :
126
+ self ._targeting_context_accessor : Optional [Callable [[], TargetingContext ]] = kwargs .pop (
127
+ "targeting_context_accessor" , None
128
+ )
129
+
130
+ def on_start (self , span : Span , parent_context : Optional [Context ] = None ) -> None :
131
+ """
132
+ Attaches the targeting ID to the span and baggage when a new span is started.
133
+
134
+ :param Span span: The span that was started.
135
+ :param parent_context: The parent context of the span.
136
+ """
137
+ if not HAS_AZURE_MONITOR_EVENTS_EXTENSION :
138
+ logger .warning ("Azure Monitor Events Extension is not installed." )
139
+ return
140
+ if self ._targeting_context_accessor and callable (self ._targeting_context_accessor ):
141
+ if inspect .iscoroutinefunction (self ._targeting_context_accessor ):
142
+ logger .warning ("Async targeting_context_accessor is not supported." )
143
+ return
144
+ targeting_context = self ._targeting_context_accessor ()
145
+ if not targeting_context or not isinstance (targeting_context , TargetingContext ):
146
+ logger .warning (
147
+ "targeting_context_accessor did not return a TargetingContext. Received type %s." ,
148
+ type (targeting_context ),
149
+ )
150
+ return
151
+ if not targeting_context .user_id :
152
+ logger .debug ("TargetingContext does not have a user ID." )
153
+ return
154
+ span .set_attribute (TARGETING_ID , targeting_context .user_id )
0 commit comments