Onesettings local storage toggle - #48379
Conversation
Refactor BaseExporter to create offline storage via an idempotent _enable_local_storage() helper, preparing for OneSettings-driven FEATURE_LOCAL_STORAGE toggling. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
Add _local_storage_configuration_callback which evaluates the FEATURE_LOCAL_STORAGE flag to enable/disable offline storage at runtime, plus _disable_local_storage. OneSettings acts as a kill-switch: it can force storage off and re-enable only when the user did not explicitly opt out via disable_offline_storage=True. Also document the analogous connection-string gate in the live metrics callback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
…customer sdkstats Statsbeat's own offline storage is now always disabled (best-effort internal diagnostics that never persist to disk), independent of the user's setting. The StatsbeatConfig.disable_offline_storage field now carries the customer's setting solely for DISK_RETRY feature-bit reporting, preserved across reconfigures. Customer sdkstats now honors the user's disable_offline_storage, since it shares the user's local storage directory (same instrumentation key). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
BaseExporter now registers _local_storage_configuration_callback with the configuration manager so local (offline) storage can be toggled remotely via the FEATURE_LOCAL_STORAGE feature flag. Statsbeat and customer-sdkstats exporters are skipped: statsbeat never persists to disk and customer-sdkstats mirrors the user's setting via its own manager. Registration is a NoOp when the control plane worker is not running or is disabled via environment variable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
Capture self.storage into a local before use at the four export-path sites (_transmit_from_storage, _handle_transmit_from_storage, and the overflow/206-resend paths in _transmit). Once the OneSettings worker is active, _disable_local_storage() can null self.storage on a worker thread between an 'if self.storage' check and the subsequent put()/gets() call, causing an AttributeError. Reading once into a local eliminates that window; a concurrent close() on the captured reference is benign since close() only stops the maintenance thread. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
…orter Customer-sdkstats writes to the same on-disk folder as the main exporter (keyed on the customer's ikey), so it must follow the same remote FEATURE_LOCAL_STORAGE kill-switch. Remove the customer-sdkstats skip from the callback registration gate (statsbeat stays excluded - it never persists to disk). The user's static disable_offline_storage still applies via the customer manager, and the callback's hard gate never re-enables a user opt-out. Add a registration test for the customer-sdkstats path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
|
Azure Pipelines: Successfully started running 1 pipeline(s). 9 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Adds OneSettings-controlled runtime toggling for offline storage and aligns internal stats exporters with storage opt-out behavior.
Changes:
- Adds runtime storage enable/disable callbacks.
- Disables statsbeat persistence while propagating customer storage settings.
- Expands tests and release notes.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_base_exporter.py |
Tests storage toggling and callback registration. |
tests/statsbeat/test_manager.py |
Tests statsbeat storage configuration. |
tests/customer_sdk_stats/test_manager.py |
Tests customer SDK stats opt-out propagation. |
tests/customer_sdk_stats/test_customer_sdkstats.py |
Tests forwarding the storage setting. |
CHANGELOG.md |
Documents runtime storage toggling. |
statsbeat/customer/_manager.py |
Passes storage opt-out to the customer stats exporter. |
statsbeat/customer/_customer_sdkstats.py |
Forwards the originating exporter’s setting. |
statsbeat/_manager.py |
Disables statsbeat disk persistence. |
export/_base.py |
Implements storage callbacks and concurrent access handling. |
_quickpulse/_live_metrics.py |
Clarifies live-metrics re-enable gating. |
Remove the OneSettings gate comment block in live metrics and correct the StatsbeatConfig.from_config docstring: sdkstats's own storage is always disabled (never persists to disk), not enabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py:239
- This registration cannot make the advertised remote toggle work:
get_configuration_manager()only constructs the singleton andregister_callback()only appends to its callback list. The onlyBaseExportercall toinitialize()remains commented out above (lines 121–124 and 213–222), and there is no production initializer elsewhere in the repository, so no configuration worker ever polls OneSettings or invokes this callback. Start the manager with the exporter profile as part of the production lifecycle before relying on the callback.
if not self._is_stats_exporter():
config_manager = get_configuration_manager()
if config_manager:
config_manager.register_callback(self._local_storage_configuration_callback)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py:239
- Registering this bound method gives the process-wide manager a strong reference to every exporter, but the manager has no unregister operation and trace/log/metric
shutdown()only closes storage. Consequently shut-down exporters cannot be collected and can still receive later toggles; after a prior disable, an enable callback can even create a newLocalFileStoragemaintenance thread for an exporter that has already shut down. Add callback unregistration to exporter shutdown (or store weak callbacks) so the callback lifetime matches the exporter lifetime.
config_manager = get_configuration_manager()
if config_manager:
config_manager.register_callback(self._local_storage_configuration_callback)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py:239
- Registration does not replay the manager's cached settings. Callbacks are notified only when a new settings payload is fetched, so an exporter created after
FEATURE_LOCAL_STORAGE=disabledwas cached starts with storage enabled and can remain that way indefinitely while change checks return 304. Make subscription and delivery of the current settings atomic so every new exporter immediately honors the active kill-switch without racing a concurrent update.
config_manager = get_configuration_manager()
if config_manager:
config_manager.register_callback(self._local_storage_configuration_callback)
register_callback only appends the callback and does not replay the current cached configuration; notifications fire only on a config change. An exporter created after FEATURE_LOCAL_STORAGE was already cached as disabled would therefore create active storage and keep it until the next change (which may never come). After registering, read the cached settings via get_settings() and apply them once so late-created exporters (e.g. customer-sdkstats) honor an existing kill-switch immediately. An empty cache is a no-op, leaving storage per the user's setting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py:239
- The singleton manager stores this bound method strongly, but exporter
shutdown()only closes storage and never unregisters it; the callback list is cleared only when the entire configuration manager shuts down. Consequently every discarded exporter remains reachable for the manager's lifetime, and a later disable/enable notification can create a new storage thread for an exporter that was already shut down. Please add callback lifecycle cleanup (or register a weak callback) and prevent post-shutdown re-enablement.
if not self._is_stats_exporter():
config_manager = get_configuration_manager()
if config_manager:
config_manager.register_callback(self._local_storage_configuration_callback)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py:248
- Registering the callback and replaying
get_settings()are not atomic with configuration updates. For example, this thread can read cacheddisabled, the worker can then publish and finish anenabledcallback (which sees the initially active storage and does nothing), and this stale replay can finally disable storage, leaving the exporter opposite to the latest OneSettings state. Please make callback registration plus initial replay ordered with manager updates (for example, a manager API that serializes registration/replay with notifications).
# once here so late-created exporters honor an existing kill-switch immediately. An
# empty cache evaluates to a no-op, leaving storage per the user's setting.
cached_settings = config_manager.get_settings()
if cached_settings:
self._local_storage_configuration_callback(cached_settings)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/_manager.py:252
- This key decoupling behavior is not exercised by the updated tests: the new assertions only verify that
StatsbeatConfigpreserves the user's value, so they would still pass if this argument were changed back toconfig.disable_offline_storage. Add a_do_initialize/initializetest withconfig.disable_offline_storage=Falsethat mocksAzureMonitorMetricExporterand asserts it is constructed withdisable_offline_storage=True.
# Statsbeat never persists its own envelopes to disk. It is best-effort internal
# diagnostics, so it does not need disk-backed retry, and disabling storage avoids any
# disk writes for users who opted out. config.disable_offline_storage reflects the
# customer's config and is used only for DISK_RETRY reporting below.
disable_offline_storage=True,
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py:226
- A cached
FEATURE_LOCAL_STORAGE=disabledis applied only after_enable_local_storage()has constructedLocalFileStorage. Its constructor creates/checks the directory, runs maintenance, and starts a thread, so a late exporter still touches the disk before immediately closing storage. Read/evaluate the cached setting before creating storage so an already-active kill switch prevents initialization entirely.
if not self._disable_offline_storage:
self._enable_local_storage()
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py:736
- The shutdown guard is a check-then-act race. An enable callback can read
_local_storage_shutdown == False, pause while shutdown sets it toTrueand observesstorage is None, then resume and construct/assign a newLocalFileStorage; the exporter is shut down but its maintenance thread has been resurrected. Guard enable/disable/shutdown with the same lock and make the shutdown check plus storage assignment atomic.
if self._local_storage_shutdown:
return
if self.storage is not None:
return
if self._storage_directory is None:
self._storage_directory = _get_storage_directory(self._instrumentation_key or "")
self.storage = LocalFileStorage( # pyright: ignore
This comment has been minimized.
This comment has been minimized.
Switch the OneSettings FEATURE_LOCAL_STORAGE kill-switch from a create/destroy model to a lightweight enable/disable flag. The LocalFileStorage instance and its maintenance thread are constructed once and kept for the exporter's lifetime; a remote disable now flips LocalFileStorage._active off so put()/gets() no-op, and a remote enable flips it back on without reconstructing storage or its thread. - _storage.py: add _active flag + enable()/disable(); gate put/gets/get. - _base.py: _enable_local_storage constructs once then enable()s; _disable_local_storage disable()s instead of close+null. - Update callback/late-exporter tests to assert flag state (instance is reused, not dropped); add storage-level toggle tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
The flag model never nulls self.storage, so the check-then-use race the capture comments described can no longer happen. Correct those comments (self.storage is None only on user opt-out) and drop the redundant _active re-check in LocalFileStorage.get(), which gets() already gates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
The _ConfigurationManager singleton held each exporter's bound-method callback strongly, pinning every discarded exporter for the process lifetime. Wrap bound methods in weakref.WeakMethod (detected via inspect.ismethod) so a dropped exporter can be garbage collected even when shutdown() is never called; plain module-level function callbacks are stored as-is. _notify_callbacks resolves weak refs, skips dead ones, and prunes them so _callbacks does not grow under exporter churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
Add a CHANGELOG entry for the WeakMethod callback leak fix and a TODO in CustomerSdkStatsManager._cleanup marking a pre-existing failed-init leak of the LocalFileStorage maintenance thread. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
1de7e49 to
70dd434
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (1)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py:248
- The register-then-replay sequence is not ordered with configuration updates. If
get_settings()returns the old cache, the worker can publish and invoke this callback with a newer value before this constructor appliescached_settings; the constructor then overwrites the newer toggle and leaves storage stale until another configuration change. Please make callback registration plus initial replay atomic/versioned in_ConfigurationManagerrather than composingregister_callback()andget_settings()here.
cached_settings = config_manager.get_settings()
if cached_settings:
self._local_storage_configuration_callback(cached_settings)
[Pilot] PR Pipeline Failure AnalysisA CI pipeline failed on this pull request. Here is an automated analysis of what went wrong and how to get the build green. What failedThe Pylint check for
Both issues are in code added or modified by this PR. Recommended next steps
Raw pipeline analysis (azsdk ci analyze)
|
Merge the two identical CLIENT_STORAGE_DISABLED returns in LocalFileStorage.put to stay within the return-statement limit while preserving behavior, and wrap the customer-sdkstats cleanup TODO to satisfy the line-length limit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b13aa0ee-c777-4719-a18e-21985cf46358
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (4)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py:248
- The cached-state replay is not ordered with worker notifications. If this thread reads a disabled snapshot, the worker can publish and invoke the callback with a newer enabled state, and then this thread applies the stale disabled snapshot, leaving storage in the wrong state until another configuration change. Registration and replay need to be atomic/ordered inside the configuration manager (for example with a generation-aware register-and-replay API).
cached_settings = config_manager.get_settings()
if cached_settings:
self._local_storage_configuration_callback(cached_settings)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/customer/_customer_sdkstats.py:25
- This setting is only propagated for the first exporter:
collect_customer_sdkstatsskipsinitializeonce the singleton manager is active. If exporter A enables storage and exporter B later specifiesdisable_offline_storage=True, B's SDK stats still feed the existing disk-backed exporter, so its explicit opt-out is not honored. The singleton must adopt the strongest opt-out or reconfigure when a later exporter disables storage.
customer_stats.initialize(
connection_string=exporter._connection_string, # type: ignore
credential=exporter._credential, # type: ignore
disable_offline_storage=exporter._disable_offline_storage, # type: ignore
sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_base_exporter.py:336
- This cleanup does not stop
LocalFileStorage'sPeriodicTask, so every new test that constructs aBaseExporterleaves a daemon maintenance thread running for the rest of the test process (the same pattern is repeated in the new cleanup blocks below). Close the storage before cleaning its path.
finally:
if base.storage is not None:
clean_folder(base.storage._path)
sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_configuration/init.py:87
- Dead
WeakMethodentries are pruned only when_notify_callbacksruns. With a stable configuration—or currently, while manager initialization remains disabled—exporter churn continuously appends dead weak references and the singleton list still grows without bound; the eventual per-entrylist.removecleanup is also quadratic. Prune opportunistically during registration or use weakref finalizers/a single-pass rebuild so the leak fix does not leave an unbounded callback registry.
if inspect.ismethod(callback):
self._callbacks.append(weakref.WeakMethod(callback))
else:
self._callbacks.append(callback)
| cached_settings = config_manager.get_settings() | ||
| if cached_settings: | ||
| self._local_storage_configuration_callback(cached_settings) |
Remote local-storage toggle: Local (offline) disk storage can be enabled/disabled at runtime via the OneSettings FEATURE_LOCAL_STORAGE flag, and never overrides an explicit disable_offline_storage=True user opt-out.
• Flag-based toggling: Disabling keeps the LocalFileStorage instance and its maintenance thread alive and just flips an _active flag so put() / gets() no-op — avoiding teardown/reconstruction churn and thread-restart races on re-enable.
• Late-exporter handling: Exporters created after a disabled state was already cached apply the cached kill-switch immediately on registration.
• Stats exporters: Statsbeat storage is decoupled from the user setting (always off); customer-sdkstats honors the user setting and follows the remote toggle (shared on-disk folder).
• Memory-leak fix: Bound-method OneSettings callbacks are stored via weakref.WeakMethod , so discarded exporters are garbage-collected instead of pinned for the process lifetime; dead refs are pruned on notify.