44# license information.
55# --------------------------------------------------------------------------
66import logging
7- from typing import Dict , Optional
8- from .._models import VariantAssignmentReason , 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__ )
912
1013try :
1114 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
1217
1318 HAS_AZURE_MONITOR_EVENTS_EXTENSION = True
1419except ImportError :
1520 HAS_AZURE_MONITOR_EVENTS_EXTENSION = False
16- logging .warning (
21+ logger .warning (
1722 "azure-monitor-events-extension is not installed. Telemetry will not be sent to Application Insights."
1823 )
24+ SpanProcessor = object # type: ignore
25+ Span = object # type: ignore
26+ Context = object # type: ignore
1927
2028FEATURE_NAME = "FeatureName"
2129ENABLED = "Enabled"
2230TARGETING_ID = "TargetingId"
2331VARIANT = "Variant"
2432REASON = "VariantAssignmentReason"
2533
34+ DEFAULT_WHEN_ENABLED = "DefaultWhenEnabled"
35+ VERSION = "Version"
36+ VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage"
37+ MICROSOFT_TARGETING_ID = "Microsoft.TargetingId"
38+ SPAN = "Span"
39+
2640EVENT_NAME = "FeatureEvaluation"
2741
2842EVALUATION_EVENT_VERSION = "1.1.0"
@@ -64,7 +78,7 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
6478 event : Dict [str , Optional [str ]] = {
6579 FEATURE_NAME : feature .name ,
6680 ENABLED : str (evaluation_event .enabled ),
67- "Version" : EVALUATION_EVENT_VERSION ,
81+ VERSION : EVALUATION_EVENT_VERSION ,
6882 }
6983
7084 reason = evaluation_event .reason
@@ -78,25 +92,64 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
7892 # VariantAllocationPercentage
7993 allocation_percentage = 0
8094 if reason == VariantAssignmentReason .DEFAULT_WHEN_ENABLED :
81- event ["VariantAssignmentPercentage" ] = str (100 )
95+ event [VARIANT_ASSIGNMENT_PERCENTAGE ] = str (100 )
8296 if feature .allocation :
8397 for allocation in feature .allocation .percentile :
8498 allocation_percentage += allocation .percentile_to - allocation .percentile_from
85- event ["VariantAssignmentPercentage" ] = str (100 - allocation_percentage )
99+ event [VARIANT_ASSIGNMENT_PERCENTAGE ] = str (100 - allocation_percentage )
86100 elif reason == VariantAssignmentReason .PERCENTILE :
87101 if feature .allocation and feature .allocation .percentile :
88102 for allocation in feature .allocation .percentile :
89103 if variant and allocation .variant == variant .name :
90104 allocation_percentage += allocation .percentile_to - allocation .percentile_from
91- event ["VariantAssignmentPercentage" ] = str (allocation_percentage )
105+ event [VARIANT_ASSIGNMENT_PERCENTAGE ] = str (allocation_percentage )
92106
93107 # DefaultWhenEnabled
94108 if feature .allocation and feature .allocation .default_when_enabled :
95- event ["DefaultWhenEnabled" ] = feature .allocation .default_when_enabled
109+ event [DEFAULT_WHEN_ENABLED ] = feature .allocation .default_when_enabled
96110
97111 if feature .telemetry :
98112 for metadata_key , metadata_value in feature .telemetry .metadata .items ():
99113 if metadata_key not in event :
100114 event [metadata_key ] = metadata_value
101115
102116 track_event (EVENT_NAME , evaluation_event .user , event_properties = event )
117+
118+
119+ class TargetingSpanProcessor (SpanProcessor ):
120+ """
121+ A custom SpanProcessor that attaches the targeting ID to the span and baggage when a new span is started.
122+ :keyword Callable[[], TargetingContext] targeting_context_accessor: Callback function to get the current targeting
123+ context if one isn't provided.
124+ """
125+
126+ def __init__ (self , ** kwargs : Any ) -> None :
127+ self ._targeting_context_accessor : Optional [Callable [[], TargetingContext ]] = kwargs .pop (
128+ "targeting_context_accessor" , None
129+ )
130+
131+ def on_start (self , span : Span , parent_context : Optional [Context ] = None ) -> None :
132+ """
133+ Attaches the targeting ID to the span and baggage when a new span is started.
134+
135+ :param Span span: The span that was started.
136+ :param parent_context: The parent context of the span.
137+ """
138+ if not HAS_AZURE_MONITOR_EVENTS_EXTENSION :
139+ logger .warning ("Azure Monitor Events Extension is not installed." )
140+ return
141+ if self ._targeting_context_accessor and callable (self ._targeting_context_accessor ):
142+ if inspect .iscoroutinefunction (self ._targeting_context_accessor ):
143+ logger .warning ("Async targeting_context_accessor is not supported." )
144+ return
145+ targeting_context = self ._targeting_context_accessor ()
146+ if not targeting_context or not isinstance (targeting_context , TargetingContext ):
147+ logger .warning (
148+ "targeting_context_accessor did not return a TargetingContext. Received type %s." ,
149+ type (targeting_context ),
150+ )
151+ return
152+ if not targeting_context .user_id :
153+ logger .debug ("TargetingContext does not have a user ID." )
154+ return
155+ span .set_attribute (TARGETING_ID , targeting_context .user_id )
0 commit comments