44# license information.
55# --------------------------------------------------------------------------
66import 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__ )
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+
2639EVENT_NAME = "FeatureEvaluation"
2740
2841EVALUATION_EVENT_VERSION = "1.0.0"
@@ -64,7 +77,7 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
6477 event : Dict [str , Optional [str ]] = {
6578 FEATURE_NAME : feature .name ,
6679 ENABLED : str (evaluation_event .enabled ),
67- "Version" : EVALUATION_EVENT_VERSION ,
80+ VERSION : EVALUATION_EVENT_VERSION ,
6881 }
6982
7083 reason = evaluation_event .reason
@@ -75,9 +88,67 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
7588 if variant :
7689 event [VARIANT ] = variant .name
7790
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+
78110 if feature .telemetry :
79111 for metadata_key , metadata_value in feature .telemetry .metadata .items ():
80112 if metadata_key not in event :
81113 event [metadata_key ] = metadata_value
82114
83115 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