Skip to content
Draft
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
11 changes: 9 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,17 @@ dsm_throughput:
- when: manual


validate_supported_configurations_local_file:
validate_supported_configurations_v2_local_file:
stage: build
rules:
- when: on_success
extends: .validate_supported_configurations_local_file
extends: .validate_supported_configurations_v2_local_file
variables:
LOCAL_JSON_PATH: "tracer/src/Datadog.Trace/Configuration/supported-configurations.json"
BACKFILLED: true

update_central_configurations_version_range_v2:
extends: .update_central_configurations_version_range_v2
variables:
LOCAL_JSON_PATH: "tracer/src/Datadog.Trace/Configuration/supported-configurations.json"
LANGUAGE_NAME: "dotnet"
4 changes: 2 additions & 2 deletions .gitlab/one-pipeline.locked.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# DO NOT EDIT THIS FILE MANUALLY
# DO NOT EDIT THIS FILE MANUALLY
# This file is auto-generated by automation.
include:
- remote: https://gitlab-templates.ddbuild.io/libdatadog/one-pipeline/ca/75745fafbee537a84b24d8d5fe735299438cbe46d6949d686cd73818296c5f44/one-pipeline.yml
- remote: https://gitlab-templates.ddbuild.io/libdatadog/one-pipeline/ca/f14ac28614630d12bcfe6cba4fd8d72dce142c62ff0b053ba7c323622104ebd7/one-pipeline.yml
58 changes: 42 additions & 16 deletions docs/development/Configuration/AddingConfigurationKeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ This guide explains how to add new configuration keys to the .NET Tracer. Config

Configuration keys in the .NET Tracer are defined in two source files:

- **`tracer/src/Datadog.Trace/Configuration/supported-configurations.json`** - Defines the configuration keys, their
- **`tracer/src/Datadog.Trace/Configuration/supported-configurations.json`** - Defines the configuration keys, their
environment variable names, and optional fallbacks.
- **`tracer/src/Datadog.Trace/Configuration/supported-configurations-docs.yaml`** - Contains XML documentation for
- **`tracer/src/Datadog.Trace/Configuration/supported-configurations-docs.yaml`** - Contains XML documentation for
each key. We're using yaml here as it makes it easier for some of the long documentation summaries and formatting.

Two source generators read these files at build time:
Expand All @@ -40,22 +40,39 @@ Two source generators read these files at build time:

### 1. Add the Configuration Key Definition

Add your new configuration key to `tracer/src/Datadog.Trace/Configuration/supported-configurations.json`, specifying
an arbitrary version string (e.g. `"A"`, as shown below). and specifying the product if required. Any product name
is allowed, but try to reuse the existing ones (see [Common products](#common-products)) if it makes sense, as they will create another partial class, ie
Add your new configuration key to `tracer/src/Datadog.Trace/Configuration/supported-configurations.json`, specifying
an implementation string (`"A"` being the default one, as shown below) and specifying the product if required. Any product name
is allowed, but try to reuse the existing ones (see [Common products](#common-products)) if it makes sense, as they will create another partial class, ie
ConfigurationKeys.ProductName.cs. Without a product name, the keys will go in the main class, ConfigurationKeys.cs.

**Required fields (mandatory):**
- `implementation`: The implementation identifier
- `"A"` being the default one, it needs to match the registry implementation with the same type and default values
- `type`: The type of the configuration value (for example `string`, `boolean`, `int`, `decimal`)
- `default`: The default value applied by the tracer when the env var is not set. Use `null` if there is no default.

These fields are mandatory to keep the configuration registry complete and to ensure consistent behavior and documentation across products.

**Example:**
```json
{
"version": "2",
"supportedConfigurations": {
"DD_TRACE_SAMPLE_RATE": {
"version": ["A"]
},
"OTEL_EXPORTER_OTLP_TIMEOUT": {
"version": ["A"],
"product": "OpenTelemetry"
}
"DD_TRACE_SAMPLE_RATE": [
{
"implementation": "A",
"type": "decimal",
"default": null
}
],
"OTEL_EXPORTER_OTLP_TIMEOUT": [
{
"implementation": "A",
"type": "int",
"default": null,
"product": "OpenTelemetry"
}
]
}
}
```
Expand Down Expand Up @@ -84,13 +101,22 @@ OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: |

### 3. (Optional) Add Aliases

Configuration keys can have **aliases** that are checked in order of appearance when the primary key is not found. Add them to the `aliases` section in `supported-configurations.json`:
Configuration keys can have **aliases** that are checked in order of appearance when the primary key is not found. Add them to the `aliases` property of the configuration entry in `supported-configurations.json`:

```json
{
"aliases": {
"version": "2",
"supportedConfigurations": {
"OTEL_EXPORTER_OTLP_LOGS_TIMEOUT": [
"OTEL_EXPORTER_OTLP_TIMEOUT"
{
"implementation": "A",
"type": "int",
"default": null,
"product": "OpenTelemetry",
"aliases": [
"OTEL_EXPORTER_OTLP_TIMEOUT"
]
}
]
}
}
Expand Down Expand Up @@ -384,7 +410,7 @@ dotnet build tracer/src/Datadog.Trace/Datadog.Trace.csproj

## Related Files

- **Source generators:**
- **Source generators:**
- `tracer/src/Datadog.Trace.SourceGenerators/Configuration/ConfigurationKeysGenerator.cs` - Generates configuration key constants
- `tracer/src/Datadog.Trace.SourceGenerators/Configuration/ConfigKeyAliasesSwitcherGenerator.cs` - Generates alias resolution logic
- **Configuration source:** `tracer/src/Datadog.Trace/Configuration/supported-configurations.json`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,13 @@ public class ConfigKeyAliasesSwitcherGenerator : IIncrementalGenerator
/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Get the supported-configurations.json file and parse only the aliases section
// We only track changes to the aliases section since that's what affects the generated code
var additionalText = context.AdditionalTextsProvider
.Where(static file => Path.GetFileName(file.Path).Equals(SupportedConfigurationsFileName, StringComparison.OrdinalIgnoreCase))
.WithTrackingName(TrackingNames.ConfigurationKeysAdditionalText);

var aliasSection = additionalText.Select(static (file, ct) => ExtractAliasesSection(file, ct));

var aliasesContent = aliasSection.Select(static (extractResult, ct) =>
{
if (extractResult.Errors.Count > 0)
{
// Return the errors from extraction
return new Result<ConfigurationAliases>(null!, extractResult.Errors);
}

return ParseAliasesContent(extractResult.Value, ct);
})
.WithTrackingName(TrackingNames.ConfigurationKeysParseConfiguration);
var aliasesContent = additionalText
.Select(static (file, ct) => ParseAliasesFromV2File(file, ct))
.WithTrackingName(TrackingNames.ConfigurationKeysParseConfiguration);

// Always generate source code, even when there are errors
// This ensures compilation doesn't fail due to missing generated types
Expand All @@ -70,15 +58,15 @@ private static void Execute(SourceProductionContext context, Result<Configuratio
context.AddSource($"{ClassName}.g.cs", SourceText.From(generatedSource, Encoding.UTF8));
}

private static Result<string> ExtractAliasesSection(AdditionalText file, CancellationToken cancellationToken)
private static Result<ConfigurationAliases> ParseAliasesFromV2File(AdditionalText file, CancellationToken cancellationToken)
{
try
{
var sourceText = file.GetText(cancellationToken);
if (sourceText is null)
{
return new Result<string>(
string.Empty,
return new Result<ConfigurationAliases>(
null!,
new EquatableArray<DiagnosticInfo>(
[
CreateDiagnosticInfo("DDSG0003", "Configuration file not found", $"The file '{file.Path}' could not be read. Make sure the supported-configurations.json file exists and is included as an AdditionalFile.", DiagnosticSeverity.Error)
Expand All @@ -87,93 +75,90 @@ private static Result<string> ExtractAliasesSection(AdditionalText file, Cancell

var jsonContent = sourceText.ToString();

// Extract only the aliases section from the JSON using System.Text.Json
using var document = JsonDocument.Parse(jsonContent);
var root = document.RootElement;

if (root.TryGetProperty("aliases", out var aliasesElement))
if (!root.TryGetProperty("supportedConfigurations", out var supportedConfigurationsElement) ||
supportedConfigurationsElement.ValueKind != JsonValueKind.Object)
{
// Return the raw JSON string of the aliases section
return new Result<string>(aliasesElement.GetRawText(), default);
return new Result<ConfigurationAliases>(
null!,
new EquatableArray<DiagnosticInfo>(
[
CreateDiagnosticInfo("DDSG0002", "Aliases parsing error", "Missing or invalid 'supportedConfigurations' section", DiagnosticSeverity.Error)
]));
}

return new Result<string>(string.Empty, default);
var aliases = ParseAliasesFromV2SupportedConfigurations(supportedConfigurationsElement);
return new Result<ConfigurationAliases>(new ConfigurationAliases(aliases), default);
}
catch (Exception ex)
{
return new Result<string>(
string.Empty,
return new Result<ConfigurationAliases>(
null!,
new EquatableArray<DiagnosticInfo>(
[
CreateDiagnosticInfo("DDSG0004", "Configuration file read error", $"Failed to read configuration file '{file.Path}': {ex.Message}", DiagnosticSeverity.Error)
]));
}
}

private static Result<ConfigurationAliases> ParseAliasesContent(string aliasesContent, CancellationToken cancellationToken)
private static Dictionary<string, string[]> ParseAliasesFromV2SupportedConfigurations(JsonElement supportedConfigurationsElement)
{
try
var aliases = new Dictionary<string, string[]>();

foreach (var setting in supportedConfigurationsElement.EnumerateObject())
{
if (string.IsNullOrEmpty(aliasesContent))
var mainKey = setting.Name;
var definitions = setting.Value;

if (definitions.ValueKind != JsonValueKind.Array)
{
// Empty aliases section is valid - just return empty configuration
return new Result<ConfigurationAliases>(new ConfigurationAliases(new Dictionary<string, string[]>()), default);
throw new InvalidOperationException($"Configuration entry '{mainKey}' must be an array of implementation objects");
}

cancellationToken.ThrowIfCancellationRequested();
List<string>? aliasList = null;
HashSet<string>? seen = null;

// Parse the aliases section using System.Text.Json
var aliases = ParseAliasesFromJson(aliasesContent);
var configurationData = new ConfigurationAliases(aliases);
foreach (var implementation in definitions.EnumerateArray())
{
if (implementation.ValueKind != JsonValueKind.Object)
{
throw new InvalidOperationException($"Configuration entry '{mainKey}' must be an object");
}

return new Result<ConfigurationAliases>(configurationData, default);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
return new Result<ConfigurationAliases>(
null!,
new EquatableArray<DiagnosticInfo>(
[
CreateDiagnosticInfo("DDSG0002", "Aliases parsing error", $"Failed to parse aliases section: {ex.Message}")
]));
}
}
if (!implementation.TryGetProperty("aliases", out var aliasesElement) ||
aliasesElement.ValueKind != JsonValueKind.Array)
{
continue;
}

private static Dictionary<string, string[]> ParseAliasesFromJson(string aliasesJson)
{
var aliases = new Dictionary<string, string[]>();
foreach (var aliasElement in aliasesElement.EnumerateArray())
{
if (aliasElement.ValueKind != JsonValueKind.String)
{
continue;
}

using var document = JsonDocument.Parse(aliasesJson);
var root = document.RootElement;
var alias = aliasElement.GetString();
if (string.IsNullOrEmpty(alias))
{
continue;
}

foreach (var property in root.EnumerateObject())
{
var mainKey = property.Name;
var aliasArray = property.Value;
aliasList ??= [];
seen ??= new HashSet<string>(StringComparer.Ordinal);

if (aliasArray.ValueKind == JsonValueKind.Array)
{
var aliasList = new List<string>();
foreach (var aliasElement in aliasArray.EnumerateArray())
{
if (aliasElement.ValueKind == JsonValueKind.String)
if (seen.Add(alias!))
{
var alias = aliasElement.GetString();
if (!string.IsNullOrEmpty(alias))
{
aliasList.Add(alias!);
}
aliasList.Add(alias!);
}
}
}

if (aliasList.Count > 0)
{
aliases[mainKey] = aliasList.ToArray();
}
if (aliasList is { Count: > 0 })
{
aliases[mainKey] = aliasList.ToArray();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,18 +268,37 @@ private static Dictionary<string, ConfigEntry> ParseConfigurationEntries(JsonEle
var key = property.Name;
var value = property.Value;

// Validate that the value is an object
if (value.ValueKind != JsonValueKind.Object)
// v2 schema: each entry is an array of implementation objects
if (value.ValueKind != JsonValueKind.Array)
{
throw new InvalidOperationException($"Configuration entry '{key}' must be an object");
throw new InvalidOperationException($"Configuration entry '{key}' must be an array of implementation objects");
}

// Extract the product field if it exists
// Extract the product field (first non-empty product in the implementations, if any)
var product = string.Empty;
if (value.TryGetProperty("product", out var productElement) &&
productElement.ValueKind == JsonValueKind.String)
foreach (var implementation in value.EnumerateArray())
{
product = productElement.GetString() ?? string.Empty;
if (implementation.ValueKind != JsonValueKind.Object)
{
throw new InvalidOperationException($"Configuration entry '{key}' has an implementation object that is not an object");
}

if (implementation.TryGetProperty("product", out var productElement))
{
if (productElement.ValueKind != JsonValueKind.String)
{
throw new InvalidOperationException($"Configuration entry '{key}' has a 'product' field that is not a string");
}

var productValue = productElement.GetString();
if (productValue is null || productValue.Length == 0)
{
throw new InvalidOperationException($"Configuration entry '{key}' has an empty 'product' field, if present, it must be a non-empty string");
}

product = productValue;
break;
}
}

configurations[key] = new ConfigEntry(key, string.Empty, product);
Expand Down
Loading
Loading