diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/Tracer/ConfigureIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/Tracer/ConfigureIntegration.cs index e1b546343979..1f1b6866c535 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/Tracer/ConfigureIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/Tracer/ConfigureIntegration.cs @@ -73,69 +73,10 @@ internal static void ConfigureSettingsWithManualOverrides(Dictionary internal class GlobalConfigurationSource { - private static IConfigurationSource _dynamicConfigConfigurationSource = NullConfigurationSource.Instance; - private static ManualInstrumentationConfigurationSourceBase _manualConfigurationSource = new ManualInstrumentationConfigurationSource(new Dictionary(), useDefaultSources: true); - /// /// Gets the configuration source instance. /// @@ -32,10 +29,6 @@ internal class GlobalConfigurationSource internal static GlobalConfigurationSourceResult CreationResult { get; private set; } = CreateDefaultConfigurationSource(); - internal static IConfigurationSource DynamicConfigurationSource => _dynamicConfigConfigurationSource; - - internal static ManualInstrumentationConfigurationSourceBase ManualConfigurationSource => _manualConfigurationSource; - /// /// Creates a by combining environment variables, /// Precedence is as follows: @@ -142,14 +135,4 @@ private static string GetCurrentDirectory() { return AppDomain.CurrentDomain.BaseDirectory ?? Directory.GetCurrentDirectory(); } - - public static void UpdateDynamicConfigConfigurationSource(IConfigurationSource dynamic) - { - Interlocked.Exchange(ref _dynamicConfigConfigurationSource, dynamic); - } - - public static void UpdateManualConfigurationSource(ManualInstrumentationConfigurationSourceBase manual) - { - Interlocked.Exchange(ref _manualConfigurationSource, manual); - } } diff --git a/tracer/src/Datadog.Trace/Configuration/DynamicConfigurationManager.cs b/tracer/src/Datadog.Trace/Configuration/DynamicConfigurationManager.cs index 01f431267baf..bcebc7ad13a9 100644 --- a/tracer/src/Datadog.Trace/Configuration/DynamicConfigurationManager.cs +++ b/tracer/src/Datadog.Trace/Configuration/DynamicConfigurationManager.cs @@ -13,7 +13,6 @@ using System.Threading; using System.Threading.Tasks; using Datadog.Trace.Configuration.ConfigurationSources; -using Datadog.Trace.Configuration.ConfigurationSources.Telemetry; using Datadog.Trace.Configuration.Telemetry; using Datadog.Trace.Debugger; using Datadog.Trace.Debugger.Configurations; @@ -68,79 +67,17 @@ public void Dispose() } } - internal static void OnlyForTests_ApplyConfiguration(IConfigurationSource dynamicConfig) + internal static void OnlyForTests_ApplyConfiguration(IConfigurationSource dynamicConfig, TracerSettings tracerSettings) { - OnConfigurationChanged(dynamicConfig); + OnConfigurationChanged(dynamicConfig, tracerSettings); } - private static void OnConfigurationChanged(IConfigurationSource dynamicConfig) + private static void OnConfigurationChanged(IConfigurationSource dynamicConfig, TracerSettings tracerSettings) { - var tracerSettings = Tracer.Instance.Settings; - var manualSource = GlobalConfigurationSource.ManualConfigurationSource; - var mutableSettings = manualSource.UseDefaultSources - ? tracerSettings.InitialMutableSettings - : MutableSettings.CreateWithoutDefaultSources(tracerSettings); - - // We save this immediately, even if there's no manifest changes in the final settings - GlobalConfigurationSource.UpdateDynamicConfigConfigurationSource(dynamicConfig); - - OnConfigurationChanged( - dynamicConfig, - manualSource, - mutableSettings, - tracerSettings, - // TODO: In the future this will 'live' elsewhere - currentSettings: tracerSettings.MutableSettings, - new ConfigurationTelemetry(), - new OverrideErrorLog()); // TODO: We'll later report these - } - - private static void OnConfigurationChanged( - IConfigurationSource dynamicConfig, - ManualInstrumentationConfigurationSourceBase manualConfig, - MutableSettings initialSettings, - TracerSettings tracerSettings, - MutableSettings currentSettings, - ConfigurationTelemetry telemetry, - OverrideErrorLog errorLog) - { - var newMutableSettings = MutableSettings.CreateUpdatedMutableSettings( - dynamicConfig, - manualConfig, - initialSettings, - tracerSettings, - telemetry, - errorLog); - - TracerSettings newSettings; - if (currentSettings.Equals(newMutableSettings)) - { - Log.Debug("No changes detected in the new dynamic configuration"); - // Even though there were no "real" changes, there may be _effective_ changes in telemetry that - // need to be recorded (e.g. the customer set the value in code but it was already set via - // env vars). We _should_ record exporter settings too, but that introduces a bunch of complexity - // which we'll resolve later anyway, so just have that gap for now (it's very niche). - // If there are changes, they're recorded automatically in Tracer.Configure() - telemetry.CopyTo(TelemetryFactory.Config); - newSettings = tracerSettings; - } - else + var wasUpdated = tracerSettings.Manager.UpdateDynamicConfigurationSettings(dynamicConfig, TelemetryFactory.Config); + if (wasUpdated) { - Log.Information("Applying new dynamic configuration"); - - newSettings = tracerSettings with { MutableSettings = newMutableSettings }; - - /* - if (debugLogsEnabled != null && debugLogsEnabled.Value != GlobalSettings.Instance.DebugEnabled) - { - GlobalSettings.SetDebugEnabled(debugLogsEnabled.Value); - Security.Instance.SetDebugEnabled(debugLogsEnabled.Value); - - NativeMethods.UpdateSettings(new[] { ConfigurationKeys.DebugEnabled }, new[] { debugLogsEnabled.Value ? "1" : "0" }); - } - */ - - Tracer.Configure(newSettings); + Log.Information("Setting updates made via dynamic configuration were applied"); } // TODO: This might not record the config in the correct order in future, but would require @@ -173,7 +110,7 @@ private static void OnConfigurationChanged( var newDebuggerSettings = oldDebuggerSettings with { DynamicSettings = dynamicDebuggerSettings }; - DebuggerManager.Instance.UpdateConfiguration(newSettings, newDebuggerSettings) + DebuggerManager.Instance.UpdateConfiguration(Tracer.Instance.Settings, newDebuggerSettings) .ContinueWith(t => Log.Error(t?.Exception, "Error updating dynamic configuration for debugger"), TaskContinuationOptions.OnlyOnFaulted); } @@ -247,7 +184,8 @@ private ApplyDetails[] ConfigurationUpdated( private void ApplyMergedConfiguration(List remoteConfigurations) { // Get current service/environment for filtering - var currentSettings = Tracer.Instance.CurrentTraceSettings.Settings; + var tracer = Tracer.Instance; + var currentSettings = tracer.CurrentTraceSettings.Settings; var mergedConfigJToken = ApmTracingConfigMerger.MergeConfigurations( remoteConfigurations, @@ -256,7 +194,7 @@ private void ApplyMergedConfiguration(List remoteConfigurat var configurationSource = new DynamicConfigConfigurationSource(mergedConfigJToken, ConfigurationOrigins.RemoteConfig); - OnConfigurationChanged(configurationSource); + OnConfigurationChanged(configurationSource, tracer.Settings); } } } diff --git a/tracer/src/Datadog.Trace/Configuration/MutableSettings.cs b/tracer/src/Datadog.Trace/Configuration/MutableSettings.cs index df7516c7d0f6..7881d83c2199 100644 --- a/tracer/src/Datadog.Trace/Configuration/MutableSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/MutableSettings.cs @@ -1087,6 +1087,13 @@ public static MutableSettings CreateWithoutDefaultSources(TracerSettings tracerS new OverrideErrorLog(), tracerSettings); + public static MutableSettings CreateForTesting(TracerSettings tracerSettings, Dictionary settings) + => CreateInitialMutableSettings( + new DictionaryConfigurationSource(settings.ToDictionary(x => x.Key, x => x.Value?.ToString()!)), + new ConfigurationTelemetry(), + new OverrideErrorLog(), + tracerSettings); + private static ConfigurationBuilder.ClassConfigurationResultWithKey> RemapOtelTags( in ConfigurationBuilder.ClassConfigurationResultWithKey> original) { diff --git a/tracer/src/Datadog.Trace/Configuration/SettingsManager.cs b/tracer/src/Datadog.Trace/Configuration/SettingsManager.cs new file mode 100644 index 000000000000..8a0005207c0a --- /dev/null +++ b/tracer/src/Datadog.Trace/Configuration/SettingsManager.cs @@ -0,0 +1,245 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Threading; +using Datadog.Trace.Configuration.ConfigurationSources; +using Datadog.Trace.Configuration.ConfigurationSources.Telemetry; +using Datadog.Trace.Configuration.Telemetry; +using Datadog.Trace.Logging; + +namespace Datadog.Trace.Configuration; + +public partial record TracerSettings +{ + internal class SettingsManager( + TracerSettings tracerSettings, + MutableSettings initialMutable, + ExporterSettings initialExporter) + { + private readonly TracerSettings _tracerSettings = tracerSettings; + private readonly List _subscribers = []; + + private IConfigurationSource _dynamicConfigurationSource = NullConfigurationSource.Instance; + private ManualInstrumentationConfigurationSourceBase _manualConfigurationSource = + new ManualInstrumentationConfigurationSource(new Dictionary(), useDefaultSources: true); + + private SettingChanges? _latest; + + /// + /// Gets the initial . On app startup, these will be the values read from + /// static sources. To subscribe to updates to these settings, from code or remote config, call . + /// + public MutableSettings InitialMutableSettings { get; } = initialMutable; + + /// + /// Gets the initial . On app startup, these will be the values read from + /// static sources. To subscribe to updates to these settings, from code or remote config, call . + /// + public ExporterSettings InitialExporterSettings { get; } = initialExporter; + + /// + /// Subscribe to changes in and/or . + /// is called whenever these settings change. If the settings have already changed when + /// is called, is synchronously invoked immediately with the latest configuration. + /// Also note that calling twice with the same callback + /// will invoke the callback twice. Callbacks should complete quickly to avoid blocking other operations. + /// + /// The method to invoke + /// An that should be disposed to unsubscribe + public IDisposable SubscribeToChanges(Action callback) + { + var subscription = new SettingChangeSubscription(this, callback); + lock (_subscribers) + { + _subscribers.Add(subscription); + + if (_latest is { } currentConfig) + { + try + { + // If we already have updates, call this immediately + callback(currentConfig); + } + catch (Exception ex) + { + Log.Error(ex, "Error notifying subscriber of updated MutableSettings during subscribe"); + } + } + } + + return subscription; + } + + /// + /// Regenerate the application's new and + /// based on runtime configuration sources. + /// + /// An containing the new settings created by manual configuration (in code) + /// The central to report config telemetry updates to + /// True if changes were detected and consumers were updated, false otherwise + public bool UpdateManualConfigurationSettings( + ManualInstrumentationConfigurationSourceBase manualSource, + IConfigurationTelemetry centralTelemetry) + { + // we lock this whole method so that we can't conflict with UpdateDynamicConfigurationSettings calls too + lock (_subscribers) + { + _manualConfigurationSource = manualSource; + return UpdateSettings(_dynamicConfigurationSource, manualSource, centralTelemetry); + } + } + + /// + /// Regenerate the application's new and + /// based on runtime configuration sources. + /// + /// An for dynamic config via remote config + /// The central to report config telemetry updates to + /// True if changes were detected and consumers were updated, false otherwise + public bool UpdateDynamicConfigurationSettings( + IConfigurationSource dynamicConfigSource, + IConfigurationTelemetry centralTelemetry) + { + lock (_subscribers) + { + _dynamicConfigurationSource = dynamicConfigSource; + return UpdateSettings(dynamicConfigSource, _manualConfigurationSource, centralTelemetry); + } + } + + private bool UpdateSettings( + IConfigurationSource dynamicConfigSource, + ManualInstrumentationConfigurationSourceBase manualSource, + IConfigurationTelemetry centralTelemetry) + { + if (BuildNewSettings(dynamicConfigSource, manualSource, centralTelemetry) is { } newSettings) + { + NotifySubscribers(newSettings); + return true; + } + + return false; + } + + // Internal for testing + internal SettingChanges? BuildNewSettings( + IConfigurationSource dynamicConfigSource, + ManualInstrumentationConfigurationSourceBase manualSource, + IConfigurationTelemetry centralTelemetry) + { + var initialSettings = manualSource.UseDefaultSources + ? InitialMutableSettings + : MutableSettings.CreateWithoutDefaultSources(_tracerSettings); + + var current = _latest; + var currentMutable = current?.UpdatedMutable ?? current?.PreviousMutable ?? InitialMutableSettings; + var currentExporter = current?.UpdatedExporter ?? current?.PreviousExporter ?? InitialExporterSettings; + + var telemetry = new ConfigurationTelemetry(); + var newMutableSettings = MutableSettings.CreateUpdatedMutableSettings( + dynamicConfigSource, + manualSource, + initialSettings, + _tracerSettings, + telemetry, + new OverrideErrorLog()); // TODO: We'll later report these + + // The only exporter setting we currently _allow_ to change is the AgentUri, but if that does change, + // it can mean that _everything_ about the exporter settings changes. To minimize the work to do, and + // to simplify comparisons, we try to read the agent url from the manual setting. If it's missing, not + // set, or unchanged, there's no need to update the exporter settings. + // We only technically need to do this today if _manual_ config changes, not if remote config changes, + // but for simplicity we don't distinguish currently. + var exporterTelemetry = new ConfigurationTelemetry(); + var newRawExporterSettings = ExporterSettings.Raw.CreateUpdatedFromManualConfig( + currentExporter.RawSettings, + manualSource, + exporterTelemetry, + manualSource.UseDefaultSources); + + var isSameMutableSettings = currentMutable.Equals(newMutableSettings); + var isSameExporterSettings = currentExporter.RawSettings.Equals(newRawExporterSettings); + + if (isSameMutableSettings && isSameExporterSettings) + { + Log.Debug("No changes detected in the new configuration"); + // Even though there were no "real" changes, there may be _effective_ changes in telemetry that + // need to be recorded (e.g. the customer set the value in code, but it was already set via + // env vars). We _should_ record exporter settings too, but that introduces a bunch of complexity + // which we'll resolve later anyway, so just have that gap for now (it's very niche). + // If there are changes, they're recorded automatically in ConfigureInternal + telemetry.CopyTo(centralTelemetry); + return null; + } + + Log.Information("Notifying consumers of new settings"); + var updatedMutableSettings = isSameMutableSettings ? null : newMutableSettings; + var updatedExporterSettings = isSameExporterSettings ? null : new ExporterSettings(newRawExporterSettings, exporterTelemetry); + + return new SettingChanges(updatedMutableSettings, updatedExporterSettings, currentMutable, currentExporter); + } + + private void NotifySubscribers(SettingChanges settings) + { + _latest = settings; + + foreach (var subscriber in _subscribers) + { + try + { + subscriber.Notify(settings); + } + catch (Exception ex) + { + Log.Error(ex, "Error notifying subscriber of MutableSettings change"); + } + } + } + + private sealed class SettingChangeSubscription(SettingsManager owner, Action notify) : IDisposable + { + private readonly SettingsManager _owner = owner; + + public Action Notify { get; } = notify; + + public void Dispose() + { + lock (_owner._subscribers) + { + _owner._subscribers.Remove(this); + } + } + } + + public sealed class SettingChanges(MutableSettings? updatedMutable, ExporterSettings? updatedExporter, MutableSettings previousMutable, ExporterSettings previousExporter) + { + /// + /// Gets the new , if they have changed. + /// If there are no changes, returns null. + /// + public MutableSettings? UpdatedMutable { get; } = updatedMutable; + + /// + /// Gets the new , if they have changed. + /// If there are no changes, returns null. + /// + public ExporterSettings? UpdatedExporter { get; } = updatedExporter; + + /// + /// Gets the previous , prior to this update. + /// + public MutableSettings PreviousMutable { get; } = previousMutable; + + /// + /// Gets the previous , prior to this update. + /// + public ExporterSettings PreviousExporter { get; } = previousExporter; + } + } +} diff --git a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs index 79eed12b3322..b64595f40f34 100644 --- a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs @@ -30,7 +30,7 @@ namespace Datadog.Trace.Configuration /// /// Contains Tracer settings. /// - public record TracerSettings + public partial record TracerSettings { private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); private static readonly HashSet DefaultExperimentalFeatures = ["DD_TAGS", ConfigurationKeys.PropagateProcessTags]; @@ -729,6 +729,7 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) = InitialMutableSettings = MutableSettings.CreateInitialMutableSettings(source, telemetry, errorLog, this); MutableSettings = InitialMutableSettings; + Manager = new(this, InitialMutableSettings, Exporter); } internal bool IsRunningInCiVisibility { get; } @@ -1320,6 +1321,8 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) = /// public int PartialFlushMinSpans { get; } + internal SettingsManager Manager { get; } + internal List JsonConfigurationFilePaths { get; } = new(); /// diff --git a/tracer/src/Datadog.Trace/TracerManager.cs b/tracer/src/Datadog.Trace/TracerManager.cs index 89d28ad425ed..51ba3f9caacf 100644 --- a/tracer/src/Datadog.Trace/TracerManager.cs +++ b/tracer/src/Datadog.Trace/TracerManager.cs @@ -687,6 +687,25 @@ private static void OneTimeSetup(TracerSettings tracerSettings) // Record the service discovery metadata ServiceDiscoveryHelper.StoreTracerMetadata(tracerSettings); + + // Register for rebuilding the settings on changes + // TODO: This is only temporary, we want to _stop_ rebuilding everything whenever settings change in the future + // We also don't bother to dispose this because we never unsubscribe + tracerSettings.Manager.SubscribeToChanges(updatedSettings => + { + var newSettings = updatedSettings switch + { + { UpdatedExporter: { } e, UpdatedMutable: { } m } => Tracer.Instance.Settings with { Exporter = e, MutableSettings = m }, + { UpdatedExporter: { } e } => Tracer.Instance.Settings with { Exporter = e }, + { UpdatedMutable: { } m } => Tracer.Instance.Settings with { MutableSettings = m }, + _ => null, + }; + if (newSettings != null) + { + // Update the global instance + Trace.Tracer.Configure(newSettings); + } + }); } private static Task RunShutdownTasksAsync(Exception ex) => RunShutdownTasksAsync(_instance, _heartbeatTimer); diff --git a/tracer/test/Datadog.Trace.Tests/Configuration/DynamicConfigurationTests.cs b/tracer/test/Datadog.Trace.Tests/Configuration/DynamicConfigurationTests.cs index 97e1f3afb1c1..dfb699fd684a 100644 --- a/tracer/test/Datadog.Trace.Tests/Configuration/DynamicConfigurationTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Configuration/DynamicConfigurationTests.cs @@ -29,7 +29,7 @@ public void ApplyServiceMappingToNewTraces() Tracer.Instance.CurrentTraceSettings.GetServiceName("test") .Should().Be($"{Tracer.Instance.DefaultServiceName}-test"); - DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_service_mapping", "test:ok"))); + DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_service_mapping", "test:ok")), Tracer.Instance.Settings); Tracer.Instance.CurrentTraceSettings.GetServiceName("test") .Should().Be($"{Tracer.Instance.DefaultServiceName}-test", "the old configuration should be used inside of the active trace"); @@ -45,13 +45,13 @@ public void ApplyConfigurationTwice() { var tracer = TracerManager.Instance; - DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_sampling_rate", 0.4))); + DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_sampling_rate", 0.4)), tracer.Settings); var newTracer = TracerManager.Instance; newTracer.Should().NotBeSameAs(tracer); - DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_sampling_rate", 0.4))); + DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_sampling_rate", 0.4)), tracer.Settings); TracerManager.Instance.Should().BeSameAs(newTracer); } @@ -60,14 +60,17 @@ public void ApplyConfigurationTwice() public void ApplyTagsToDirectLogs() { var tracerSettings = TracerSettings.Create(new() { { ConfigurationKeys.GlobalTags, "key1:value1" } }); - TracerManager.ReplaceGlobalManager(tracerSettings, TracerManagerFactory.Instance); - TracerManager.Instance.DirectLogSubmission.Formatter.Tags.Should().Be("key1:value1"); + // emulate the one-time subscribe that TracerManager.Instance does + var tracerManager = TracerManagerFactory.Instance.CreateTracerManager(tracerSettings, null); + using var sub = tracerSettings.Manager.SubscribeToChanges(x => tracerManager = UpdateTracerManager(x, tracerSettings, tracerManager)); + + tracerManager.DirectLogSubmission.Formatter.Tags.Should().Be("key1:value1"); var configBuilder = CreateConfig(("tracing_tags", new[] { "key2:value2" })); - DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(configBuilder); + DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(configBuilder, tracerSettings); - TracerManager.Instance.DirectLogSubmission.Formatter.Tags.Should().Be("key2:value2"); + tracerManager.DirectLogSubmission.Formatter.Tags.Should().Be("key2:value2"); } [Fact] @@ -79,14 +82,15 @@ public void DoesNotOverrideDirectLogsTags() { ConfigurationKeys.DirectLogSubmission.EnabledIntegrations, "xunit" }, { ConfigurationKeys.GlobalTags, "key2:value2" }, }); - TracerManager.ReplaceGlobalManager(tracerSettings, TracerManagerFactory.Instance); + var tracerManager = TracerManagerFactory.Instance.CreateTracerManager(tracerSettings, null); + using var sub = tracerSettings.Manager.SubscribeToChanges(x => tracerManager = UpdateTracerManager(x, tracerSettings, tracerManager)); - TracerManager.Instance.DirectLogSubmission.Formatter.Tags.Should().Be("key1:value1"); + tracerManager.DirectLogSubmission.Formatter.Tags.Should().Be("key1:value1"); var configBuilder = CreateConfig(("tracing_tags", new[] { "key3:value3" })); - DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(configBuilder); + DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(configBuilder, tracerSettings); - TracerManager.Instance.DirectLogSubmission.Formatter.Tags.Should().Be("key1:value1"); + tracerManager.DirectLogSubmission.Formatter.Tags.Should().Be("key1:value1"); } [Fact] @@ -97,32 +101,34 @@ public void DoesNotReplaceRuntimeMetricsWriter() { ConfigurationKeys.RuntimeMetricsEnabled, "true" }, { ConfigurationKeys.GlobalTags, "key1:value1" }, }); - TracerManager.ReplaceGlobalManager(tracerSettings, TracerManagerFactory.Instance); + var tracerManager = TracerManagerFactory.Instance.CreateTracerManager(tracerSettings, null); + using var sub = tracerSettings.Manager.SubscribeToChanges(x => tracerManager = UpdateTracerManager(x, tracerSettings, tracerManager)); - var previousRuntimeMetrics = TracerManager.Instance.RuntimeMetrics; + var previousRuntimeMetrics = tracerManager.RuntimeMetrics; var configBuilder = CreateConfig(("tracing_tags", new[] { "key2:value2" })); - DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(configBuilder); + DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(configBuilder, tracerSettings); - TracerManager.Instance.RuntimeMetrics.Should().Be(previousRuntimeMetrics); + tracerManager.RuntimeMetrics.Should().Be(previousRuntimeMetrics); } [Fact] public void EnableTracing() { var tracerSettings = new TracerSettings(); - TracerManager.ReplaceGlobalManager(tracerSettings, TracerManagerFactory.Instance); + var tracerManager = TracerManagerFactory.Instance.CreateTracerManager(tracerSettings, null); + using var sub = tracerSettings.Manager.SubscribeToChanges(x => tracerManager = UpdateTracerManager(x, tracerSettings, tracerManager)); // tracing is enabled by default - TracerManager.Instance.Settings.TraceEnabled.Should().BeTrue(); + tracerManager.Settings.TraceEnabled.Should().BeTrue(); // disable "remotely" - DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_enabled", false))); - TracerManager.Instance.Settings.TraceEnabled.Should().BeFalse(); + DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_enabled", false)), tracerSettings); + tracerManager.Settings.TraceEnabled.Should().BeFalse(); // re-enable "remotely" - DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_enabled", true))); - TracerManager.Instance.Settings.TraceEnabled.Should().BeTrue(); + DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(CreateConfig(("tracing_enabled", true)), tracerSettings); + tracerManager.Settings.TraceEnabled.Should().BeTrue(); } [Fact] @@ -141,12 +147,13 @@ public void SetSamplingRules() { "DD_TRACE_SAMPLING_RULES", localSamplingRulesJson } }); - TracerManager.ReplaceGlobalManager(tracerSettings, TracerManagerFactory.Instance); + var tracerManager = TracerManagerFactory.Instance.CreateTracerManager(tracerSettings, null); + using var sub = tracerSettings.Manager.SubscribeToChanges(x => tracerManager = UpdateTracerManager(x, tracerSettings, tracerManager)); - TracerManager.Instance.Settings.CustomSamplingRules.Should().Be(localSamplingRulesJson); - TracerManager.Instance.Settings.CustomSamplingRulesIsRemote.Should().BeFalse(); + tracerManager.Settings.CustomSamplingRules.Should().Be(localSamplingRulesJson); + tracerManager.Settings.CustomSamplingRulesIsRemote.Should().BeFalse(); - var rules = ((TraceSampler)TracerManager.Instance.PerTraceSettings.TraceSampler)!.GetRules(); + var rules = ((TraceSampler)tracerManager.PerTraceSettings.TraceSampler)!.GetRules(); rules.Should() .BeEquivalentTo( @@ -171,13 +178,13 @@ public void SetSamplingRules() }; var configBuilder = CreateConfig(("tracing_sampling_rules", remoteSamplingRulesConfig)); - DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(configBuilder); + DynamicConfigurationManager.OnlyForTests_ApplyConfiguration(configBuilder, tracerSettings); var remoteSamplingRulesJson = JsonConvert.SerializeObject(remoteSamplingRulesConfig); - TracerManager.Instance.Settings.CustomSamplingRules.Should().Be(remoteSamplingRulesJson); - TracerManager.Instance.Settings.CustomSamplingRulesIsRemote.Should().BeTrue(); + tracerManager.Settings.CustomSamplingRules.Should().Be(remoteSamplingRulesJson); + tracerManager.Settings.CustomSamplingRulesIsRemote.Should().BeTrue(); - rules = ((TraceSampler)TracerManager.Instance.PerTraceSettings.TraceSampler)!.GetRules(); + rules = ((TraceSampler)tracerManager.PerTraceSettings.TraceSampler)!.GetRules(); // new list should include the remote rules, not the local rules rules.Should() @@ -271,5 +278,23 @@ private static DynamicConfigConfigurationSource CreateConfig(params (string Key, return new DynamicConfigConfigurationSource(configObj, ConfigurationOrigins.RemoteConfig); } + + private static TracerManager UpdateTracerManager(TracerSettings.SettingsManager.SettingChanges updates, TracerSettings settings, TracerManager tracerManager) + { + var newSettings = updates switch + { + { UpdatedExporter: { } e, UpdatedMutable: { } m } => settings with { Exporter = e, MutableSettings = m }, + { UpdatedExporter: { } e } => settings with { Exporter = e }, + { UpdatedMutable: { } m } => settings with { MutableSettings = m }, + _ => null, + }; + + if (newSettings != null) + { + tracerManager = TracerManagerFactory.Instance.CreateTracerManager(newSettings, tracerManager); + } + + return tracerManager; + } } } diff --git a/tracer/test/Datadog.Trace.Tests/Configuration/TracerSettingsSettingManagerTests.cs b/tracer/test/Datadog.Trace.Tests/Configuration/TracerSettingsSettingManagerTests.cs new file mode 100644 index 000000000000..a8dff10efd2c --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/Configuration/TracerSettingsSettingManagerTests.cs @@ -0,0 +1,31 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Collections.Generic; +using System.Linq; +using Datadog.Trace.Configuration; +using Datadog.Trace.Configuration.ConfigurationSources; +using Datadog.Trace.Configuration.Telemetry; +using FluentAssertions; +using Xunit; +using SettingChanges = Datadog.Trace.Configuration.TracerSettings.SettingsManager.SettingChanges; + +namespace Datadog.Trace.Tests.Configuration; + +public class TracerSettingsSettingManagerTests +{ + [Fact] + public void UpdatingTracerSettingsDoesNotReplaceSettingsManager() + { + var tracerSettings = TracerSettings.Create([]); + tracerSettings.MutableSettings.Should().BeSameAs(tracerSettings.InitialMutableSettings); + + var originalManager = tracerSettings.Manager; + var newSettings = tracerSettings with { MutableSettings = MutableSettings.CreateForTesting(tracerSettings, []) }; + + newSettings.MutableSettings.Should().NotBeSameAs(newSettings.InitialMutableSettings); + newSettings.Manager.Should().BeSameAs(originalManager); + } +}