Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ sphinx
sphinx_rtd_theme
sphinx-toolbox
myst_parser
opentelemetry-api
opentelemetry-sdk
4 changes: 3 additions & 1 deletion featuremanagement/azuremonitor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# -------------------------------------------------------------------------
from ._send_telemetry import publish_telemetry, track_event
from ._send_telemetry import publish_telemetry, track_event, attach_targeting_info, TargetingSpanProcessor


__all__ = [
"publish_telemetry",
"track_event",
"attach_targeting_info",
"TargetingSpanProcessor",
]
53 changes: 48 additions & 5 deletions featuremanagement/azuremonitor/_send_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,32 @@

try:
from azure.monitor.events.extension import track_event as azure_monitor_track_event # type: ignore
from opentelemetry import trace, baggage, context
from opentelemetry.context.context import Context
from opentelemetry.sdk.trace import Span, SpanProcessor

HAS_AZURE_MONITOR_EVENTS_EXTENSION = True
except ImportError:
HAS_AZURE_MONITOR_EVENTS_EXTENSION = False
logging.warning(
"azure-monitor-events-extension is not installed. Telemetry will not be sent to Application Insights."
)
SpanProcessor = object # type: ignore
Span = object # type: ignore
Context = object # type: ignore

FEATURE_NAME = "FeatureName"
ENABLED = "Enabled"
TARGETING_ID = "TargetingId"
VARIANT = "Variant"
REASON = "VariantAssignmentReason"

DEFAULT_WHEN_ENABLED = "DefaultWhenEnabled"
VERSION = "Version"
VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage"
MICROSOFT_TARGETING_ID = "Microsoft.TargetingId"
SPAN = "Span"

EVENT_NAME = "FeatureEvaluation"

EVALUATION_EVENT_VERSION = "1.1.0"
Expand Down Expand Up @@ -64,7 +76,7 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
event: Dict[str, Optional[str]] = {
FEATURE_NAME: feature.name,
ENABLED: str(evaluation_event.enabled),
"Version": EVALUATION_EVENT_VERSION,
VERSION: EVALUATION_EVENT_VERSION,
}

reason = evaluation_event.reason
Expand All @@ -78,25 +90,56 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
# VariantAllocationPercentage
allocation_percentage = 0
if reason == VariantAssignmentReason.DEFAULT_WHEN_ENABLED:
event["VariantAssignmentPercentage"] = str(100)
event[VARIANT_ASSIGNMENT_PERCENTAGE] = str(100)
if feature.allocation:
for allocation in feature.allocation.percentile:
allocation_percentage += allocation.percentile_to - allocation.percentile_from
event["VariantAssignmentPercentage"] = str(100 - allocation_percentage)
event[VARIANT_ASSIGNMENT_PERCENTAGE] = str(100 - allocation_percentage)
elif reason == VariantAssignmentReason.PERCENTILE:
if feature.allocation and feature.allocation.percentile:
for allocation in feature.allocation.percentile:
if variant and allocation.variant == variant.name:
allocation_percentage += allocation.percentile_to - allocation.percentile_from
event["VariantAssignmentPercentage"] = str(allocation_percentage)
event[VARIANT_ASSIGNMENT_PERCENTAGE] = str(allocation_percentage)

# DefaultWhenEnabled
if feature.allocation and feature.allocation.default_when_enabled:
event["DefaultWhenEnabled"] = feature.allocation.default_when_enabled
event[DEFAULT_WHEN_ENABLED] = feature.allocation.default_when_enabled

if feature.telemetry:
for metadata_key, metadata_value in feature.telemetry.metadata.items():
if metadata_key not in event:
event[metadata_key] = metadata_value

track_event(EVENT_NAME, evaluation_event.user, event_properties=event)


def attach_targeting_info(targeting_id: str) -> None:
"""
Attaches the targeting ID to the current span and baggage.

:param str targeting_id: The targeting ID to attach.
"""
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
return
context.attach(baggage.set_baggage(MICROSOFT_TARGETING_ID, targeting_id))
trace.get_current_span().set_attribute(TARGETING_ID, targeting_id)


class TargetingSpanProcessor(SpanProcessor):
"""
A custom SpanProcessor that attaches the targeting ID to the span and baggage when a new span is started.
"""

def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
"""
Attaches the targeting ID to the span and baggage when a new span is started.

:param Span span: The span that was started.
:param parent_context: The parent context of the span.
"""
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
return
target_baggage = baggage.get_baggage(MICROSOFT_TARGETING_ID, parent_context)
if target_baggage is not None and isinstance(target_baggage, str):
span.set_attribute(TARGETING_ID, target_baggage)
2 changes: 1 addition & 1 deletion tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
azure-monitor-opentelemetry
azure-monitor-events-extension
azure-monitor-events-extension