Skip to content

Commit 5a6cf33

Browse files
Add SDKStats module with Azure Monitor bridge for feature/instrumentation reporting (#89)
* Add SDKStats module with Azure Monitor bridge for feature/instrumentation reporting - New _sdkstats package: state management, observable metrics, and manager for SDK self-telemetry (feature flags and instrumentation bitmasks) - Feature bits aligned with Azure Monitor exporter statsbeat encoding: bits 1-64 match exporter, bits 128+ are distro-specific (A365_EXPORT, OTLP_EXPORT, CONSOLE_EXPORT, SPECTRA_EXPORT) - When Azure Monitor is enabled: bridge distro bits into exporter's statsbeat via _StatsbeatMetrics._FEATURE_ATTRIBUTES and _INSTRUMENTATIONS_BIT_MASK (single pipeline, no duplication) - When Azure Monitor is disabled: standalone SdkStatsManager runs its own AzureMonitorMetricExporter(is_sdkstats=True) pipeline - Distro records feature flags for each enabled exporter target and instrumentation bits for each activated library - 37 tests covering state, metrics, manager, bridge, and thread safety * Address Copilot review: metric naming, env parsing, error handling, dead code - Use lowercase metric names ('feature', 'feature.instrumentations') to match exporter convention and test assertions; remove unused _ATTACH_METRIC_NAME constant - Add 'on' to is_sdkstats_enabled() disable values for consistency with _env_bool() - Split bare except in snippet_injector into ImportError (silent) vs Exception (debug-logged) for diagnosability - Remove unused _DEFAULT_EXPORT_INTERVAL_SECS constant from _manager.py - Add test_disabled_by_on test case * Address comments * Address comments * Bring back env set line --------- Co-authored-by: Radhika Gupta <guptaradhika@microsoft.com>
1 parent 910330c commit 5a6cf33

7 files changed

Lines changed: 1106 additions & 5 deletions

File tree

src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/snippet_injector.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,48 @@
3535

3636

3737
def _mark_browser_loader_feature(is_enabled: bool) -> None:
38-
"""Record browser SDK loader usage in statsbeat when available.
38+
"""Record browser SDK loader usage in SDKStats.
39+
40+
Uses the in-repo sdkstats module first, then falls back to the Azure
41+
Monitor Exporter statsbeat for backward compatibility.
3942
4043
:param is_enabled: Indicates whether the browser loader is enabled.
4144
:type is_enabled: bool
4245
"""
4346
if not is_enabled:
4447
return
48+
49+
# Record via the in-repo sdkstats module (always available)
50+
try:
51+
from microsoft.opentelemetry._sdkstats._state import (
52+
SdkStatsFeature,
53+
is_sdkstats_enabled,
54+
get_sdkstats_shutdown,
55+
set_sdkstats_feature,
56+
)
57+
58+
if is_sdkstats_enabled() and not get_sdkstats_shutdown():
59+
set_sdkstats_feature(SdkStatsFeature.AZURE_MONITOR_BROWSER_SDK_LOADER)
60+
except Exception: # pylint: disable=broad-exception-caught
61+
_logger.debug("Failed to record browser loader sdkstats usage", exc_info=True)
62+
63+
# Also propagate to the Azure Monitor Exporter statsbeat (if available)
4564
try:
4665
from azure.monitor.opentelemetry.exporter.statsbeat._state import (
4766
get_statsbeat_browser_sdk_loader_feature_set,
4867
get_statsbeat_shutdown,
4968
is_statsbeat_enabled,
5069
set_statsbeat_browser_sdk_loader_feature_set,
5170
)
52-
except ImportError:
53-
return
5471

55-
try:
5672
if (
5773
is_statsbeat_enabled()
5874
and not get_statsbeat_shutdown()
5975
and not get_statsbeat_browser_sdk_loader_feature_set()
6076
):
6177
set_statsbeat_browser_sdk_loader_feature_set()
78+
except ImportError:
79+
pass
6280
except Exception: # pylint: disable=broad-exception-caught
6381
_logger.debug("Failed to record browser loader statsbeat usage", exc_info=True)
6482

src/microsoft/opentelemetry/_distro.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,26 @@
6464
)
6565
from microsoft.opentelemetry._instrumentation import get_dist_dependency_conflicts
6666
from microsoft.opentelemetry._otlp import is_otlp_enabled
67+
from microsoft.opentelemetry._sdkstats._state import (
68+
SdkStatsFeature,
69+
get_sdkstats_feature_flags,
70+
get_sdkstats_instrumentation_flags,
71+
is_sdkstats_enabled,
72+
set_sdkstats_feature,
73+
set_sdkstats_instrumentation_by_name,
74+
)
6775
from microsoft.opentelemetry._utils import (
6876
_append_azure_monitor_components,
6977
_append_console_components,
7078
_append_otlp_components,
7179
)
7280
from microsoft.opentelemetry._version import VERSION
7381

82+
import azure.monitor.opentelemetry.exporter._utils as _exporter_utils
83+
from azure.monitor.opentelemetry.exporter.statsbeat._statsbeat_metrics import (
84+
_StatsbeatMetrics,
85+
)
86+
7487
_logger = getLogger(__name__)
7588

7689

@@ -177,7 +190,7 @@ def use_microsoft_opentelemetry(**kwargs: object) -> None: # pylint: disable=to
177190
"""
178191

179192
os.environ[MICROSOFT_OPENTELEMETRY_VERSION_ENV] = VERSION
180-
enable_azure_monitor = kwargs.pop(ENABLE_AZURE_MONITOR_ARG, False)
193+
enable_azure_monitor: bool = bool(kwargs.pop(ENABLE_AZURE_MONITOR_ARG, False))
181194
enable_console: bool = bool(kwargs.pop(ENABLE_CONSOLE_ARG, False))
182195
enable_a365: bool = bool(kwargs.pop(ENABLE_A365_ARG, False))
183196
a365_token_resolver = kwargs.pop(A365_TOKEN_RESOLVER_ARG, None)
@@ -219,8 +232,13 @@ def use_microsoft_opentelemetry(**kwargs: object) -> None: # pylint: disable=to
219232
inst_opts.setdefault(lib, {}).setdefault("enabled", False)
220233
otel_kwargs[INSTRUMENTATION_OPTIONS_ARG] = inst_opts
221234

235+
# ---- SDKStats: record distro feature flag ----
236+
set_sdkstats_feature(SdkStatsFeature.DISTRO)
237+
222238
# ---- OTLP exporters (append to user-supplied processors/readers) ----
223239
_append_otlp_components(otel_kwargs)
240+
if is_otlp_enabled():
241+
set_sdkstats_feature(SdkStatsFeature.OTLP_EXPORT)
224242

225243
# ---- Spectra sidecar exporter (OTLP gRPC/HTTP to localhost) ----
226244
_append_spectra_components(
@@ -252,6 +270,8 @@ def use_microsoft_opentelemetry(**kwargs: object) -> None: # pylint: disable=to
252270
if not enable_console and not enable_azure_monitor and not enable_a365 and not is_otlp_enabled():
253271
enable_console = True
254272
_append_console_components(otel_kwargs, enable_console)
273+
if enable_console:
274+
set_sdkstats_feature(SdkStatsFeature.CONSOLE_EXPORT)
255275

256276
# ---- Build and register providers ----
257277
tracer_provider: Optional[TracerProvider] = None
@@ -293,6 +313,9 @@ def use_microsoft_opentelemetry(**kwargs: object) -> None: # pylint: disable=to
293313
# ---- Instrumentations (always, after providers are set) ----
294314
_setup_instrumentations(otel_kwargs)
295315

316+
# ---- SDKStats manager (after providers, before returning) ----
317+
_initialize_sdkstats(enable_azure_monitor)
318+
296319
if enable_azure_monitor:
297320
_logger.info("Azure Monitor enabled.")
298321

@@ -305,6 +328,52 @@ def _env_bool(name: str, default: bool = False) -> bool:
305328
return val in ("true", "1", "yes", "on")
306329

307330

331+
def _initialize_sdkstats(enable_azure_monitor: bool) -> None:
332+
"""Set up SDKStats — always sends to the Application Insights statsbeat endpoint.
333+
334+
When Azure Monitor is active the exporter package's own StatsbeatManager
335+
handles everything and we bridge our distro-level feature/instrumentation
336+
bits into its state so it reports the full picture. For A365-only,
337+
OTLP-only, or Console-only customers this function creates a standalone
338+
pipeline using ``AzureMonitorMetricExporter(is_sdkstats=True)`` pointed
339+
at the well-known statsbeat ingestion endpoint. The customer's telemetry
340+
pipeline is not affected.
341+
"""
342+
if not is_sdkstats_enabled():
343+
return
344+
345+
if enable_azure_monitor:
346+
# The exporter package runs its own statsbeat. Bridge our
347+
# distro-level feature bits (A365_EXPORT, OTLP_EXPORT, etc.)
348+
# and instrumentation bits into the exporter's state so they
349+
# appear in the same observation. Our bit values (128+) do
350+
# not collide with the exporter's (1–64).
351+
_bridge_sdkstats_to_azure_monitor()
352+
return
353+
354+
from microsoft.opentelemetry._sdkstats._manager import SdkStatsManager
355+
356+
manager = SdkStatsManager()
357+
manager.initialize()
358+
359+
360+
def _bridge_sdkstats_to_azure_monitor() -> None:
361+
"""OR distro feature/instrumentation bits into the exporter's statsbeat."""
362+
# Feature bits — OR our flags into the class-level dict that the
363+
# exporter's _get_feature_metric callback reads each cycle.
364+
feature_flags = get_sdkstats_feature_flags()
365+
if feature_flags:
366+
current = _StatsbeatMetrics._FEATURE_ATTRIBUTES.get("feature") or 0
367+
_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] = current | feature_flags
368+
369+
# Instrumentation bits — OR directly into the exporter's module-
370+
# level bitmask (thread-safe via their lock).
371+
instrumentation_flags = get_sdkstats_instrumentation_flags()
372+
if instrumentation_flags:
373+
with _exporter_utils._INSTRUMENTATIONS_BIT_MASK_LOCK:
374+
_exporter_utils._INSTRUMENTATIONS_BIT_MASK |= instrumentation_flags
375+
376+
308377
def _append_a365_components(
309378
enable_a365: bool,
310379
otel_kwargs: Dict[str, Any],
@@ -367,6 +436,8 @@ def _append_a365_components(
367436
otel_kwargs[SPAN_PROCESSORS_ARG] = list(otel_kwargs.get(SPAN_PROCESSORS_ARG) or [])
368437
otel_kwargs[SPAN_PROCESSORS_ARG].append(baggage_processor)
369438

439+
set_sdkstats_feature(SdkStatsFeature.A365_EXPORT)
440+
370441
# Resolve configuration: kwargs > env vars > defaults
371442
resolved_cluster_category = cluster_category or os.environ.get(A365_CLUSTER_CATEGORY_ENV, "prod")
372443
resolved_use_s2s = use_s2s_endpoint if use_s2s_endpoint is not None else _env_bool(A365_USE_S2S_ENDPOINT_ENV)
@@ -448,6 +519,8 @@ def _append_spectra_components(
448519
"""
449520
if not enable_spectra:
450521
return
522+
523+
set_sdkstats_feature(SdkStatsFeature.SPECTRA_EXPORT)
451524

452525
if otel_kwargs.get(DISABLE_TRACING_ARG, False):
453526
return
@@ -619,6 +692,7 @@ def _setup_instrumentations(otel_kwargs: Dict[str, Any]) -> None:
619692
continue
620693
instrumentor: Any = entry_point.load()
621694
instrumentor().instrument(skip_dep_check=True)
695+
set_sdkstats_instrumentation_by_name(lib_name)
622696
except Exception as ex: # pylint: disable=broad-except
623697
_logger.warning(
624698
"Exception occurred when instrumenting: %s.",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
7+
"""SDK self-telemetry (SDKStats) for the Microsoft OpenTelemetry Distro.
8+
9+
This module provides backend-agnostic SDK health and usage telemetry that
10+
works regardless of which export backends are enabled (Azure Monitor, OTLP,
11+
A365, Console). It tracks:
12+
13+
- **Features**: which distro features are active (disk retry, AAD, live
14+
metrics, browser SDK loader, etc.)
15+
- **Instrumentations**: which library instrumentations are enabled (Django,
16+
FastAPI, OpenAI, LangChain, etc.)
17+
18+
The module is initialised by :func:`use_microsoft_opentelemetry` during
19+
distro setup and sends metrics to the Application Insights statsbeat
20+
ingestion endpoint via ``AzureMonitorMetricExporter``.
21+
"""
22+
23+
from microsoft.opentelemetry._sdkstats._state import (
24+
get_sdkstats_feature_flags,
25+
get_sdkstats_instrumentation_flags,
26+
is_sdkstats_enabled,
27+
set_sdkstats_feature,
28+
set_sdkstats_instrumentation,
29+
set_sdkstats_shutdown,
30+
)
31+
from microsoft.opentelemetry._sdkstats._manager import SdkStatsManager
32+
33+
__all__ = [
34+
"SdkStatsManager",
35+
"get_sdkstats_feature_flags",
36+
"get_sdkstats_instrumentation_flags",
37+
"is_sdkstats_enabled",
38+
"set_sdkstats_feature",
39+
"set_sdkstats_instrumentation",
40+
"set_sdkstats_shutdown",
41+
]

0 commit comments

Comments
 (0)