Skip to content

Commit 68dfdf8

Browse files
authored
[ConfigRegistry] 3/5 Generate ConfigurationKeys with source generator (#7698)
## Context Part of **Configuration Inversion (Step 3)** - Stack progress: 1. [#7548](#7548) - Add GitLab step and JSON configuration file 2. [#7688](#7688) - Cleanup configuration / platform keys + analyzers 3. **→ [#7698](#7698) - Generate ConfigurationKeys with source generator (this PR)** 4. [#7689](#7689) - Aliases handling via source generator 5. [#7697](#7697) - Replace manual ConfigurationKeys by generated ones in the whole solution ## Summary Adds source generator to auto-generate `ConfigurationKeys` from `supported-configurations.json` with name mapping to preserve existing constant names. ## Changes **Source Generator:** - [ConfigurationKeysGenerator](cci:7://file:///var/folders/4j/q1f1tvc503g_thnq6p79cp500000gp/T/SourceGeneratedDocuments/D0E9C9C62821E2CB6FADE080/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator:0:0-0:0) reads `supported-configurations.json` and [supported-configurations-docs.yaml](cci:7://file:///Users/anna.yafi/go/src/github.com/DataDog/dd-trace-dotnet3/tracer/src/Datadog.Trace/Configuration/supported-configurations-docs.yaml:0:0-0:0) - Auto-generates `ConfigurationKeys` with nested product classes (AppSec, CIVisibility, OpenTelemetry, etc.) - Generates XML documentation and `[Obsolete]` attributes from JSON **Configuration Mapping:** - `configuration_keys_mapping.json` maps env vars to original constant names (e.g., `DD_TRACE_ENABLED` → `TraceEnabled`) - Avoids refactoring hundreds of references across codebase **Updates:** - Added missing `DD_TRACE_ACTIVITY_LISTENER_ENABLED` key - Committed temporary [ConfigurationKeys2.g.cs](cci:7://file:///Users/anna.yafi/go/src/github.com/DataDog/dd-trace-dotnet3/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys2.g.cs:0:0-0:0) files for validation ## Motivation Makes `supported-configurations.json` the single source of truth while preserving existing constant names to avoid massive refactoring. ## Test Coverage - Added `ConfigurationKeysGeneratorTests.cs` with comprehensive unit tests - Tests validate generator output, XML documentation generation, and obsolete attribute application - Tests cover nested product classes, deprecations, and YAML documentation integration - All existing tests pass without modification ## Related Work Builds on #7548 (configuration registry) and #7688 (PlatformKeys separation + analyzers).
1 parent 0d21d39 commit 68dfdf8

File tree

77 files changed

+10708
-10
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+10708
-10
lines changed

tracer/src/Datadog.Trace.SourceGenerators/Configuration/ConfigurationKeysGenerator.cs

Lines changed: 769 additions & 0 deletions
Large diffs are not rendered by default.

tracer/src/Datadog.Trace.SourceGenerators/Constants.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// <copyright file="Constants.cs" company="Datadog">
1+
// <copyright file="Constants.cs" company="Datadog">
22
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
44
// </copyright>
@@ -20,5 +20,13 @@ internal static class Constants
2020
#nullable enable
2121
2222
23+
""";
24+
25+
public const string ConfigurationGeneratorComment =
26+
"""
27+
// This file is auto-generated from supported-configurations.json and supported-configurations-docs.yaml
28+
// Do not edit this file directly. The source generator will regenerate it on build.
29+
// NOTE: If you remove keys/products from the JSON, run 'dotnet clean' and remove old generated files.
30+
2331
""";
2432
}

tracer/src/Datadog.Trace.SourceGenerators/Datadog.Trace.SourceGenerators.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>netstandard2.0</TargetFramework>
44
<Nullable>enable</Nullable>
@@ -12,6 +12,7 @@
1212
<ItemGroup>
1313
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
1414
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
15+
<PackageReference Include="System.Text.Json" Version="10.0.0-rc.2.25502.107" PrivateAssets="all" />
1516
</ItemGroup>
1617
<ItemGroup>
1718
<Compile Include="..\Datadog.Trace\ClrProfiler\InstrumentationCategory.cs" Link="InstrumentationDefinitions\InstrumentationCategory.cs" />

tracer/src/Datadog.Trace.SourceGenerators/Helpers/TrackingNames.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,19 @@ internal class TrackingNames
3131
public const string AssemblyCallTargetDefinitionSource = nameof(AssemblyCallTargetDefinitionSource);
3232
public const string AdoNetCallTargetDefinitionSource = nameof(AdoNetCallTargetDefinitionSource);
3333
public const string AdoNetSignatures = nameof(AdoNetSignatures);
34+
35+
// Configuration key matcher
36+
public const string ConfigurationKeysParseConfiguration = nameof(ConfigurationKeysParseConfiguration);
37+
public const string ConfigurationKeyMatcherDiagnostics = nameof(ConfigurationKeyMatcherDiagnostics);
38+
public const string ConfigurationKeyMatcherValidData = nameof(ConfigurationKeyMatcherValidData);
39+
public const string ConfigurationKeysAdditionalText = nameof(ConfigurationKeysAdditionalText);
40+
41+
// Configuration key generator
42+
public const string ConfigurationKeysGenJsonFile = nameof(ConfigurationKeysGenJsonFile);
43+
public const string ConfigurationKeysGenYamlFile = nameof(ConfigurationKeysGenYamlFile);
44+
public const string ConfigurationKeysGenMappingFile = nameof(ConfigurationKeysGenMappingFile);
45+
public const string ConfigurationKeysGenMergeData = nameof(ConfigurationKeysGenMergeData);
46+
public const string ConfigurationKeysGenParseConfiguration = nameof(ConfigurationKeysGenParseConfiguration);
47+
public const string ConfigurationKeysGenParseYaml = nameof(ConfigurationKeysGenParseYaml);
48+
public const string ConfigurationKeysGenParseMapping = nameof(ConfigurationKeysGenParseMapping);
3449
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// <copyright file="YamlReader.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+
using System;
7+
using System.Collections.Generic;
8+
using System.Text;
9+
10+
namespace Datadog.Trace.SourceGenerators.Helpers
11+
{
12+
/// <summary>
13+
/// Simple YAML parser for reading documentation strings from YAML files.
14+
/// Supports basic YAML features needed for configuration documentation.
15+
/// </summary>
16+
internal static class YamlReader
17+
{
18+
/// <summary>
19+
/// Parses a YAML file containing configuration key documentation.
20+
/// Expects format: KEY: | followed by multi-line documentation
21+
/// </summary>
22+
public static Dictionary<string, string> ParseDocumentation(string yamlContent)
23+
{
24+
var result = new Dictionary<string, string>();
25+
26+
string? currentKey = null;
27+
var currentDoc = new StringBuilder();
28+
var inMultiLine = false;
29+
var baseIndent = 0;
30+
31+
foreach (var line in new LineEnumerator(yamlContent.AsSpan()))
32+
{
33+
// Skip empty lines and comments when not in multi-line
34+
if (!inMultiLine && (IsWhiteSpace(line) || line.TrimStart().StartsWith("#".AsSpan(), StringComparison.Ordinal)))
35+
{
36+
continue;
37+
}
38+
39+
// Check for new key (starts at column 0, contains colon)
40+
if (!inMultiLine && line.Length > 0 && line[0] != ' ' && line.IndexOf(':') >= 0)
41+
{
42+
// Save previous key if exists
43+
if (currentKey != null)
44+
{
45+
result[currentKey] = currentDoc.ToString().TrimEnd();
46+
currentDoc.Clear();
47+
}
48+
49+
var colonIndex = line.IndexOf(':');
50+
currentKey = line.Slice(0, colonIndex).Trim().ToString();
51+
52+
// Check if it's a multi-line string (|)
53+
var afterColon = line.Slice(colonIndex + 1).Trim();
54+
if (afterColon.Length == 1 && (afterColon[0] == '|' || afterColon[0] == '>'))
55+
{
56+
inMultiLine = true;
57+
baseIndent = -1; // Will be set on first content line
58+
}
59+
else if (afterColon.Length > 0)
60+
{
61+
// Single line value
62+
currentDoc.Append(afterColon.ToString());
63+
result[currentKey] = currentDoc.ToString();
64+
currentDoc.Clear();
65+
currentKey = null;
66+
}
67+
68+
continue;
69+
}
70+
71+
// Handle multi-line content
72+
if (inMultiLine && currentKey != null)
73+
{
74+
// Check if we've reached the next key (no indentation)
75+
if (line.Length > 0 && line[0] != ' ' && line.IndexOf(':') >= 0)
76+
{
77+
// Save current key and process this line as new key
78+
result[currentKey] = currentDoc.ToString().TrimEnd();
79+
currentDoc.Clear();
80+
inMultiLine = false;
81+
82+
var colonIndex = line.IndexOf(':');
83+
currentKey = line.Slice(0, colonIndex).Trim().ToString();
84+
var afterColon = line.Slice(colonIndex + 1).Trim();
85+
if (afterColon.Length == 1 && (afterColon[0] == '|' || afterColon[0] == '>'))
86+
{
87+
inMultiLine = true;
88+
baseIndent = -1;
89+
}
90+
91+
continue;
92+
}
93+
94+
// Determine base indentation from first content line
95+
if (baseIndent == -1 && line.Length > 0 && line[0] == ' ')
96+
{
97+
baseIndent = 0;
98+
while (baseIndent < line.Length && line[baseIndent] == ' ')
99+
{
100+
baseIndent++;
101+
}
102+
}
103+
104+
// Add content line (remove base indentation)
105+
if (line.Length > 0)
106+
{
107+
var contentStart = 0;
108+
while (contentStart < line.Length && contentStart < baseIndent && line[contentStart] == ' ')
109+
{
110+
contentStart++;
111+
}
112+
113+
if (currentDoc.Length > 0)
114+
{
115+
currentDoc.AppendLine();
116+
}
117+
118+
currentDoc.Append(line.Slice(contentStart).ToString());
119+
}
120+
else
121+
{
122+
// Empty line in multi-line content
123+
if (currentDoc.Length > 0)
124+
{
125+
currentDoc.AppendLine();
126+
}
127+
}
128+
}
129+
}
130+
131+
// Save last key
132+
if (currentKey != null)
133+
{
134+
result[currentKey] = currentDoc.ToString().TrimEnd();
135+
}
136+
137+
return result;
138+
}
139+
140+
/// <summary>
141+
/// Checks if a span contains only whitespace characters.
142+
/// </summary>
143+
private static bool IsWhiteSpace(ReadOnlySpan<char> span)
144+
{
145+
for (int i = 0; i < span.Length; i++)
146+
{
147+
if (!char.IsWhiteSpace(span[i]))
148+
{
149+
return false;
150+
}
151+
}
152+
153+
return true;
154+
}
155+
156+
/// <summary>
157+
/// Enumerator for iterating through lines in a ReadOnlySpan without allocations.
158+
/// </summary>
159+
private ref struct LineEnumerator
160+
{
161+
private ReadOnlySpan<char> _remaining;
162+
private ReadOnlySpan<char> _current;
163+
private bool _isEnumeratorActive;
164+
165+
public LineEnumerator(ReadOnlySpan<char> text)
166+
{
167+
_remaining = text;
168+
_current = default;
169+
_isEnumeratorActive = true;
170+
}
171+
172+
public ReadOnlySpan<char> Current => _current;
173+
174+
public LineEnumerator GetEnumerator() => this;
175+
176+
public bool MoveNext()
177+
{
178+
if (!_isEnumeratorActive)
179+
{
180+
return false;
181+
}
182+
183+
if (_remaining.Length == 0)
184+
{
185+
_isEnumeratorActive = false;
186+
return false;
187+
}
188+
189+
var idx = _remaining.IndexOfAny('\r', '\n');
190+
if (idx < 0)
191+
{
192+
// Last line without line ending
193+
_current = _remaining;
194+
_remaining = default;
195+
return true;
196+
}
197+
198+
_current = _remaining.Slice(0, idx);
199+
200+
// Skip past the line ending (\r\n or \n or \r)
201+
var advance = idx + 1;
202+
if (idx < _remaining.Length - 1 && _remaining[idx] == '\r' && _remaining[idx + 1] == '\n')
203+
{
204+
advance = idx + 2;
205+
}
206+
207+
_remaining = _remaining.Slice(advance);
208+
return true;
209+
}
210+
}
211+
}
212+
}

tracer/src/Datadog.Trace/Configuration/ConfigurationKeys.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ namespace Datadog.Trace.Configuration
1414
/// </summary>
1515
internal static partial class ConfigurationKeys
1616
{
17+
// temporary so that ConfigurationKeys2 can resolve reference, all this is removed later
18+
public const string TraceLogPath = "DD_TRACE_LOG_PATH";
19+
20+
/// <summary>
21+
/// Configuration key to enable or disable the ActivityListener.
22+
/// </summary>
23+
public const string ActivityListenerEnabled = "DD_TRACE_ACTIVITY_LISTENER_ENABLED";
24+
1725
/// <summary>
1826
/// Configuration key to enable experimental features.
1927
/// </summary>
@@ -994,7 +1002,7 @@ internal static class DataStreamsMonitoring
9941002
/// Configuration key for enabling legacy binary headers in Data Streams Monitoring.
9951003
/// false by default if DSM is in default state, true otherwise
9961004
/// </summary>
997-
/// <see cref="TracerSettings.IsDataStreamsLegacyHeadersEnabled"/>
1005+
/// <see cref="TracerSettings.IsDataStreamsLegacyHeadersEnabled"/>
9981006
public const string LegacyHeadersEnabled = "DD_DATA_STREAMS_LEGACY_HEADERS";
9991007
}
10001008
}

0 commit comments

Comments
 (0)