Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config refactor - fix struct/class T nullable ref issues, and allow "raw" access to ConfigurationResult #5715

Merged
merged 3 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions tracer/src/Datadog.Trace/AppSec/SecuritySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,13 @@ public SecuritySettings(IConfigurationSource? source, IConfigurationTelemetry te
.WithKeys(ConfigurationKeys.AppSec.JsonBlockedTemplate)
.AsString(SecurityConstants.BlockedJsonTemplate);

bool isEnabledSet = true;

bool GetEnabledDefaultValue()
{
isEnabledSet = false;
return false;
}

// both should default to false
var enabledEnvVar = config
.WithKeys(ConfigurationKeys.AppSec.Enabled)
.AsBool(GetEnabledDefaultValue, null);
.AsBoolResult();

Enabled = enabledEnvVar.Value;
CanBeToggled = !isEnabledSet;
CanBeToggled = !enabledEnvVar.ConfigurationResult.IsValid;
Enabled = enabledEnvVar.WithDefault(false);

Rules = config.WithKeys(ConfigurationKeys.AppSec.Rules).AsString();
CustomIpHeader = config.WithKeys(ConfigurationKeys.AppSec.CustomIpHeader).AsString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,59 @@ public ConfigurationBuilder(IConfigurationSource source, IConfigurationTelemetry

public HasKeys WithKeys(string key, string fallbackKey1, string fallbackKey2, string fallbackKey3) => new(_source, _telemetry, key, fallbackKey1, fallbackKey2, fallbackKey3);

private static bool TryHandleResult<T>(
Copy link
Collaborator

@zacharycmontoya zacharycmontoya Jun 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It looks like you call this in GetAs<T> knowing that the getDefaultValue argument is always non-null, and then in GetAsClass<T> / GetAsStruct<T> you always pass null. Could you break out TryHandleResult<T> into two overloads, one where it is present and one where it is not nullable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you break out TryHandleResult into two overloads, one where it is present and one where it is not nullable?

I definitely could, I was just trying to reduce the amount of duplication 😅 Maybe it's not worth the hassle 🤷‍♂️

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the really annoying thing here is that I can't do that with method overloads. So we're need to call it something like TryHandleResultWithOptionalDefault or something 😅 and it still doesn't solve the need to use ! because of the weirdness with nullables and struct/classes, so I'm not sure it gains a lot overall 🤔

IConfigurationTelemetry telemetry,
string key,
ConfigurationResult<T> result,
bool recordValue,
Func<DefaultResult<T>>? getDefaultValue,
[NotNullIfNotNull(nameof(getDefaultValue))] out T? value)
{
if (result is { Result: { } ddResult, IsValid: true })
{
value = ddResult;
return true;
}

// don't have a default value, so caller (which knows what the <T> is needs
// to return the default value. Necessary because we can't create a generic
// method that works "correctly" for both value types and reference types.
if (getDefaultValue is null)
{
value = default;
return false;
}

var defaultValue = getDefaultValue();
RecordTelemetry(telemetry, key, recordValue, defaultValue);

// The compiler complains about this, because technically you _could_ call it as `TryHandleResult<int?>` (for example)
// in which case Func<DefaultResult<T>> _could_ return a `null` value, so the `[NotNullIfNotNull]` annotation
// would be wrong. In practice, we control the calls to this method, and we know that T is always non-null
// so it's safe to use the dammit here.
value = defaultValue.Result!;
return true;
}

private static void RecordTelemetry<T>(IConfigurationTelemetry telemetry, string key, bool recordValue, DefaultResult<T> defaultValue)
{
switch (defaultValue.Result)
{
case int intVal:
telemetry.Record(key, intVal, ConfigurationOrigins.Default);
break;
case double doubleVal:
telemetry.Record(key, doubleVal, ConfigurationOrigins.Default);
break;
case bool boolVal:
telemetry.Record(key, boolVal, ConfigurationOrigins.Default);
break;
default:
telemetry.Record(key, defaultValue.TelemetryValue, recordValue, ConfigurationOrigins.Default);
break;
}
}

internal readonly struct HasKeys
{
public HasKeys(ITelemeteredConfigurationSource source, IConfigurationTelemetry telemetry, string key, string? fallbackKey1 = null, string? fallbackKey2 = null, string? fallbackKey3 = null)
Expand Down Expand Up @@ -196,26 +249,32 @@ public string AsString(string defaultValue, Func<string, bool>? validator)
return defaultValue;
}

// We have to use different methods for class/struct when we _don't_ have a null value, because NRTs don't work properly otherwise
[return: NotNullIfNotNull(nameof(getDefaultValue))]
public T? GetAs<T>(Func<DefaultResult<T>>? getDefaultValue, Func<T, bool>? validator, Func<string, ParsingResult<T>> converter)
public T GetAs<T>(Func<DefaultResult<T>> getDefaultValue, Func<T, bool>? validator, Func<string, ParsingResult<T>> converter)
{
var result = GetAs(validator, converter);
return TryHandleResult(Telemetry, Key, result, recordValue: true, getDefaultValue, out var value)
? value
: default!; // TryHandleResult always returns true as getDefaultValue != null
}

// We have a valid value
if (result is { Result: { } value, IsValid: true })
{
return value;
}

// don't have a valid value
if (getDefaultValue is null)
{
return default;
}
public T? GetAsClass<T>(Func<T, bool>? validator, Func<string, ParsingResult<T>> converter)
where T : class
{
var result = GetAs(validator, converter);
return TryHandleResult(Telemetry, Key, result, recordValue: true, getDefaultValue: null, out var value)
? value
: null;
}

var defaultValue = getDefaultValue();
Telemetry.Record(Key, defaultValue.TelemetryValue, recordValue: true, ConfigurationOrigins.Default);
return defaultValue.Result!;
public T? GetAsStruct<T>(Func<T, bool>? validator, Func<string, ParsingResult<T>> converter)
where T : struct
{
var result = GetAs(validator, converter);
return TryHandleResult(Telemetry, Key, result, recordValue: true, getDefaultValue: null, out var value)
? value
: null;
}

[return: NotNullIfNotNull(nameof(getDefaultValue))]
Expand Down Expand Up @@ -587,6 +646,90 @@ public bool AsBoolWithOpenTelemetryMapping(bool defaultValue, string openTelemet
return null;
}

// ****************
// Raw result accessors
// ****************
public ClassConfigurationResultWithKey<string> AsStringResult()
=> new(Telemetry, Key, recordValue: true, configurationResult: GetStringResult(validator: null, converter: null, recordValue: true));

public ClassConfigurationResultWithKey<string> AsStringResult(Func<string, ParsingResult<string>>? converter)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetStringResult(validator: null, converter, recordValue: true));

public ClassConfigurationResultWithKey<string> AsStringResult(Func<string, bool>? validator, Func<string, ParsingResult<string>>? converter)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetStringResult(validator, converter, recordValue: true));

public ClassConfigurationResultWithKey<string> AsRedactedStringResult()
=> new(Telemetry, Key, recordValue: false, configurationResult: GetStringResult(validator: null, converter: null, recordValue: false));

public ClassConfigurationResultWithKey<string> AsRedactedStringResult(Func<string, ParsingResult<string>>? converter)
=> new(Telemetry, Key, recordValue: false, configurationResult: GetStringResult(validator: null, converter, recordValue: false));

public ClassConfigurationResultWithKey<string> AsRedactedStringResult(Func<string, bool>? validator, Func<string, ParsingResult<string>>? converter)
=> new(Telemetry, Key, recordValue: false, configurationResult: GetStringResult(validator, converter, recordValue: false));

public ClassConfigurationResultWithKey<string> AsStringResult(Func<string, bool>? validator, Func<string, ParsingResult<string>>? converter, bool recordValue)
=> new(Telemetry, Key, recordValue, GetStringResult(validator, converter, recordValue));

// bool
public StructConfigurationResultWithKey<bool> AsBoolResult()
=> new(Telemetry, Key, recordValue: true, configurationResult: GetBoolResult(validator: null, converter: null));

public StructConfigurationResultWithKey<bool> AsBoolResult(Func<string, ParsingResult<bool>>? converter)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetBoolResult(validator: null, converter));

public StructConfigurationResultWithKey<bool> AsBoolResult(Func<bool, bool>? validator, Func<string, ParsingResult<bool>>? converter)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetBoolResult(validator, converter));

// T
public ClassConfigurationResultWithKey<T> GetAsClassResult<T>(Func<string, ParsingResult<T>> converter)
where T : class
=> new(Telemetry, Key, recordValue: true, configurationResult: GetAs(validator: null, converter));

public ClassConfigurationResultWithKey<T> GetAsClassResult<T>(Func<T, bool>? validator, Func<string, ParsingResult<T>> converter)
where T : class
=> new(Telemetry, Key, recordValue: true, configurationResult: GetAs(validator, converter));

public StructConfigurationResultWithKey<T> GetAsStructResult<T>(Func<string, ParsingResult<T>> converter)
where T : struct
=> new(Telemetry, Key, recordValue: true, configurationResult: GetAs(validator: null, converter));

public StructConfigurationResultWithKey<T> GetAsStructResult<T>(Func<T, bool>? validator, Func<string, ParsingResult<T>> converter)
where T : struct
=> new(Telemetry, Key, recordValue: true, configurationResult: GetAs(validator, converter));

// int
public StructConfigurationResultWithKey<int> AsInt32Result()
=> new(Telemetry, Key, recordValue: true, configurationResult: GetInt32Result(validator: null, converter: null));

public StructConfigurationResultWithKey<int> AsInt32Result(Func<string, ParsingResult<int>>? converter)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetInt32Result(validator: null, converter));

public StructConfigurationResultWithKey<int> AsInt32Result(Func<int, bool>? validator, Func<string, ParsingResult<int>>? converter)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetInt32Result(validator, converter));

// double
public StructConfigurationResultWithKey<double> AsDoubleResult()
=> new(Telemetry, Key, recordValue: true, configurationResult: GetDoubleResult(validator: null, converter: null));

public StructConfigurationResultWithKey<double> AsDoubleResult(Func<string, ParsingResult<double>>? converter)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetDoubleResult(validator: null, converter));

public StructConfigurationResultWithKey<double> AsDoubleResult(Func<double, bool>? validator, Func<string, ParsingResult<double>>? converter)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetDoubleResult(validator, converter));

// dictionary
public ClassConfigurationResultWithKey<IDictionary<string, string>> AsDictionaryResult()
=> new(Telemetry, Key, recordValue: true, configurationResult: GetDictionaryResult(allowOptionalMappings: false, separator: ':'));

public ClassConfigurationResultWithKey<IDictionary<string, string>> AsDictionaryResult(bool allowOptionalMappings)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetDictionaryResult(allowOptionalMappings, separator: ':'));

public ClassConfigurationResultWithKey<IDictionary<string, string>> AsDictionaryResult(char separator)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetDictionaryResult(allowOptionalMappings: false, separator));

public ClassConfigurationResultWithKey<IDictionary<string, string>> AsDictionaryResult(bool allowOptionalMappings, char separator)
=> new(Telemetry, Key, recordValue: true, configurationResult: GetDictionaryResult(allowOptionalMappings, separator));

private ConfigurationResult<string> GetStringResult(Func<string, bool>? validator, Func<string, ParsingResult<string>>? converter, bool recordValue)
=> converter is null
? GetResult(AsStringSelector, validator, recordValue)
Expand Down Expand Up @@ -694,4 +837,48 @@ private ConfigurationResult<IDictionary<string, string>> GetDictionaryResult(boo
return result;
}
}

internal readonly struct StructConfigurationResultWithKey<T>(IConfigurationTelemetry telemetry, string key, bool recordValue, ConfigurationResult<T> configurationResult)
where T : struct
{
public readonly string Key = key;
public readonly IConfigurationTelemetry Telemetry = telemetry;
public readonly bool RecordValue = recordValue;
public readonly ConfigurationResult<T> ConfigurationResult = configurationResult;

public T WithDefault(T defaultValue)
=> WithDefault(getDefaultValue: () => defaultValue);

public T WithDefault(Func<DefaultResult<T>> getDefaultValue)
{
if (TryHandleResult(Telemetry, Key, ConfigurationResult, RecordValue, getDefaultValue, out var value))
{
return value;
}

return default; // should never be invoked because we have a value for getDefaultValue
}
}

internal readonly struct ClassConfigurationResultWithKey<T>(IConfigurationTelemetry telemetry, string key, bool recordValue, ConfigurationResult<T> configurationResult)
where T : class
{
public readonly string Key = key;
public readonly IConfigurationTelemetry Telemetry = telemetry;
public readonly bool RecordValue = recordValue;
public readonly ConfigurationResult<T> ConfigurationResult = configurationResult;

public T WithDefault(T defaultValue)
=> WithDefault(getDefaultValue: () => defaultValue);

public T WithDefault(Func<DefaultResult<T>> getDefaultValue)
{
if (TryHandleResult(Telemetry, Key, ConfigurationResult, RecordValue, getDefaultValue, out var value))
{
return value;
}

return default!; // should never be invoked because we have a value for getDefaultValue
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ private static void OnConfigurationChanged(ConfigurationBuilder settings)
TraceEnabled = settings.WithKeys(ConfigurationKeys.TraceEnabled).AsBool(),
// RuntimeMetricsEnabled = settings.WithKeys(ConfigurationKeys.RuntimeMetricsEnabled).AsBool(),
// DataStreamsMonitoringEnabled = settings.WithKeys(ConfigurationKeys.DataStreamsMonitoring.Enabled).AsBool(),
SamplingRules = settings.WithKeys(ConfigurationKeys.CustomSamplingRules).GetAs<string>(null, null, s => s),
// Note: Calling GetAsClass<string>() here instead of GetAsString() as we need to get the
// "serialized JToken", which in JsonConfigurationSource is different, as it allows for non-string tokens
SamplingRules = settings.WithKeys(ConfigurationKeys.CustomSamplingRules).GetAsClass<string>(validator: null, converter: s => s),
Comment on lines +80 to +82
Copy link
Member

@lucaspimentel lucaspimentel Jun 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! 😅

GlobalSamplingRate = settings.WithKeys(ConfigurationKeys.GlobalSamplingRate).AsDouble(),
// SpanSamplingRules = settings.WithKeys(ConfigurationKeys.SpanSamplingRules).AsString(),
LogsInjectionEnabled = settings.WithKeys(ConfigurationKeys.LogsInjectionEnabled).AsBool(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,10 +627,9 @@ public void GetAs_ReturnsNullWhenCantParseAndNoDefault(string value)

var actual = new ConfigurationBuilder(source, _telemetry)
.WithKeys("key")
.GetAs(
getDefaultValue: null,
.GetAsStruct(
validator: null,
converter: _nullableConverter);
converter: _converter);

actual.Should().BeNull();
}
Expand Down
Loading