From 2f80ab2094aea7d8b7d63ee8635dc15d89af4773 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 14:45:45 -0800 Subject: [PATCH 01/18] New OTel Integration --- featuremanagement/azuremonitor/__init__.py | 4 ++- .../azuremonitor/_send_telemetry.py | 29 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/featuremanagement/azuremonitor/__init__.py b/featuremanagement/azuremonitor/__init__.py index 22099f5..d4b7310 100644 --- a/featuremanagement/azuremonitor/__init__.py +++ b/featuremanagement/azuremonitor/__init__.py @@ -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", ] diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index a48628c..39d4c77 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -5,10 +5,12 @@ # -------------------------------------------------------------------------- import logging from typing import Dict, Optional -from .._models import VariantAssignmentReason, EvaluationEvent +from .._models import VariantAssignmentReason, EvaluationEvent, TargetingContext try: from azure.monitor.events.extension import track_event as azure_monitor_track_event # type: ignore + from opentelemetry import trace, baggage, context + from opentelemetry.sdk.trace import Span, SpanProcessor HAS_AZURE_MONITOR_EVENTS_EXTENSION = True except ImportError: @@ -23,6 +25,12 @@ VARIANT = "Variant" REASON = "VariantAssignmentReason" +DefaultWhenEnabled = "DefaultWhenEnabled" +VERSION = "Version" +VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage" +Microsoft_TargetingId = "Microsoft.TargetingId" +SPAN = "Span" + EVENT_NAME = "FeatureEvaluation" EVALUATION_EVENT_VERSION = "1.1.0" @@ -64,7 +72,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 @@ -78,21 +86,21 @@ 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[DefaultWhenEnabled] = feature.allocation.default_when_enabled if feature.telemetry: for metadata_key, metadata_value in feature.telemetry.metadata.items(): @@ -100,3 +108,12 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None: event[metadata_key] = metadata_value track_event(EVENT_NAME, evaluation_event.user, event_properties=event) + +def attach_targeting_info(targeting_id: str): + context.attach(baggage.set_baggage(Microsoft_TargetingId, targeting_id)) + trace.get_current_span().set_attribute(TARGETING_ID, targeting_id) + +class TargetingSpanProcessor(SpanProcessor): + def on_start(self, span: Span, parent_context = None): + if (baggage.get_baggage(Microsoft_TargetingId, parent_context) != None): + span.set_attribute(TARGETING_ID, baggage.get_baggage(Microsoft_TargetingId, parent_context)) \ No newline at end of file From 70f6a2c367ae9e93a1a6fc8e2c332334aa3b3373 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:01:32 -0800 Subject: [PATCH 02/18] fixing issues --- .../azuremonitor/_send_telemetry.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 39d4c77..6b11115 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -25,10 +25,10 @@ VARIANT = "Variant" REASON = "VariantAssignmentReason" -DefaultWhenEnabled = "DefaultWhenEnabled" +DEFAULT_WHEN_ENABLED = "DefaultWhenEnabled" VERSION = "Version" VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage" -Microsoft_TargetingId = "Microsoft.TargetingId" +MICROSOFT_TARGETING_ID = "Microsoft.TargetingId" SPAN = "Span" EVENT_NAME = "FeatureEvaluation" @@ -100,7 +100,7 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None: # 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(): @@ -110,10 +110,26 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None: track_event(EVENT_NAME, evaluation_event.user, event_properties=event) def attach_targeting_info(targeting_id: str): - context.attach(baggage.set_baggage(Microsoft_TargetingId, targeting_id)) + """ + Attaches the targeting ID to the current span and baggage. + + :param str targeting_id: The targeting ID to attach. + """ + context.attach(baggage.set_baggage(MICROSOFT_TARGETING_ID, targeting_id)) trace.get_current_span().set_attribute(TARGETING_ID, targeting_id) +""" +This class is a custom SpanProcessor that attaches the targeting ID to the span and baggage when a new span is started. +""" class TargetingSpanProcessor(SpanProcessor): + def on_start(self, span: Span, parent_context = None): - if (baggage.get_baggage(Microsoft_TargetingId, parent_context) != None): - span.set_attribute(TARGETING_ID, baggage.get_baggage(Microsoft_TargetingId, parent_context)) \ No newline at end of file + """ + 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. + """ + target_baggage = baggage.get_baggage(MICROSOFT_TARGETING_ID, parent_context) + if (target_baggage != None): + span.set_attribute(TARGETING_ID, target_baggage) From 959081a01dcbcc8e16a80d424852abe6a00f06c8 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:07:17 -0800 Subject: [PATCH 03/18] pylint fixes --- .../azuremonitor/_send_telemetry.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 6b11115..7da034b 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- import logging from typing import Dict, Optional -from .._models import VariantAssignmentReason, EvaluationEvent, TargetingContext +from .._models import VariantAssignmentReason, EvaluationEvent try: from azure.monitor.events.extension import track_event as azure_monitor_track_event # type: ignore @@ -109,6 +109,7 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None: track_event(EVENT_NAME, evaluation_event.user, event_properties=event) + def attach_targeting_info(targeting_id: str): """ Attaches the targeting ID to the current span and baggage. @@ -118,18 +119,19 @@ def attach_targeting_info(targeting_id: str): context.attach(baggage.set_baggage(MICROSOFT_TARGETING_ID, targeting_id)) trace.get_current_span().set_attribute(TARGETING_ID, targeting_id) -""" -This class is a custom SpanProcessor that attaches the targeting ID to the span and baggage when a new span is started. -""" + 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 = None): + def on_start(self, span: Span, parent_context=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. """ target_baggage = baggage.get_baggage(MICROSOFT_TARGETING_ID, parent_context) - if (target_baggage != None): + if target_baggage is not None: span.set_attribute(TARGETING_ID, target_baggage) From c6122785252c0f4fbb01d213caef1c671cd1e0f9 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:27:01 -0800 Subject: [PATCH 04/18] fixing mypy issues --- featuremanagement/azuremonitor/_send_telemetry.py | 8 +++++--- tests/requirements.txt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 7da034b..7ddf546 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -10,7 +10,9 @@ 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 + from opentelemetry.util import types HAS_AZURE_MONITOR_EVENTS_EXTENSION = True except ImportError: @@ -110,7 +112,7 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None: track_event(EVENT_NAME, evaluation_event.user, event_properties=event) -def attach_targeting_info(targeting_id: str): +def attach_targeting_info(targeting_id: str) -> None: """ Attaches the targeting ID to the current span and baggage. @@ -125,7 +127,7 @@ 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=None): + 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. @@ -133,5 +135,5 @@ def on_start(self, span: Span, parent_context=None): :param parent_context: The parent context of the span. """ target_baggage = baggage.get_baggage(MICROSOFT_TARGETING_ID, parent_context) - if target_baggage is not None: + if target_baggage is not None and isinstance(target_baggage, str): span.set_attribute(TARGETING_ID, target_baggage) diff --git a/tests/requirements.txt b/tests/requirements.txt index b91565e..f52da80 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1,2 @@ azure-monitor-opentelemetry -azure-monitor-events-extension +azure-monitor-events-extension \ No newline at end of file From 8743d229b2dffa06d24b8d39bbbcde4af071a74f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:28:38 -0800 Subject: [PATCH 05/18] Update _send_telemetry.py --- featuremanagement/azuremonitor/_send_telemetry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 7ddf546..01641d1 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -12,7 +12,6 @@ from opentelemetry import trace, baggage, context from opentelemetry.context.context import Context from opentelemetry.sdk.trace import Span, SpanProcessor - from opentelemetry.util import types HAS_AZURE_MONITOR_EVENTS_EXTENSION = True except ImportError: From fd663ef5613f69d8f59734d7881ccec432fbc4ca Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:29:52 -0800 Subject: [PATCH 06/18] Update _send_telemetry.py --- featuremanagement/azuremonitor/_send_telemetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 01641d1..64d81c4 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -126,7 +126,7 @@ 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: + 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. From 0e8745b85facabbbd7f454ad766fae3596d075c5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:42:43 -0800 Subject: [PATCH 07/18] Update dev_requirements.txt --- dev_requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev_requirements.txt b/dev_requirements.txt index 2eff0bb..b5fba0b 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -8,3 +8,5 @@ sphinx sphinx_rtd_theme sphinx-toolbox myst_parser +opentelemetry-api +opentelemetry-sdk From 91ac325afe96d39ef9f86f4c4760e3c969792598 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:44:34 -0800 Subject: [PATCH 08/18] fixing checks --- dev_requirements.txt | 2 -- featuremanagement/azuremonitor/_send_telemetry.py | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index b5fba0b..2eff0bb 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -8,5 +8,3 @@ sphinx sphinx_rtd_theme sphinx-toolbox myst_parser -opentelemetry-api -opentelemetry-sdk diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 64d81c4..ed5e9f4 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -9,9 +9,9 @@ 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 + from opentelemetry import trace, baggage, context # type: ignore + from opentelemetry.context.context import Context # type: ignore + from opentelemetry.sdk.trace import Span, SpanProcessor # type: ignore HAS_AZURE_MONITOR_EVENTS_EXTENSION = True except ImportError: From 889a2ece39b16812a9502d25f382de3ccfedc55b Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:48:59 -0800 Subject: [PATCH 09/18] Update _send_telemetry.py --- .../azuremonitor/_send_telemetry.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index ed5e9f4..cea1dae 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -36,7 +36,6 @@ EVALUATION_EVENT_VERSION = "1.1.0" - def track_event(event_name: str, user: str, event_properties: Optional[Dict[str, Optional[str]]] = None) -> None: """ Tracks an event with the specified name and properties. @@ -117,22 +116,25 @@ def attach_targeting_info(targeting_id: str) -> None: :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: +if HAS_AZURE_MONITOR_EVENTS_EXTENSION: + class TargetingSpanProcessor(SpanProcessor): """ - 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. + A custom SpanProcessor that attaches the targeting ID to the span and baggage when a new span is started. """ - 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) + + 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. + """ + 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) From 1cd8703ea922c04f38b717ac6cb4534ab892cd60 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:50:49 -0800 Subject: [PATCH 10/18] Update _send_telemetry.py --- featuremanagement/azuremonitor/_send_telemetry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index cea1dae..634ccde 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -36,6 +36,7 @@ EVALUATION_EVENT_VERSION = "1.1.0" + def track_event(event_name: str, user: str, event_properties: Optional[Dict[str, Optional[str]]] = None) -> None: """ Tracks an event with the specified name and properties. @@ -123,6 +124,7 @@ def attach_targeting_info(targeting_id: str) -> None: if HAS_AZURE_MONITOR_EVENTS_EXTENSION: + class TargetingSpanProcessor(SpanProcessor): """ A custom SpanProcessor that attaches the targeting ID to the span and baggage when a new span is started. From 9f50e95996189e3ae7e5b15efc1844f3dfc85921 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 30 Jan 2025 15:54:48 -0800 Subject: [PATCH 11/18] trying mypy fix --- dev_requirements.txt | 2 ++ featuremanagement/azuremonitor/_send_telemetry.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 2eff0bb..b5fba0b 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -8,3 +8,5 @@ sphinx sphinx_rtd_theme sphinx-toolbox myst_parser +opentelemetry-api +opentelemetry-sdk diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 634ccde..47f347c 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -9,9 +9,9 @@ try: from azure.monitor.events.extension import track_event as azure_monitor_track_event # type: ignore - from opentelemetry import trace, baggage, context # type: ignore - from opentelemetry.context.context import Context # type: ignore - from opentelemetry.sdk.trace import Span, SpanProcessor # 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: From f98661ea63b0baaa4cc4079eacda23bcdf0b41f5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 31 Jan 2025 10:23:06 -0800 Subject: [PATCH 12/18] Update _send_telemetry.py --- .../azuremonitor/_send_telemetry.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 47f347c..89db16d 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -19,6 +19,9 @@ 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" @@ -123,20 +126,20 @@ def attach_targeting_info(targeting_id: str) -> None: trace.get_current_span().set_attribute(TARGETING_ID, targeting_id) -if HAS_AZURE_MONITOR_EVENTS_EXTENSION: +class TargetingSpanProcessor(SpanProcessor): + """ + A custom SpanProcessor that attaches the targeting ID to the span and baggage when a new span is started. + """ - 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. - 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. - """ - 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) + :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) From 3643d9ae09f73a1107cd768c555cceba61f1888f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 3 Feb 2025 15:44:15 -0800 Subject: [PATCH 13/18] Update to use accessor --- .../azuremonitor/_send_telemetry.py | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 89db16d..2f869c2 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -4,19 +4,21 @@ # license information. # -------------------------------------------------------------------------- import logging -from typing import Dict, Optional -from .._models import VariantAssignmentReason, EvaluationEvent +import inspect +from typing import Dict, Optional, Callable +from .._models import VariantAssignmentReason, EvaluationEvent, TargetingContext + +logger = logging.getLogger(__name__) 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( + logger.warning( "azure-monitor-events-extension is not installed. Telemetry will not be sent to Application Insights." ) SpanProcessor = object # type: ignore @@ -114,23 +116,18 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None: 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. + :keyword Callable[[], TargetingContext] targeting_context_accessor: Callback function to get the current targeting + context if one isn't provided. """ + def __init__(self, **kwargs) -> None: + self._targeting_context_accessor: Optional[Callable[[], TargetingContext]] = kwargs.pop( + "targeting_context_accessor", None + ) + 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. @@ -140,6 +137,20 @@ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None """ 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) + if self._targeting_context_accessor and callable(self._targeting_context_accessor): + if inspect.iscoroutinefunction(self._targeting_context_accessor): + logger.warning("Async targeting_context_accessor is not supported.") + return + targeting_context = self._targeting_context_accessor() + if not targeting_context or not isinstance(targeting_context, TargetingContext): + logger.warning( + "targeting_context_accessor did not return a TargetingContext. Received type %s.", + type(targeting_context), + ) + return + if not targeting_context.user_id: + logger.debug("TargetingContext does not have a user ID.") + return + span.set_attribute(TARGETING_ID, targeting_context.user_id) + + From b314b2807b6080c4a58645959f2b296bde42d7c4 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 3 Feb 2025 16:04:56 -0800 Subject: [PATCH 14/18] remove attach_targeting_info --- featuremanagement/azuremonitor/__init__.py | 3 +-- featuremanagement/azuremonitor/_send_telemetry.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/featuremanagement/azuremonitor/__init__.py b/featuremanagement/azuremonitor/__init__.py index d4b7310..138bcfb 100644 --- a/featuremanagement/azuremonitor/__init__.py +++ b/featuremanagement/azuremonitor/__init__.py @@ -3,12 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from ._send_telemetry import publish_telemetry, track_event, attach_targeting_info, TargetingSpanProcessor +from ._send_telemetry import publish_telemetry, track_event, TargetingSpanProcessor __all__ = [ "publish_telemetry", "track_event", - "attach_targeting_info", "TargetingSpanProcessor", ] diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index 2f869c2..c4512a8 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- import logging import inspect -from typing import Dict, Optional, Callable +from typing import Any, Callable, Dict, Optional from .._models import VariantAssignmentReason, EvaluationEvent, TargetingContext logger = logging.getLogger(__name__) @@ -123,7 +123,7 @@ class TargetingSpanProcessor(SpanProcessor): context if one isn't provided. """ - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: Any) -> None: self._targeting_context_accessor: Optional[Callable[[], TargetingContext]] = kwargs.pop( "targeting_context_accessor", None ) @@ -152,5 +152,3 @@ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None logger.debug("TargetingContext does not have a user ID.") return span.set_attribute(TARGETING_ID, targeting_context.user_id) - - From 391de390678f78b074f4cfe0bdcf986e6b4392a2 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 4 Feb 2025 10:10:40 -0800 Subject: [PATCH 15/18] added tests --- .../azuremonitor/_send_telemetry.py | 1 + tests/test_send_telemetry_appinsights.py | 52 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/featuremanagement/azuremonitor/_send_telemetry.py b/featuremanagement/azuremonitor/_send_telemetry.py index c4512a8..5876eee 100644 --- a/featuremanagement/azuremonitor/_send_telemetry.py +++ b/featuremanagement/azuremonitor/_send_telemetry.py @@ -136,6 +136,7 @@ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None :param parent_context: The parent context of the span. """ if not HAS_AZURE_MONITOR_EVENTS_EXTENSION: + logger.warning("Azure Monitor Events Extension is not installed.") return if self._targeting_context_accessor and callable(self._targeting_context_accessor): if inspect.iscoroutinefunction(self._targeting_context_accessor): diff --git a/tests/test_send_telemetry_appinsights.py b/tests/test_send_telemetry_appinsights.py index 073c55f..8d29f7d 100644 --- a/tests/test_send_telemetry_appinsights.py +++ b/tests/test_send_telemetry_appinsights.py @@ -3,15 +3,19 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +import logging from unittest.mock import patch import pytest -from featuremanagement import EvaluationEvent, FeatureFlag, Variant, VariantAssignmentReason +from featuremanagement import EvaluationEvent, FeatureFlag, Variant, VariantAssignmentReason, TargetingContext import featuremanagement.azuremonitor._send_telemetry +from featuremanagement.azuremonitor import TargetingSpanProcessor @pytest.mark.usefixtures("caplog") class TestSendTelemetryAppinsights: + user_id = None + def test_send_telemetry_appinsights(self): feature_flag = FeatureFlag.convert_from_json( { @@ -211,3 +215,49 @@ def test_send_telemetry_appinsights_allocation(self): assert mock_track_event.call_args[0][1]["VariantAssignmentReason"] == "Percentile" assert mock_track_event.call_args[0][1]["VariantAssignmentPercentage"] == "25" assert "DefaultWhenEnabled" not in mock_track_event.call_args[0][1] + + def test_targeting_span_processor(self, caplog): + processor = TargetingSpanProcessor() + processor.on_start(None) + assert "" in caplog.text + caplog.clear() + + processor = TargetingSpanProcessor(targeting_context_accessor="not callable") + processor.on_start(None) + assert "" in caplog.text + caplog.clear() + + processor = TargetingSpanProcessor(targeting_context_accessor=self.bad_targeting_context_accessor) + processor.on_start(None) + assert ( + "targeting_context_accessor did not return a TargetingContext. Received type ." in caplog.text + ) + caplog.clear() + + processor = TargetingSpanProcessor(targeting_context_accessor=self.async_targeting_context_accessor) + processor.on_start(None) + assert "Async targeting_context_accessor is not supported." in caplog.text + caplog.clear() + + processor = TargetingSpanProcessor(targeting_context_accessor=self.accessor_callback) + logging.getLogger().setLevel(logging.DEBUG) + processor.on_start(None) + assert "TargetingContext does not have a user ID." in caplog.text + caplog.clear() + + with patch("opentelemetry.sdk.trace.Span") as mock_span: + self.user_id = "test_user" + processor.on_start(mock_span) + assert mock_span.set_attribute.call_args[0][0] == "TargetingId" + assert mock_span.set_attribute.call_args[0][1] == "test_user" + + self.user_id = None + + def bad_targeting_context_accessor(self): + return "not targeting context" + + async def async_targeting_context_accessor(self): + return TargetingContext(user_id=self.user_id) + + def accessor_callback(self): + return TargetingContext(user_id=self.user_id) From 171c02915b8e7f635a77a2769b5d97cbd39cf259 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 11 Feb 2025 09:21:11 -0800 Subject: [PATCH 16/18] Adding Quart sample --- project-words.txt | 1 + samples/quarty_sample.py | 59 ++++++++++++++++++++++++++++++++++++++++ samples/requirements.txt | 1 + 3 files changed, 61 insertions(+) create mode 100644 samples/quarty_sample.py diff --git a/project-words.txt b/project-words.txt index 20f4119..ea9f1a6 100644 --- a/project-words.txt +++ b/project-words.txt @@ -12,3 +12,4 @@ featuremanagerbase quickstart rtype usefixtures +urandom diff --git a/samples/quarty_sample.py b/samples/quarty_sample.py new file mode 100644 index 0000000..db3867d --- /dev/null +++ b/samples/quarty_sample.py @@ -0,0 +1,59 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import uuid +import os +from quart import Quart, request, session +from quart.sessions import SecureCookieSessionInterface +from azure.appconfiguration.provider import load +from azure.identity import DefaultAzureCredential +from featuremanagement.aio import FeatureManager +from featuremanagement import TargetingContext + +try: + from azure.monitor.opentelemetry import configure_azure_monitor # pylint: disable=ungrouped-imports + + # Configure Azure Monitor + configure_azure_monitor(connection_string=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")) +except ImportError: + pass + +app = Quart(__name__) +app.session_interface = SecureCookieSessionInterface() +app.secret_key = os.urandom(24) + +endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING") +credential = DefaultAzureCredential() + + +async def my_targeting_accessor() -> TargetingContext: + session_id = "" + if "Session-ID" in request.headers: + session_id = request.headers["Session-ID"] + return TargetingContext(user_id=session_id) + + +# Connecting to Azure App Configuration using AAD +config = load(endpoint=endpoint, credential=credential, feature_flag_enabled=True, feature_flag_refresh_enabled=True) + +# Load feature flags and set up targeting context accessor +feature_manager = FeatureManager(config, targeting_context_accessor=my_targeting_accessor) + + +@app.before_request +async def before_request(): + if "session_id" not in session: + session["session_id"] = str(uuid.uuid4()) # Generate a new session ID + request.headers["Session-ID"] = session["session_id"] + + +@app.route("/") +async def hello(): + variant = await feature_manager.get_variant("Message") + return str(variant.configuration if variant else "No variant found") + + +app.run() diff --git a/samples/requirements.txt b/samples/requirements.txt index f4673f1..79cbd93 100644 --- a/samples/requirements.txt +++ b/samples/requirements.txt @@ -2,3 +2,4 @@ featuremanagement azure-appconfiguration-provider azure-monitor-opentelemetry azure-monitor-events-extension +quart From 9f33d0c64d28248eddf9458100f288e8c12c7f1f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 11 Feb 2025 10:02:44 -0800 Subject: [PATCH 17/18] Added otel to quart sample --- samples/quarty_sample.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/samples/quarty_sample.py b/samples/quarty_sample.py index db3867d..8bfd08e 100644 --- a/samples/quarty_sample.py +++ b/samples/quarty_sample.py @@ -10,16 +10,25 @@ from quart.sessions import SecureCookieSessionInterface from azure.appconfiguration.provider import load from azure.identity import DefaultAzureCredential +from azure.monitor.opentelemetry import configure_azure_monitor from featuremanagement.aio import FeatureManager from featuremanagement import TargetingContext +from featuremanagement.azuremonitor import TargetingSpanProcessor -try: - from azure.monitor.opentelemetry import configure_azure_monitor # pylint: disable=ungrouped-imports - # Configure Azure Monitor - configure_azure_monitor(connection_string=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")) -except ImportError: - pass +# A callback for assigning a TargetingContext for both Telemetry logs and Feature Flag evaluation +async def my_targeting_accessor() -> TargetingContext: + session_id = "" + if "Session-ID" in request.headers: + session_id = request.headers["Session-ID"] + return TargetingContext(user_id=session_id) + + +# Configure Azure Monitor +configure_azure_monitor( + connection_string=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"), + span_processors=[TargetingSpanProcessor(targeting_context_accessor=my_targeting_accessor)], +) app = Quart(__name__) app.session_interface = SecureCookieSessionInterface() @@ -28,14 +37,6 @@ endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING") credential = DefaultAzureCredential() - -async def my_targeting_accessor() -> TargetingContext: - session_id = "" - if "Session-ID" in request.headers: - session_id = request.headers["Session-ID"] - return TargetingContext(user_id=session_id) - - # Connecting to Azure App Configuration using AAD config = load(endpoint=endpoint, credential=credential, feature_flag_enabled=True, feature_flag_refresh_enabled=True) From 2772068f1cc11ad5303745b16b7fe6b5600c9975 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 11 Feb 2025 10:05:20 -0800 Subject: [PATCH 18/18] Update requirements.txt --- samples/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/requirements.txt b/samples/requirements.txt index 79cbd93..1c84dca 100644 --- a/samples/requirements.txt +++ b/samples/requirements.txt @@ -3,3 +3,4 @@ azure-appconfiguration-provider azure-monitor-opentelemetry azure-monitor-events-extension quart +azure-identity