|
| 1 | +// <copyright file="SettingsManager.cs" company="Datadog"> |
| 2 | +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. |
| 3 | +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. |
| 4 | +// </copyright> |
| 5 | + |
| 6 | +#nullable enable |
| 7 | + |
| 8 | +using System; |
| 9 | +using System.Collections.Generic; |
| 10 | +using System.Threading; |
| 11 | +using Datadog.Trace.Configuration.ConfigurationSources; |
| 12 | +using Datadog.Trace.Configuration.ConfigurationSources.Telemetry; |
| 13 | +using Datadog.Trace.Configuration.Telemetry; |
| 14 | +using Datadog.Trace.Logging; |
| 15 | + |
| 16 | +namespace Datadog.Trace.Configuration; |
| 17 | + |
| 18 | +internal class SettingsManager( |
| 19 | + TracerSettings tracerSettings, MutableSettings initialMutable, ExporterSettings initialExporter) |
| 20 | +{ |
| 21 | + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<SettingsManager>(); |
| 22 | + private readonly TracerSettings _tracerSettings = tracerSettings; |
| 23 | + private readonly List<SettingChangeSubscription> _subscribers = []; |
| 24 | + private SettingChanges? _latest; |
| 25 | + |
| 26 | + /// <summary> |
| 27 | + /// Gets the initial <see cref="MutableSettings"/>. On app startup, these will be the values read from |
| 28 | + /// static sources. To subscribe to updates to these settings, from code or remote config, call <see cref="SubscribeToChanges"/>. |
| 29 | + /// </summary> |
| 30 | + public MutableSettings InitialMutableSettings { get; } = initialMutable; |
| 31 | + |
| 32 | + /// <summary> |
| 33 | + /// Gets the initial <see cref="ExporterSettings"/>. On app startup, these will be the values read from |
| 34 | + /// static sources. To subscribe to updates to these settings, from code or remote config, call <see cref="SubscribeToChanges"/>. |
| 35 | + /// </summary> |
| 36 | + public ExporterSettings InitialExporterSettings { get; } = initialExporter; |
| 37 | + |
| 38 | + /// <summary> |
| 39 | + /// Subscribe to changes in <see cref="MutableSettings"/> and/or <see cref="ExporterSettings"/>. |
| 40 | + /// Called whenever these settings change. If the settings have already changed when <see cref="SubscribeToChanges"/> |
| 41 | + /// is called, <paramref name="callback"/> is invoked immediately with the latest configuration. |
| 42 | + /// Also note that calling <see cref="SubscribeToChanges"/> twice with the same callback |
| 43 | + /// will invoke the callback twice. |
| 44 | + /// </summary> |
| 45 | + /// <param name="callback">The method to invoke</param> |
| 46 | + /// <returns>An <see cref="IDisposable"/> that should be disposed to unsubscribe</returns> |
| 47 | + public IDisposable SubscribeToChanges(Action<SettingChanges> callback) |
| 48 | + { |
| 49 | + var subscription = new SettingChangeSubscription(this, callback); |
| 50 | + lock (_subscribers) |
| 51 | + { |
| 52 | + _subscribers.Add(subscription); |
| 53 | + } |
| 54 | + |
| 55 | + if (Volatile.Read(ref _latest) is { } currentConfig) |
| 56 | + { |
| 57 | + try |
| 58 | + { |
| 59 | + // If we already have updates, call this immediately |
| 60 | + callback(currentConfig); |
| 61 | + } |
| 62 | + catch (Exception ex) |
| 63 | + { |
| 64 | + Log.Error(ex, "Error notifying subscriber of updated MutableSettings during subscribe"); |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + return subscription; |
| 69 | + } |
| 70 | + |
| 71 | + /// <summary> |
| 72 | + /// Regenerate the application's new <see cref="MutableSettings"/> based on runtime configuration sources |
| 73 | + /// </summary> |
| 74 | + /// <param name="dynamicConfigSource">An <see cref="IConfigurationSource"/> for dynamic config via remote config</param> |
| 75 | + /// <param name="manualSource">An <see cref="IConfigurationSource"/> for manual configuration (in code)</param> |
| 76 | + /// <param name="centralTelemetry">The central <see cref="IConfigurationTelemetry"/> to report config telemetry updates to</param> |
| 77 | + /// <returns>True if changes were detected and consumers were updated, false otherwise</returns> |
| 78 | + public bool UpdateSettings( |
| 79 | + IConfigurationSource dynamicConfigSource, |
| 80 | + ManualInstrumentationConfigurationSourceBase manualSource, |
| 81 | + IConfigurationTelemetry centralTelemetry) |
| 82 | + { |
| 83 | + if (BuildNewSettings(dynamicConfigSource, manualSource, centralTelemetry) is { } newSettings) |
| 84 | + { |
| 85 | + NotifySubscribers(newSettings); |
| 86 | + return true; |
| 87 | + } |
| 88 | + |
| 89 | + return false; |
| 90 | + } |
| 91 | + |
| 92 | + // Internal for testing |
| 93 | + internal SettingChanges? BuildNewSettings( |
| 94 | + IConfigurationSource dynamicConfigSource, |
| 95 | + ManualInstrumentationConfigurationSourceBase manualSource, |
| 96 | + IConfigurationTelemetry centralTelemetry) |
| 97 | + { |
| 98 | + var initialSettings = manualSource.UseDefaultSources |
| 99 | + ? InitialMutableSettings |
| 100 | + : MutableSettings.CreateWithoutDefaultSources(_tracerSettings); |
| 101 | + |
| 102 | + var current = Volatile.Read(ref _latest); |
| 103 | + var currentMutable = current?.Mutable ?? InitialMutableSettings; |
| 104 | + var currentExporter = current?.Exporter ?? InitialExporterSettings; |
| 105 | + |
| 106 | + var telemetry = new ConfigurationTelemetry(); |
| 107 | + var newMutableSettings = MutableSettings.CreateUpdatedMutableSettings( |
| 108 | + dynamicConfigSource, |
| 109 | + manualSource, |
| 110 | + initialSettings, |
| 111 | + _tracerSettings, |
| 112 | + telemetry, |
| 113 | + new OverrideErrorLog()); // TODO: We'll later report these |
| 114 | + |
| 115 | + // The only exporter setting we currently _allow_ to change is the AgentUri, but if that does change, |
| 116 | + // it can mean that _everything_ about the exporter settings changes. To minimize the work to do, and |
| 117 | + // to simplify comparisons, we try to read the agent url from the manual setting. If it's missing, not |
| 118 | + // set, or unchanged, there's no need to update the exporter settings. |
| 119 | + // We only technically need to do this today if _manual_ config changes, not if remote config changes, |
| 120 | + // but for simplicity we don't distinguish currently. |
| 121 | + var exporterTelemetry = new ConfigurationTelemetry(); |
| 122 | + var newRawExporterSettings = ExporterSettings.Raw.CreateUpdatedFromManualConfig( |
| 123 | + currentExporter.RawSettings, |
| 124 | + manualSource, |
| 125 | + exporterTelemetry, |
| 126 | + manualSource.UseDefaultSources); |
| 127 | + |
| 128 | + var isSameMutableSettings = currentMutable.Equals(newMutableSettings); |
| 129 | + var isSameExporterSettings = currentExporter.RawSettings.Equals(newRawExporterSettings); |
| 130 | + |
| 131 | + if (isSameMutableSettings && isSameExporterSettings) |
| 132 | + { |
| 133 | + Log.Debug("No changes detected in the new configuration"); |
| 134 | + // Even though there were no "real" changes, there may be _effective_ changes in telemetry that |
| 135 | + // need to be recorded (e.g. the customer set the value in code, but it was already set via |
| 136 | + // env vars). We _should_ record exporter settings too, but that introduces a bunch of complexity |
| 137 | + // which we'll resolve later anyway, so just have that gap for now (it's very niche). |
| 138 | + // If there are changes, they're recorded automatically in ConfigureInternal |
| 139 | + telemetry.CopyTo(centralTelemetry); |
| 140 | + return null; |
| 141 | + } |
| 142 | + |
| 143 | + Log.Information("Notifying consumers of new settings"); |
| 144 | + var updatedMutableSettings = isSameMutableSettings ? null : newMutableSettings; |
| 145 | + var updatedExporterSettings = isSameExporterSettings ? null : new ExporterSettings(newRawExporterSettings, exporterTelemetry); |
| 146 | + |
| 147 | + return new SettingChanges(updatedMutableSettings, updatedExporterSettings); |
| 148 | + } |
| 149 | + |
| 150 | + private void NotifySubscribers(SettingChanges settings) |
| 151 | + { |
| 152 | + // Strictly, for safety, we only need to lock in the subscribers list access. However, |
| 153 | + // there's nothing to prevent NotifySubscribers being called concurrently, |
| 154 | + // which could result in weird out-of-order notifications for customers. So for simplicity |
| 155 | + // we just lock the whole method to ensure serialized updates. |
| 156 | + |
| 157 | + lock (_subscribers) |
| 158 | + { |
| 159 | + var subscribers = _subscribers; |
| 160 | + Volatile.Write(ref _latest, settings); |
| 161 | + |
| 162 | + foreach (var subscriber in subscribers) |
| 163 | + { |
| 164 | + try |
| 165 | + { |
| 166 | + subscriber.Notify(settings); |
| 167 | + } |
| 168 | + catch (Exception ex) |
| 169 | + { |
| 170 | + Log.Error(ex, "Error notifying subscriber of MutableSettings change"); |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + private sealed class SettingChangeSubscription(SettingsManager owner, Action<SettingChanges> notify) : IDisposable |
| 177 | + { |
| 178 | + private readonly SettingsManager _owner = owner; |
| 179 | + |
| 180 | + public Action<SettingChanges> Notify { get; } = notify; |
| 181 | + |
| 182 | + public void Dispose() |
| 183 | + { |
| 184 | + lock (_owner._subscribers) |
| 185 | + { |
| 186 | + _owner._subscribers.Remove(this); |
| 187 | + } |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + public sealed class SettingChanges(MutableSettings? mutableSettings, ExporterSettings? exporterSettings) |
| 192 | + { |
| 193 | + /// <summary> |
| 194 | + /// Gets the new <see cref="MutableSettings"/>, if they have changed. |
| 195 | + /// If there are no changes, returns null. |
| 196 | + /// </summary> |
| 197 | + public MutableSettings? Mutable { get; } = mutableSettings; |
| 198 | + |
| 199 | + /// <summary> |
| 200 | + /// Gets the new <see cref="ExporterSettings"/>, if they have changed. |
| 201 | + /// If there are no changes, returns null. |
| 202 | + /// </summary> |
| 203 | + public ExporterSettings? Exporter { get; } = exporterSettings; |
| 204 | + } |
| 205 | +} |
0 commit comments