Skip to content

Commit 6c0290a

Browse files
committed
Create a SettingsManager for tracking and notifying about settings changes
1 parent bc4d1ed commit 6c0290a

File tree

1 file changed

+205
-0
lines changed

1 file changed

+205
-0
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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

Comments
 (0)