diff --git a/tracer/src/Datadog.Trace.Manual/Datadog.Trace.Manual.csproj b/tracer/src/Datadog.Trace.Manual/Datadog.Trace.Manual.csproj index 4305a960bec8..df523ed9a80b 100644 --- a/tracer/src/Datadog.Trace.Manual/Datadog.Trace.Manual.csproj +++ b/tracer/src/Datadog.Trace.Manual/Datadog.Trace.Manual.csproj @@ -26,7 +26,8 @@ - + diff --git a/tracer/src/Datadog.Trace.SourceGenerators/Configuration/ConfigKeyAliasesSwitcherGenerator.cs b/tracer/src/Datadog.Trace.SourceGenerators/Configuration/ConfigKeyAliasesSwitcherGenerator.cs new file mode 100644 index 000000000000..54b610882700 --- /dev/null +++ b/tracer/src/Datadog.Trace.SourceGenerators/Configuration/ConfigKeyAliasesSwitcherGenerator.cs @@ -0,0 +1,386 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading; +using Datadog.Trace.SourceGenerators; +using Datadog.Trace.SourceGenerators.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +/// +/// Source generator that reads supported-configurations.json and generates a switch case +/// for configuration key matching with alias support. +/// +[Generator] +public class ConfigKeyAliasesSwitcherGenerator : IIncrementalGenerator +{ + private const string SupportedConfigurationsFileName = "supported-configurations.json"; + private const string MainKeyParamName = "mainKey"; + private const string ClassName = "ConfigKeyAliasesSwitcher"; + + /// + 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.Collect() + .Select(static (files, ct) => + { + if (files.Length == 0) + { + // No supported-configurations.json file found + return new Result( + string.Empty, + new EquatableArray( + [ + CreateDiagnosticInfo("DDSG0003", "Configuration file not found", $"The file '{SupportedConfigurationsFileName}' was not found. Make sure the supported-configurations.json file exists and is included as an AdditionalFile.", DiagnosticSeverity.Error) + ])); + } + + // Extract from the first (and should be only) file + return ExtractAliasesSection(files[0], ct); + }); + + var aliasesContent = aliasSection.Select(static (extractResult, ct) => + { + if (extractResult.Errors.Count > 0) + { + // Return the errors from extraction + return new Result(null!, extractResult.Errors); + } + + return ParseAliasesContent(extractResult.Value, ct); + }) + .WithTrackingName(TrackingNames.ConfigurationKeysParseConfiguration); + + // Always generate source code, even when there are errors + // This ensures compilation doesn't fail due to missing generated types + context.RegisterSourceOutput( + aliasesContent, + static (spc, result) => Execute(spc, result)); + } + + private static void Execute(SourceProductionContext context, Result result) + { + // Report any diagnostics first + foreach (var diagnostic in result.Errors) + { + context.ReportDiagnostic(Diagnostic.Create(diagnostic.Descriptor, diagnostic.Location?.ToLocation())); + } + + // Generate source code even if there are errors (use empty configuration as fallback) + var configurationAliases = result.Value ?? new ConfigurationAliases(new Dictionary()); + var compilationUnit = GenerateConfigurationKeyMatcher(configurationAliases); + var generatedSource = compilationUnit.NormalizeWhitespace(eol: "\n").ToFullString(); + context.AddSource($"{ClassName}.g.cs", SourceText.From(generatedSource, Encoding.UTF8)); + } + + private static Result ExtractAliasesSection(AdditionalText file, CancellationToken cancellationToken) + { + try + { + var sourceText = file.GetText(cancellationToken); + if (sourceText is null) + { + return new Result( + string.Empty, + new EquatableArray( + [ + 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) + ])); + } + + 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)) + { + // Return the raw JSON string of the aliases section + return new Result(aliasesElement.GetRawText(), default); + } + + return new Result(string.Empty, default); + } + catch (Exception ex) + { + return new Result( + string.Empty, + new EquatableArray( + [ + CreateDiagnosticInfo("DDSG0004", "Configuration file read error", $"Failed to read configuration file '{file.Path}': {ex.Message}", DiagnosticSeverity.Error) + ])); + } + } + + private static Result ParseAliasesContent(string aliasesContent, CancellationToken cancellationToken) + { + try + { + if (string.IsNullOrEmpty(aliasesContent)) + { + // Empty aliases section is valid - just return empty configuration + return new Result(new ConfigurationAliases(new Dictionary()), default); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Parse the aliases section using System.Text.Json + var aliases = ParseAliasesFromJson(aliasesContent); + var configurationData = new ConfigurationAliases(aliases); + + return new Result(configurationData, default); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + return new Result( + null!, + new EquatableArray( + [ + CreateDiagnosticInfo("DDSG0002", "Aliases parsing error", $"Failed to parse aliases section: {ex.Message}") + ])); + } + } + + private static Dictionary ParseAliasesFromJson(string aliasesJson) + { + var aliases = new Dictionary(); + + using var document = JsonDocument.Parse(aliasesJson); + var root = document.RootElement; + + foreach (var property in root.EnumerateObject()) + { + var mainKey = property.Name; + var aliasArray = property.Value; + + if (aliasArray.ValueKind == JsonValueKind.Array) + { + var aliasList = new List(); + foreach (var aliasElement in aliasArray.EnumerateArray()) + { + if (aliasElement.ValueKind == JsonValueKind.String) + { + var alias = aliasElement.GetString(); + if (!string.IsNullOrEmpty(alias)) + { + aliasList.Add(alias!); + } + } + } + + if (aliasList.Count > 0) + { + aliases[mainKey] = aliasList.ToArray(); + } + } + } + + return aliases; + } + + private static DiagnosticInfo CreateDiagnosticInfo(string id, string title, string message, DiagnosticSeverity severity = DiagnosticSeverity.Warning) + { + var descriptor = new DiagnosticDescriptor( + id, + title, + message, + "Configuration", + severity, + isEnabledByDefault: true); + + return new DiagnosticInfo(descriptor, Location.None); + } + + private static CompilationUnitSyntax GenerateConfigurationKeyMatcher(ConfigurationAliases configurationAliases) + { + var getAliasesMethod = GenerateGetAliasesMethod(configurationAliases); + + var classDeclaration = ClassDeclaration(ClassName) + .WithModifiers(TokenList(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.PartialKeyword))) + .WithLeadingTrivia( + Comment(Constants.FileHeader), + Comment("/// "), + Comment("/// Generated configuration key matcher that handles main keys and aliases."), + Comment("/// This file is auto-generated from supported-configurations.json and supported-configurations-docs.yaml. Do not edit this file directly. The source generator will regenerate it on build."), + Comment("/// ")) + .WithMembers( + List( + [ + getAliasesMethod + ])); + + var namespaceDeclaration = FileScopedNamespaceDeclaration( + QualifiedName( + QualifiedName( + IdentifierName("Datadog"), + IdentifierName("Trace")), + IdentifierName("Configuration"))) + .WithMembers(SingletonList(classDeclaration)); + + return CompilationUnit() + .WithUsings( + SingletonList( + UsingDirective(IdentifierName("System")))) + .WithMembers(SingletonList(namespaceDeclaration)); + } + + private static MethodDeclarationSyntax GenerateGetAliasesMethod(ConfigurationAliases configurationAliases) + { + var switchSections = new List(); + + // Add cases for keys that have aliases + foreach (var alias in configurationAliases.Aliases.OrderBy(a => a.Key)) + { + var mainKey = alias.Key; + var aliasKeys = alias.Value; + + var arrayElements = aliasKeys + .OrderBy(a => a) + .Select(aliasKey => LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(aliasKey))) + .Cast() + .ToArray(); + + var arrayCreation = ArrayCreationExpression( + ArrayType(PredefinedType(Token(SyntaxKind.StringKeyword))) + .WithRankSpecifiers(SingletonList(ArrayRankSpecifier(SingletonSeparatedList(OmittedArraySizeExpression()))))) + .WithInitializer(InitializerExpression(SyntaxKind.ArrayInitializerExpression, SeparatedList(arrayElements))); + + var switchSection = SwitchSection() + .WithLabels( + SingletonList( + CaseSwitchLabel(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(mainKey))))) + .WithStatements(SingletonList(ReturnStatement(arrayCreation))); + switchSections.Add(switchSection); + } + + // Add default case + var defaultSection = SwitchSection() + .WithLabels(SingletonList(DefaultSwitchLabel())) + .WithStatements( + SingletonList( + ReturnStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("Array"), + GenericName("Empty") + .WithTypeArgumentList( + TypeArgumentList( + SingletonSeparatedList( + PredefinedType(Token(SyntaxKind.StringKeyword)))))))))); + switchSections.Add(defaultSection); + + var switchStatement = SwitchStatement(IdentifierName(MainKeyParamName)) + .WithSections(List(switchSections)); + + return MethodDeclaration( + ArrayType(PredefinedType(Token(SyntaxKind.StringKeyword))) + .WithRankSpecifiers(SingletonList(ArrayRankSpecifier(SingletonSeparatedList(OmittedArraySizeExpression())))), + "GetAliases") + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter(Identifier(MainKeyParamName)) + .WithType(PredefinedType(Token(SyntaxKind.StringKeyword)))))) + .WithLeadingTrivia( + Comment("/// "), + Comment("/// Gets all aliases for the given configuration key."), + Comment("/// "), + Comment($"/// The configuration key."), + Comment("/// An array of aliases for the key, or empty array if no aliases exist.")) + .WithBody(Block(switchStatement)); + } + + private sealed class ConfigurationAliases(Dictionary aliases) : IEquatable + { + public Dictionary Aliases { get; } = aliases; + + public bool Equals(ConfigurationAliases? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + // Proper content comparison for change detection + if (Aliases.Count != other.Aliases.Count) + { + return false; + } + + foreach (var kvp in Aliases) + { + if (!other.Aliases.TryGetValue(kvp.Key, out var otherAliases)) + { + return false; + } + + if (kvp.Value.Length != otherAliases.Length) + { + return false; + } + + for (int i = 0; i < kvp.Value.Length; i++) + { + if (kvp.Value[i] != otherAliases[i]) + { + return false; + } + } + } + + return true; + } + + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || (obj is ConfigurationAliases other && Equals(other)); + } + + public override int GetHashCode() + { + var hash = new System.HashCode(); + hash.Add(Aliases.Count); + + // Include content in hash for proper change detection + foreach (var kvp in Aliases.OrderBy(x => x.Key)) + { + hash.Add(kvp.Key); + foreach (var alias in kvp.Value.OrderBy(x => x)) + { + hash.Add(alias); + } + } + + return hash.ToHashCode(); + } + } +} diff --git a/tracer/src/Datadog.Trace.Tools.Analyzers/ConfigurationAnalyzers/ConfigurationBuilderWithKeysAnalyzer.cs b/tracer/src/Datadog.Trace.Tools.Analyzers/ConfigurationAnalyzers/ConfigurationBuilderWithKeysAnalyzer.cs new file mode 100644 index 000000000000..b31bd5ec72e2 --- /dev/null +++ b/tracer/src/Datadog.Trace.Tools.Analyzers/ConfigurationAnalyzers/ConfigurationBuilderWithKeysAnalyzer.cs @@ -0,0 +1,227 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers +{ + /// + /// Analyzer to ensure that ConfigurationBuilder.WithKeys method calls only accept string constants + /// from PlatformKeys or ConfigurationKeys classes, not hardcoded strings or variables. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ConfigurationBuilderWithKeysAnalyzer : DiagnosticAnalyzer + { + /// + /// Diagnostic descriptor for when WithKeys or Or is called with a hardcoded string instead of a constant from PlatformKeys or ConfigurationKeys. + /// + public static readonly DiagnosticDescriptor UseConfigurationConstantsRule = new( + id: "DD0007", + title: "Use configuration constants instead of hardcoded strings in WithKeys/Or calls", + messageFormat: "{0} method should use constants from PlatformKeys or ConfigurationKeys classes instead of hardcoded string '{1}'", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "ConfigurationBuilder.WithKeys and HasKeys.Or method calls should only accept string constants from PlatformKeys or ConfigurationKeys classes to ensure consistency and avoid typos."); + + /// + /// Diagnostic descriptor for when WithKeys or Or is called with a variable instead of a constant from PlatformKeys or ConfigurationKeys. + /// + public static readonly DiagnosticDescriptor UseConfigurationConstantsNotVariablesRule = new( + id: "DD0008", + title: "Use configuration constants instead of variables in WithKeys/Or calls", + messageFormat: "{0} method should use constants from PlatformKeys or ConfigurationKeys classes instead of variable '{1}'", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "ConfigurationBuilder.WithKeys and HasKeys.Or method calls should only accept string constants from PlatformKeys or ConfigurationKeys classes, not variables or computed values."); + + /// + /// Gets the supported diagnostics + /// + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(UseConfigurationConstantsRule, UseConfigurationConstantsNotVariablesRule); + + /// + /// Initialize the analyzer + /// + /// context + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocationExpression, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context) + { + var invocation = (InvocationExpressionSyntax)context.Node; + + // Check if this is a WithKeys or Or method call + var methodName = GetConfigurationMethodName(invocation, context.SemanticModel); + if (methodName == null) + { + return; + } + + // Analyze each argument to the method + var argumentList = invocation.ArgumentList; + if (argumentList?.Arguments.Count > 0) + { + var argument = argumentList.Arguments[0]; // Both WithKeys and Or take a single string argument + AnalyzeConfigurationArgument(context, argument, methodName); + } + } + + private static string GetConfigurationMethodName(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + { + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + var methodName = memberAccess.Name.Identifier.ValueText; + + // Check if the method being called is "WithKeys" or "Or" + const string withKeysMethodName = "WithKeys"; + const string orMethodName = "Or"; + if (methodName is withKeysMethodName or orMethodName) + { + // Get the symbol info for the method + var symbolInfo = semanticModel.GetSymbolInfo(memberAccess); + if (symbolInfo.Symbol is IMethodSymbol method) + { + var containingType = method.ContainingType?.Name; + var containingNamespace = method.ContainingNamespace?.ToDisplayString(); + + // Check if this is the ConfigurationBuilder.WithKeys method + if (methodName == withKeysMethodName && + containingType == "ConfigurationBuilder" && + containingNamespace == "Datadog.Trace.Configuration.Telemetry") + { + return withKeysMethodName; + } + + // Check if this is the HasKeys.Or method + if (methodName == orMethodName && + containingType == "HasKeys" && + containingNamespace == "Datadog.Trace.Configuration.Telemetry") + { + return orMethodName; + } + } + } + } + + return null; + } + + private static void AnalyzeConfigurationArgument(SyntaxNodeAnalysisContext context, ArgumentSyntax argument, string methodName) + { + var expression = argument.Expression; + + switch (expression) + { + case LiteralExpressionSyntax literal when literal.Token.IsKind(SyntaxKind.StringLiteralToken): + // This is a hardcoded string literal - report diagnostic + var literalValue = literal.Token.ValueText; + var diagnostic = Diagnostic.Create( + UseConfigurationConstantsRule, + literal.GetLocation(), + methodName, + literalValue); + context.ReportDiagnostic(diagnostic); + break; + + case MemberAccessExpressionSyntax memberAccess: + // Check if this is accessing a constant from PlatformKeys or ConfigurationKeys + if (!IsValidConfigurationConstant(memberAccess, context.SemanticModel)) + { + // This is accessing something else - report diagnostic + var memberName = memberAccess.ToString(); + var memberDiagnostic = Diagnostic.Create( + UseConfigurationConstantsNotVariablesRule, + memberAccess.GetLocation(), + methodName, + memberName); + context.ReportDiagnostic(memberDiagnostic); + } + + break; + + case IdentifierNameSyntax identifier: + // This is a variable or local constant - report diagnostic + var identifierName = identifier.Identifier.ValueText; + var variableDiagnostic = Diagnostic.Create( + UseConfigurationConstantsNotVariablesRule, + identifier.GetLocation(), + methodName, + identifierName); + context.ReportDiagnostic(variableDiagnostic); + break; + + default: + // Any other expression type (method calls, computed values, etc.) - report diagnostic + var expressionText = expression.ToString(); + var defaultDiagnostic = Diagnostic.Create( + UseConfigurationConstantsNotVariablesRule, + expression.GetLocation(), + methodName, + expressionText); + context.ReportDiagnostic(defaultDiagnostic); + break; + } + } + + private static bool IsValidConfigurationConstant(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) + { + var symbolInfo = semanticModel.GetSymbolInfo(memberAccess); + if (symbolInfo.Symbol is IFieldSymbol field) + { + // Check if this is a const string field + if (field.IsConst && field.Type?.SpecialType == SpecialType.System_String) + { + var containingType = field.ContainingType; + if (containingType != null) + { + // Check if the containing type is PlatformKeys or ConfigurationKeys (or their nested classes) + return IsValidConfigurationClass(containingType); + } + } + } + + return false; + } + + private static bool IsValidConfigurationClass(INamedTypeSymbol typeSymbol) + { + // Check if this is PlatformKeys or ConfigurationKeys class or their nested classes + var currentType = typeSymbol; + while (currentType != null) + { + var typeName = currentType.Name; + var namespaceName = currentType.ContainingNamespace?.ToDisplayString(); + + // Check for PlatformKeys class + if (typeName == "PlatformKeys" && namespaceName == "Datadog.Trace.Configuration") + { + return true; + } + + // Check for ConfigurationKeys class + if (typeName == "ConfigurationKeys" && namespaceName == "Datadog.Trace.Configuration") + { + return true; + } + + // Check nested classes within PlatformKeys or ConfigurationKeys + currentType = currentType.ContainingType; + } + + return false; + } + } +} diff --git a/tracer/src/Datadog.Trace.Tools.Analyzers/ConfigurationAnalyzers/PlatformKeysAnalyzer.cs b/tracer/src/Datadog.Trace.Tools.Analyzers/ConfigurationAnalyzers/PlatformKeysAnalyzer.cs new file mode 100644 index 000000000000..a1f7ab4bdb51 --- /dev/null +++ b/tracer/src/Datadog.Trace.Tools.Analyzers/ConfigurationAnalyzers/PlatformKeysAnalyzer.cs @@ -0,0 +1,126 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers; + +/// +/// DD0010: Invalid PlatformKeys constant naming +/// +/// Ensures that constants in the PlatformKeys class do not start with reserved prefixes: +/// - OTEL (OpenTelemetry prefix) +/// - DD_ (Datadog configuration prefix) +/// - _DD_ (Internal Datadog configuration prefix) +/// - DATADOG_ (Older Datadog configuration prefix) +/// +/// Platform keys should represent environment variables from external platforms/services, +/// not Datadog-specific or OpenTelemetry configuration keys. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PlatformKeysAnalyzer : DiagnosticAnalyzer +{ + /// + /// The diagnostic ID displayed in error messages + /// + public const string DiagnosticId = "DD0010"; + + private const string PlatformKeysClassName = "PlatformKeys"; + private const string PlatformKeysNamespace = "Datadog.Trace.Configuration"; + + private static readonly string[] ForbiddenPrefixes = { "OTEL", "DD_", "_DD_", "DATADOG_ " }; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + title: "Invalid PlatformKeys constant naming", + messageFormat: "PlatformKeys constant '{0}' should not start with '{1}'. Platform keys should represent external environment variables, not Datadog or OpenTelemetry configuration keys. Use ConfigurationKeys instead.", + category: "CodeQuality", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Constants in PlatformKeys class should not start with OTEL, DD_, or _DD_ prefixes as these are reserved for OpenTelemetry and Datadog configuration keys. Platform keys should represent environment variables from external platforms and services."); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType); + } + + private static void AnalyzeNamedType(SymbolAnalysisContext context) + { + var namedTypeSymbol = (INamedTypeSymbol)context.Symbol; + + // Check if this is the PlatformKeys class in the correct namespace + if (!IsPlatformKeysClass(namedTypeSymbol)) + { + return; + } + + // Analyze all const fields in the PlatformKeys class and its nested classes + AnalyzeConstFields(context, namedTypeSymbol); + } + + private static bool IsPlatformKeysClass(INamedTypeSymbol namedTypeSymbol) + { + // Check if this is the PlatformKeys class (including partial classes) + if (namedTypeSymbol.Name != PlatformKeysClassName) + { + return false; + } + + // Check if it's in the correct namespace + var containingNamespace = namedTypeSymbol.ContainingNamespace; + return containingNamespace?.ToDisplayString() == PlatformKeysNamespace; + } + + private static void AnalyzeConstFields(SymbolAnalysisContext context, INamedTypeSymbol typeSymbol) + { + // Analyze const fields in the current type + foreach (var member in typeSymbol.GetMembers()) + { + if (member is IFieldSymbol { IsConst: true, Type.SpecialType: SpecialType.System_String } field) + { + AnalyzeConstField(context, field); + } + else if (member is INamedTypeSymbol nestedType) + { + // Recursively analyze nested classes (like Aws, AzureAppService, etc.) + AnalyzeConstFields(context, nestedType); + } + } + } + + private static void AnalyzeConstField(SymbolAnalysisContext context, IFieldSymbol field) + { + if (field.ConstantValue is not string constantValue) + { + return; + } + + // Check if the constant value starts with any forbidden prefix (case-insensitive) + var forbiddenPrefix = ForbiddenPrefixes.FirstOrDefault(prefix => constantValue.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + if (forbiddenPrefix == null) + { + return; + } + + var diagnostic = Diagnostic.Create( + Rule, + field.Locations.FirstOrDefault(), + constantValue, + forbiddenPrefix); + + context.ReportDiagnostic(diagnostic); + } +} diff --git a/tracer/src/Datadog.Trace.Tools.Runner/ProcessConfiguration.cs b/tracer/src/Datadog.Trace.Tools.Runner/ProcessConfiguration.cs index 05dd3bfa7bbc..cd8cca66735f 100644 --- a/tracer/src/Datadog.Trace.Tools.Runner/ProcessConfiguration.cs +++ b/tracer/src/Datadog.Trace.Tools.Runner/ProcessConfiguration.cs @@ -54,9 +54,10 @@ internal static class ProcessConfiguration if (logDirectory == null) { -#pragma warning disable 618 // ProfilerLogPath is deprecated but still supported - var nativeLogFile = config.WithKeys(ConfigurationKeys.ProfilerLogPath).AsString(); -#pragma warning restore 618 + // ProfilerLogPath is deprecated but still supported. For now, we bypass the WithKeys analyzer, but later we want to pull deprecations differently as part of centralized file +#pragma warning disable DD0008, 618 + var nativeLogFile = config.WithKeys(ConfigurationKeys.TraceLogPath).AsString(); +#pragma warning restore DD0008, 618 if (!string.IsNullOrEmpty(nativeLogFile)) { logDirectory = Path.GetDirectoryName(nativeLogFile); diff --git a/tracer/src/Datadog.Trace/AppSec/SecuritySettings.cs b/tracer/src/Datadog.Trace/AppSec/SecuritySettings.cs index fedf165fb326..5ed0a7ca38a6 100644 --- a/tracer/src/Datadog.Trace/AppSec/SecuritySettings.cs +++ b/tracer/src/Datadog.Trace/AppSec/SecuritySettings.cs @@ -124,7 +124,7 @@ public SecuritySettings(IConfigurationSource? source, IConfigurationTelemetry te UserEventsAutoInstrumentationMode = UserTrackingIdentMode; } - ApiSecurityEnabled = config.WithKeys(ConfigurationKeys.AppSec.ApiSecurityEnabled, "DD_EXPERIMENTAL_API_SECURITY_ENABLED") + ApiSecurityEnabled = config.WithKeys(ConfigurationKeys.AppSec.ApiSecurityEnabled) .AsBool(true); ApiSecuritySampleDelay = config.WithKeys(ConfigurationKeys.AppSec.ApiSecuritySampleDelay) diff --git a/tracer/src/Datadog.Trace/Configuration/ConfigurationSources/GlobalConfigurationSource.cs b/tracer/src/Datadog.Trace/Configuration/ConfigurationSources/GlobalConfigurationSource.cs index f197ddfc9c71..87edf73a482c 100644 --- a/tracer/src/Datadog.Trace/Configuration/ConfigurationSources/GlobalConfigurationSource.cs +++ b/tracer/src/Datadog.Trace/Configuration/ConfigurationSources/GlobalConfigurationSource.cs @@ -106,7 +106,7 @@ internal static bool TryLoadJsonConfigurationFile(IConfigurationSource configura // if environment variable is not set, look for default file name in the current directory var configurationFileName = new ConfigurationBuilder(configurationSource, telemetry) - .WithKeys(ConfigurationKeys.ConfigurationFileName, "DD_DOTNET_TRACER_CONFIG_FILE") + .WithKeys(ConfigurationKeys.ConfigurationFileName) .AsString( getDefaultValue: () => Path.Combine(baseDirectory ?? GetCurrentDirectory(), "datadog.json"), validator: null); diff --git a/tracer/src/Datadog.Trace/Configuration/ConfigurationSources/Telemetry/ConfigurationBuilder.cs b/tracer/src/Datadog.Trace/Configuration/ConfigurationSources/Telemetry/ConfigurationBuilder.cs index 0f7d7f7bf4fd..4ba02536c5c7 100644 --- a/tracer/src/Datadog.Trace/Configuration/ConfigurationSources/Telemetry/ConfigurationBuilder.cs +++ b/tracer/src/Datadog.Trace/Configuration/ConfigurationSources/Telemetry/ConfigurationBuilder.cs @@ -19,35 +19,46 @@ internal readonly struct ConfigurationBuilder(IConfigurationSource source, IConf public HasKeys WithKeys(string key) => new(_source, _telemetry, key); - public HasKeys WithKeys(string key, string fallbackKey) => new(_source, _telemetry, key, fallbackKey); - - public HasKeys WithKeys(string key, string fallbackKey1, string fallbackKey2) => new(_source, _telemetry, key, fallbackKey1, fallbackKey2); - - public HasKeys WithKeys(string key, string fallbackKey1, string fallbackKey2, string fallbackKey3) => new(_source, _telemetry, key, fallbackKey1, fallbackKey2, fallbackKey3); - - internal readonly struct HasKeys + public HasKeys WithIntegrationKey(string integrationName) => new( + _source, + _telemetry, + string.Format(IntegrationSettings.IntegrationEnabledKey, integrationName.ToUpperInvariant()), + [ + string.Format(IntegrationSettings.IntegrationEnabledKey, integrationName), + $"DD_{integrationName}_ENABLED" + ]); + + public HasKeys WithIntegrationAnalyticsKey(string integrationName) => new( + _source, + _telemetry, +#pragma warning disable 618 // App analytics is deprecated, but still used + string.Format(IntegrationSettings.AnalyticsEnabledKey, integrationName.ToUpperInvariant()), + [ + string.Format(IntegrationSettings.AnalyticsEnabledKey, integrationName), +#pragma warning restore 618 + $"DD_{integrationName}_ANALYTICS_ENABLED" + ]); + + public HasKeys WithIntegrationAnalyticsSampleRateKey(string integrationName) => new( + _source, + _telemetry, +#pragma warning disable 618 // App analytics is deprecated, but still used + string.Format(IntegrationSettings.AnalyticsSampleRateKey, integrationName.ToUpperInvariant()), + [ + string.Format(IntegrationSettings.AnalyticsSampleRateKey, integrationName), +#pragma warning restore 618 + $"DD_{integrationName}_ANALYTICS_SAMPLE_RATE" + ]); + + internal readonly struct HasKeys(IConfigurationSource source, IConfigurationTelemetry telemetry, string key, string[]? providedAliases = null) { - public HasKeys(IConfigurationSource source, IConfigurationTelemetry telemetry, string key, string? fallbackKey1 = null, string? fallbackKey2 = null, string? fallbackKey3 = null) - { - Source = source; - Telemetry = telemetry; - Key = key; - FallbackKey1 = fallbackKey1; - FallbackKey2 = fallbackKey2; - FallbackKey3 = fallbackKey3; - } + private readonly string[]? _providedAliases = providedAliases; - private IConfigurationSource Source { get; } + private IConfigurationSource Source { get; } = source; - private IConfigurationTelemetry Telemetry { get; } + private IConfigurationTelemetry Telemetry { get; } = telemetry; - private string Key { get; } - - private string? FallbackKey1 { get; } - - private string? FallbackKey2 { get; } - - private string? FallbackKey3 { get; } + private string Key { get; } = key; // **************** // String accessors @@ -493,23 +504,9 @@ private ConfigurationResult GetAs(Func? validator, FuncThe raw private ConfigurationResult GetResult(Func?, bool, ConfigurationResult> selector, Func? validator, bool recordValue) { - var result = selector(Source, Key, Telemetry, validator, recordValue); - if (result.ShouldFallBack && FallbackKey1 is not null) - { - result = selector(Source, FallbackKey1, Telemetry, validator, recordValue); - } - - if (result.ShouldFallBack && FallbackKey2 is not null) - { - result = selector(Source, FallbackKey2, Telemetry, validator, recordValue); - } - - if (result.ShouldFallBack && FallbackKey3 is not null) - { - result = selector(Source, FallbackKey3, Telemetry, validator, recordValue); - } - - return result; + var source = Source; + var telemetry = Telemetry; + return GetResultWithFallback(key => selector(source, key, telemetry, validator, recordValue)); } /// @@ -523,62 +520,48 @@ private ConfigurationResult GetResult(FuncThe raw private ConfigurationResult GetResult(Func?, Func>, bool, ConfigurationResult> selector, Func? validator, Func> converter, bool recordValue) { - var result = selector(Source, Key, Telemetry, validator, converter, recordValue); - if (result.ShouldFallBack && FallbackKey1 is not null) - { - result = selector(Source, FallbackKey1, Telemetry, validator, converter, recordValue); - } - - if (result.ShouldFallBack && FallbackKey2 is not null) - { - result = selector(Source, FallbackKey2, Telemetry, validator, converter, recordValue); - } - - if (result.ShouldFallBack && FallbackKey3 is not null) - { - result = selector(Source, FallbackKey3, Telemetry, validator, converter, recordValue); - } - - return result; + var source = Source; + var telemetry = Telemetry; + return GetResultWithFallback(key => selector(source, key, telemetry, validator, converter, recordValue)); } private ConfigurationResult> GetDictionaryResult(bool allowOptionalMappings, char separator) { - var result = Source.GetDictionary(Key, Telemetry, validator: null, allowOptionalMappings, separator); - if (result.ShouldFallBack && FallbackKey1 is not null) - { - result = Source.GetDictionary(FallbackKey1, Telemetry, validator: null, allowOptionalMappings, separator); - } - - if (result.ShouldFallBack && FallbackKey2 is not null) - { - result = Source.GetDictionary(FallbackKey2, Telemetry, validator: null, allowOptionalMappings, separator); - } - - if (result.ShouldFallBack && FallbackKey3 is not null) - { - result = Source.GetDictionary(FallbackKey3, Telemetry, validator: null, allowOptionalMappings, separator); - } - - return result; + var source = Source; + var telemetry = Telemetry; + return GetResultWithFallback(key => source.GetDictionary(key, telemetry, validator: null, allowOptionalMappings, separator)); } private ConfigurationResult> GetDictionaryResult(Func> parser) { - var result = Source.GetDictionary(Key, Telemetry, validator: null, parser); - if (result.ShouldFallBack && FallbackKey1 is not null) - { - result = Source.GetDictionary(FallbackKey1, Telemetry, validator: null, parser); - } + var source = Source; + var telemetry = Telemetry; + return GetResultWithFallback(key => source.GetDictionary(key, telemetry, validator: null, parser)); + } - if (result.ShouldFallBack && FallbackKey2 is not null) + /// + /// Common method that handles key resolution and alias fallback logic + /// + /// The method to call for each key + /// The type being retrieved + /// The raw + private ConfigurationResult GetResultWithFallback(Func> selector) + { + var result = selector(Key); + if (!result.ShouldFallBack) { - result = Source.GetDictionary(FallbackKey2, Telemetry, validator: null, parser); + return result; } - if (result.ShouldFallBack && FallbackKey3 is not null) + string[] aliases = _providedAliases ?? ConfigKeyAliasesSwitcher.GetAliases(Key); + + foreach (var alias in aliases) { - result = Source.GetDictionary(FallbackKey3, Telemetry, validator: null, parser); + result = selector(alias); + if (!result.ShouldFallBack) + { + break; + } } return result; @@ -606,10 +589,9 @@ public static StructConfigurationResultWithKey Create(IConfigurationTeleme public static StructConfigurationResultWithKey Create(IConfigurationTelemetry telemetry, string key, ConfigurationResult configurationResult) => new(telemetry, key, configurationResult); - public static StructConfigurationResultWithKey Create(IConfigurationTelemetry telemetry, string key, ConfigurationResult configurationResult) - => new(telemetry, key, configurationResult); + public static StructConfigurationResultWithKey Create(IConfigurationTelemetry telemetry, string key, ConfigurationResult configurationResult) => new(telemetry, key, configurationResult); - [return:NotNullIfNotNull(nameof(defaultValue))] + [return: NotNullIfNotNull(nameof(defaultValue))] public T? WithDefault(T? defaultValue) { if (ConfigurationResult is { Result: { } ddResult, IsValid: true }) @@ -623,7 +605,7 @@ public static StructConfigurationResultWithKey Create(IConfigurationTele public T WithDefault(T defaultValue) { - if (ConfigurationResult is { Result: { } ddResult, IsValid: true }) + if (ConfigurationResult is { Result: var ddResult, IsValid: true }) { return ddResult; } diff --git a/tracer/src/Datadog.Trace/Configuration/ExporterSettings.cs b/tracer/src/Datadog.Trace/Configuration/ExporterSettings.cs index 2ba32fe95f90..4798fc77eb2a 100644 --- a/tracer/src/Datadog.Trace/Configuration/ExporterSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/ExporterSettings.cs @@ -452,11 +452,11 @@ public Raw(IConfigurationSource source, IConfigurationTelemetry telemetry) TracesUnixDomainSocketPath = config.WithKeys(ConfigurationKeys.TracesUnixDomainSocketPath).AsString(); TraceAgentHost = config - .WithKeys(ConfigurationKeys.AgentHost, "DD_TRACE_AGENT_HOSTNAME", "DATADOG_TRACE_AGENT_HOSTNAME") + .WithKeys(ConfigurationKeys.AgentHost) .AsString(); TraceAgentPort = config - .WithKeys(ConfigurationKeys.AgentPort, "DATADOG_TRACE_AGENT_PORT") + .WithKeys(ConfigurationKeys.AgentPort) .AsInt32(); MetricsUrl = config.WithKeys(ConfigurationKeys.MetricsUri).AsString(); diff --git a/tracer/src/Datadog.Trace/Configuration/IntegrationSettings.cs b/tracer/src/Datadog.Trace/Configuration/IntegrationSettings.cs index 5a7c3dfc17e1..ee2a7ed9e417 100644 --- a/tracer/src/Datadog.Trace/Configuration/IntegrationSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/IntegrationSettings.cs @@ -6,6 +6,7 @@ #nullable enable using System; +using System.Collections.Generic; using Datadog.Trace.Configuration.Telemetry; namespace Datadog.Trace.Configuration @@ -18,7 +19,7 @@ public class IntegrationSettings : IEquatable /// /// Configuration key pattern for enabling or disabling an integration. /// - public const string IntegrationEnabled = "DD_TRACE_{0}_ENABLED"; + public const string IntegrationEnabledKey = "DD_TRACE_{0}_ENABLED"; /// /// Configuration key pattern for enabling or disabling Analytics in an integration. @@ -50,31 +51,19 @@ internal IntegrationSettings(string integrationName, IConfigurationSource? sourc // We don't record these in telemetry, because they're blocked anyway var config = new ConfigurationBuilder(source ?? NullConfigurationSource.Instance, NullConfigurationTelemetry.Instance); - var upperName = integrationName.ToUpperInvariant(); Enabled = isExplicitlyDisabled ? false : config - .WithKeys( - string.Format(IntegrationEnabled, upperName), - string.Format(IntegrationEnabled, integrationName), - $"DD_{integrationName}_ENABLED") - .AsBool() - ?? fallback?.Enabled; + .WithIntegrationKey(integrationName) + .AsBool() ?? fallback?.Enabled; #pragma warning disable 618 // App analytics is deprecated, but still used AnalyticsEnabled = config - .WithKeys( - string.Format(AnalyticsEnabledKey, upperName), - string.Format(AnalyticsEnabledKey, integrationName), - $"DD_{integrationName}_ANALYTICS_ENABLED") - .AsBool() - ?? fallback?.AnalyticsEnabled; + .WithIntegrationAnalyticsKey(integrationName) + .AsBool() ?? fallback?.AnalyticsEnabled; AnalyticsSampleRate = config - .WithKeys( - string.Format(AnalyticsSampleRateKey, upperName), - string.Format(AnalyticsSampleRateKey, integrationName), - $"DD_{integrationName}_ANALYTICS_SAMPLE_RATE") + .WithIntegrationAnalyticsSampleRateKey(integrationName) .AsDouble(fallback?.AnalyticsSampleRate ?? 1.0); #pragma warning restore 618 } diff --git a/tracer/src/Datadog.Trace/Configuration/MutableSettings.cs b/tracer/src/Datadog.Trace/Configuration/MutableSettings.cs index df7516c7d0f6..cfedb0732ff8 100644 --- a/tracer/src/Datadog.Trace/Configuration/MutableSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/MutableSettings.cs @@ -265,9 +265,9 @@ private MutableSettings( internal IConfigurationTelemetry Telemetry { get; } - internal static ReadOnlyDictionary? InitializeHeaderTags(ConfigurationBuilder config, string key, bool headerTagsNormalizationFixEnabled) + internal static ReadOnlyDictionary? InitializeHeaderTags(ConfigurationBuilder.HasKeys key, bool headerTagsNormalizationFixEnabled) => InitializeHeaderTags( - config.WithKeys(key).AsDictionaryResult(allowOptionalMappings: true), + key.AsDictionaryResult(allowOptionalMappings: true), headerTagsNormalizationFixEnabled); private static ReadOnlyDictionary? InitializeHeaderTags( @@ -866,7 +866,7 @@ public static MutableSettings CreateInitialMutableSettings( }; globalTags = config - .WithKeys(ConfigurationKeys.GlobalTags, "DD_TRACE_GLOBAL_TAGS") + .WithKeys(ConfigurationKeys.GlobalTags) .AsDictionaryResult(parser: updatedTagsParser) .OverrideWith( RemapOtelTags(in otelTags), @@ -880,7 +880,7 @@ public static MutableSettings CreateInitialMutableSettings( else { globalTags = config - .WithKeys(ConfigurationKeys.GlobalTags, "DD_TRACE_GLOBAL_TAGS") + .WithKeys(ConfigurationKeys.GlobalTags) .AsDictionaryResult() .OverrideWith( RemapOtelTags(in otelTags), @@ -901,7 +901,7 @@ public static MutableSettings CreateInitialMutableSettings( var otelServiceName = config.WithKeys(ConfigurationKeys.OpenTelemetry.ServiceName).AsStringResult(); var serviceName = config - .WithKeys(ConfigurationKeys.ServiceName, "DD_SERVICE_NAME") + .WithKeys(ConfigurationKeys.ServiceName) .AsStringResult() .OverrideWith(in otelServiceName, errorLog); @@ -987,7 +987,7 @@ public static MutableSettings CreateInitialMutableSettings( #pragma warning disable 618 // this parameter has been replaced but may still be used var maxTracesSubmittedPerSecond = config - .WithKeys(ConfigurationKeys.TraceRateLimit, ConfigurationKeys.MaxTracesSubmittedPerSecond) + .WithKeys(ConfigurationKeys.TraceRateLimit) #pragma warning restore 618 .AsInt32(defaultValue: 100); @@ -1001,10 +1001,10 @@ public static MutableSettings CreateInitialMutableSettings( .AsBool(defaultValue: true); // Filter out tags with empty keys or empty values, and trim whitespaces - var headerTags = InitializeHeaderTags(config, ConfigurationKeys.HeaderTags, headerTagsNormalizationFixEnabled) ?? ReadOnlyDictionary.Empty; + var headerTags = InitializeHeaderTags(config.WithKeys(ConfigurationKeys.HeaderTags), headerTagsNormalizationFixEnabled) ?? ReadOnlyDictionary.Empty; // Filter out tags with empty keys or empty values, and trim whitespaces - var grpcTags = InitializeHeaderTags(config, ConfigurationKeys.GrpcTags, headerTagsNormalizationFixEnabled: true) ?? ReadOnlyDictionary.Empty; + var grpcTags = InitializeHeaderTags(config.WithKeys(ConfigurationKeys.GrpcTags), headerTagsNormalizationFixEnabled: true) ?? ReadOnlyDictionary.Empty; var customSamplingRules = config.WithKeys(ConfigurationKeys.CustomSamplingRules).AsString(); @@ -1023,24 +1023,20 @@ public static MutableSettings CreateInitialMutableSettings( var kafkaCreateConsumerScopeEnabled = config .WithKeys(ConfigurationKeys.KafkaCreateConsumerScopeEnabled) .AsBool(defaultValue: true); - var serviceNameMappings = TracerSettings.InitializeServiceNameMappings(config, ConfigurationKeys.ServiceNameMappings) ?? ReadOnlyDictionary.Empty; + var serviceNameMappings = TracerSettings.TrimConfigKeysValues(config.WithKeys(ConfigurationKeys.ServiceNameMappings)) ?? ReadOnlyDictionary.Empty; var tracerMetricsEnabled = config .WithKeys(ConfigurationKeys.TracerMetricsEnabled) .AsBool(defaultValue: false); var httpServerErrorStatusCodesString = config -#pragma warning disable 618 // This config key has been replaced but may still be used - .WithKeys(ConfigurationKeys.HttpServerErrorStatusCodes, ConfigurationKeys.DeprecatedHttpServerErrorStatusCodes) -#pragma warning restore 618 + .WithKeys(ConfigurationKeys.HttpServerErrorStatusCodes) .AsString(defaultValue: "500-599"); var httpServerErrorStatusCodes = ParseHttpCodesToArray(httpServerErrorStatusCodesString); var httpClientErrorStatusCodesString = config -#pragma warning disable 618 // This config key has been replaced but may still be used - .WithKeys(ConfigurationKeys.HttpClientErrorStatusCodes, ConfigurationKeys.DeprecatedHttpClientErrorStatusCodes) -#pragma warning restore 618 + .WithKeys(ConfigurationKeys.HttpClientErrorStatusCodes) .AsString(defaultValue: "400-499"); var httpClientErrorStatusCodes = ParseHttpCodesToArray(httpClientErrorStatusCodesString); diff --git a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs index d410282bbeca..14b89aebcca6 100644 --- a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs @@ -136,7 +136,7 @@ internal TracerSettings(IConfigurationSource? source, IConfigurationTelemetry te ? ParsingResult.Success(result: false) : ParsingResult.Failure()); IsActivityListenerEnabled = config - .WithKeys(ConfigurationKeys.FeatureFlags.OpenTelemetryEnabled, "DD_TRACE_ACTIVITY_LISTENER_ENABLED") + .WithKeys(ConfigurationKeys.FeatureFlags.OpenTelemetryEnabled) .AsBoolResult() .OverrideWith(in otelActivityListenerEnabled, ErrorLog, defaultValue: false); @@ -152,7 +152,7 @@ internal TracerSettings(IConfigurationSource? source, IConfigurationTelemetry te .WithKeys(ConfigurationKeys.SpanPointersEnabled) .AsBool(defaultValue: true); - PeerServiceNameMappings = InitializeServiceNameMappings(config, ConfigurationKeys.PeerServiceNameMappings); + PeerServiceNameMappings = TrimConfigKeysValues(config.WithKeys(ConfigurationKeys.PeerServiceNameMappings)); MetadataSchemaVersion = config .WithKeys(ConfigurationKeys.MetadataSchemaVersion) @@ -223,7 +223,7 @@ not null when string.Equals(x, "http/protobuf", StringComparison.OrdinalIgnoreCa validator: null); OtlpMetricsProtocol = config - .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpMetricsProtocol, ConfigurationKeys.OpenTelemetry.ExporterOtlpProtocol) + .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpMetricsProtocol) .GetAs( defaultValue: new(OtlpProtocol.Grpc, "grpc"), converter: x => x switch @@ -258,14 +258,14 @@ not null when string.Equals(x, "http/json", StringComparison.OrdinalIgnoreCase) converter: uriString => new Uri(uriString)); OtlpMetricsHeaders = config - .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpMetricsHeaders, ConfigurationKeys.OpenTelemetry.ExporterOtlpHeaders) + .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpMetricsHeaders) .AsDictionaryResult(separator: '=') .WithDefault(new DefaultResult>(new Dictionary(), "[]")) .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key)) .ToDictionary(kvp => kvp.Key.Trim(), kvp => kvp.Value?.Trim() ?? string.Empty); OtlpMetricsTimeoutMs = config - .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpMetricsTimeoutMs, ConfigurationKeys.OpenTelemetry.ExporterOtlpTimeoutMs) + .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpMetricsTimeoutMs) .AsInt32(defaultValue: 10_000); OtlpMetricsTemporalityPreference = config @@ -282,7 +282,7 @@ not null when string.Equals(x, "lowmemory", StringComparison.OrdinalIgnoreCase) validator: null); OtlpLogsProtocol = config - .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpLogsProtocol, ConfigurationKeys.OpenTelemetry.ExporterOtlpProtocol) + .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpLogsProtocol) .GetAs( defaultValue: new(OtlpProtocol.Grpc, "grpc"), converter: x => x switch @@ -308,14 +308,14 @@ not null when string.Equals(x, "http/protobuf", StringComparison.OrdinalIgnoreCa converter: uriString => new Uri(uriString)); OtlpLogsHeaders = config - .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpLogsHeaders, ConfigurationKeys.OpenTelemetry.ExporterOtlpHeaders) + .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpLogsHeaders) .AsDictionaryResult(separator: '=') .WithDefault(new DefaultResult>(new Dictionary(), "[]")) .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key)) .ToDictionary(kvp => kvp.Key.Trim(), kvp => kvp.Value?.Trim() ?? string.Empty); OtlpLogsTimeoutMs = config - .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpLogsTimeoutMs, ConfigurationKeys.OpenTelemetry.ExporterOtlpTimeoutMs) + .WithKeys(ConfigurationKeys.OpenTelemetry.ExporterOtlpLogsTimeoutMs) .AsInt32(defaultValue: 10_000); var otelLogsExporter = config @@ -518,14 +518,14 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) = converter: otelConverter); PropagationStyleInject = config - .WithKeys(ConfigurationKeys.PropagationStyleInject, "DD_PROPAGATION_STYLE_INJECT", ConfigurationKeys.PropagationStyle) - .GetAsClassResult( - validator: injectionValidator, // invalid individual values are rejected later - converter: style => TrimSplitString(style, commaSeparator)) - .OverrideWith(in otelPropagation, ErrorLog, getDefaultPropagationHeaders); + .WithKeys(ConfigurationKeys.PropagationStyleInject) + .GetAsClassResult( + validator: injectionValidator, // invalid individual values are rejected later + converter: style => TrimSplitString(style, commaSeparator)) + .OverrideWith(in otelPropagation, ErrorLog, getDefaultPropagationHeaders); PropagationStyleExtract = config - .WithKeys(ConfigurationKeys.PropagationStyleExtract, "DD_PROPAGATION_STYLE_EXTRACT", ConfigurationKeys.PropagationStyle) + .WithKeys(ConfigurationKeys.PropagationStyleExtract) .GetAsClassResult( validator: injectionValidator, // invalid individual values are rejected later converter: style => TrimSplitString(style, commaSeparator)) @@ -576,7 +576,7 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) = .Value; IpHeader = config - .WithKeys(ConfigurationKeys.IpHeader, ConfigurationKeys.AppSec.CustomIpHeader) + .WithKeys(ConfigurationKeys.IpHeader) .AsString(); IpHeaderEnabled = config @@ -1346,11 +1346,9 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) = internal static TracerSettings FromDefaultSourcesInternal() => new(GlobalConfigurationSource.Instance, new ConfigurationTelemetry(), new()); - internal static ReadOnlyDictionary? InitializeServiceNameMappings(ConfigurationBuilder config, string key) + internal static ReadOnlyDictionary? TrimConfigKeysValues(ConfigurationBuilder.HasKeys key) { - var mappings = config - .WithKeys(key) - .AsDictionary() + var mappings = key.AsDictionary() ?.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key) && !string.IsNullOrWhiteSpace(kvp.Value)) .ToDictionary(kvp => kvp.Key.Trim(), kvp => kvp.Value.Trim()); return mappings is not null ? new(mappings) : null; @@ -1381,6 +1379,16 @@ internal static string[] TrimSplitString(string? textValues, char[] separators) return list.ToArray(); } + internal bool IsErrorStatusCode(int statusCode, bool serverStatusCode) + => MutableSettings.IsErrorStatusCode(statusCode, serverStatusCode); + + internal bool IsIntegrationEnabled(IntegrationId integration, bool defaultValue = true) + => MutableSettings.IsIntegrationEnabled(integration, defaultValue); + + [Obsolete(DeprecationMessages.AppAnalytics)] + internal double? GetIntegrationAnalyticsSampleRate(IntegrationId integration, bool enabledWithGlobalSetting) + => MutableSettings.GetIntegrationAnalyticsSampleRate(integration, enabledWithGlobalSetting); + internal string GetDefaultHttpClientExclusions() { if (IsRunningInAzureAppService) diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplaySettings.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplaySettings.cs index c9681749780b..5ee568a0f250 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplaySettings.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplaySettings.cs @@ -22,9 +22,7 @@ public ExceptionReplaySettings(IConfigurationSource? source, IConfigurationTelem source ??= NullConfigurationSource.Instance; var config = new ConfigurationBuilder(source, telemetry); -#pragma warning disable CS0612 // Type or member is obsolete - var erEnabledResult = config.WithKeys(ConfigurationKeys.Debugger.ExceptionReplayEnabled, fallbackKey: ConfigurationKeys.Debugger.ExceptionDebuggingEnabled).AsBoolResult(); -#pragma warning restore CS0612 // Type or member is obsolete + var erEnabledResult = config.WithKeys(ConfigurationKeys.Debugger.ExceptionReplayEnabled).AsBoolResult(); Enabled = erEnabledResult.WithDefault(false); CanBeEnabled = erEnabledResult.ConfigurationResult is not { IsValid: true, Result: false }; diff --git a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs new file mode 100644 index 000000000000..1fb9c6357152 --- /dev/null +++ b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs @@ -0,0 +1,140 @@ +using System; + +namespace Datadog.Trace.Configuration; +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + + +/// +/// Generated configuration key matcher that handles main keys and aliases. +/// This file is auto-generated from supported-configurations.json and supported-configurations-docs.yaml. Do not edit this file directly. The source generator will regenerate it on build. +/// +internal static partial class ConfigKeyAliasesSwitcher +{ + /// + /// Gets all aliases for the given configuration key. + /// + /// The configuration key. + /// An array of aliases for the key, or empty array if no aliases exist. + public static string[] GetAliases(string mainKey) + { + switch (mainKey) + { + case "DD_AGENT_HOST": + return new string[] + { + "DATADOG_TRACE_AGENT_HOSTNAME", + "DD_TRACE_AGENT_HOSTNAME" + }; + case "DD_API_SECURITY_ENABLED": + return new string[] + { + "DD_EXPERIMENTAL_API_SECURITY_ENABLED" + }; + case "DD_EXCEPTION_REPLAY_ENABLED": + return new string[] + { + "DD_EXCEPTION_DEBUGGING_ENABLED" + }; + case "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": + return new string[] + { + "DD_INTERNAL_RCM_POLL_INTERVAL" + }; + case "DD_SERVICE": + return new string[] + { + "DD_SERVICE_NAME" + }; + case "DD_TAGS": + return new string[] + { + "DD_TRACE_GLOBAL_TAGS" + }; + case "DD_TRACE_AGENT_PORT": + return new string[] + { + "DATADOG_TRACE_AGENT_PORT" + }; + case "DD_TRACE_CLIENT_IP_HEADER": + return new string[] + { + "DD_APPSEC_IPHEADER" + }; + case "DD_TRACE_CONFIG_FILE": + return new string[] + { + "DD_DOTNET_TRACER_CONFIG_FILE" + }; + case "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES": + return new string[] + { + "DD_HTTP_CLIENT_ERROR_STATUSES" + }; + case "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": + return new string[] + { + "DD_HTTP_SERVER_ERROR_STATUSES" + }; + case "DD_TRACE_OTEL_ENABLED": + return new string[] + { + "DD_TRACE_ACTIVITY_LISTENER_ENABLED" + }; + case "DD_TRACE_PROPAGATION_STYLE_EXTRACT": + return new string[] + { + "DD_PROPAGATION_STYLE_EXTRACT", + "DD_TRACE_PROPAGATION_STYLE" + }; + case "DD_TRACE_PROPAGATION_STYLE_INJECT": + return new string[] + { + "DD_PROPAGATION_STYLE_INJECT", + "DD_TRACE_PROPAGATION_STYLE" + }; + case "DD_TRACE_RATE_LIMIT": + return new string[] + { + "DD_MAX_TRACES_PER_SECOND" + }; + case "OTEL_EXPORTER_OTLP_LOGS_HEADERS": + return new string[] + { + "OTEL_EXPORTER_OTLP_HEADERS" + }; + case "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": + return new string[] + { + "OTEL_EXPORTER_OTLP_PROTOCOL" + }; + case "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT": + return new string[] + { + "OTEL_EXPORTER_OTLP_TIMEOUT" + }; + case "OTEL_EXPORTER_OTLP_METRICS_HEADERS": + return new string[] + { + "OTEL_EXPORTER_OTLP_HEADERS" + }; + case "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": + return new string[] + { + "OTEL_EXPORTER_OTLP_PROTOCOL" + }; + case "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": + return new string[] + { + "OTEL_EXPORTER_OTLP_TIMEOUT" + }; + default: + return Array.Empty(); + } + } +} \ No newline at end of file diff --git a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs new file mode 100644 index 000000000000..1fb9c6357152 --- /dev/null +++ b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs @@ -0,0 +1,140 @@ +using System; + +namespace Datadog.Trace.Configuration; +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + + +/// +/// Generated configuration key matcher that handles main keys and aliases. +/// This file is auto-generated from supported-configurations.json and supported-configurations-docs.yaml. Do not edit this file directly. The source generator will regenerate it on build. +/// +internal static partial class ConfigKeyAliasesSwitcher +{ + /// + /// Gets all aliases for the given configuration key. + /// + /// The configuration key. + /// An array of aliases for the key, or empty array if no aliases exist. + public static string[] GetAliases(string mainKey) + { + switch (mainKey) + { + case "DD_AGENT_HOST": + return new string[] + { + "DATADOG_TRACE_AGENT_HOSTNAME", + "DD_TRACE_AGENT_HOSTNAME" + }; + case "DD_API_SECURITY_ENABLED": + return new string[] + { + "DD_EXPERIMENTAL_API_SECURITY_ENABLED" + }; + case "DD_EXCEPTION_REPLAY_ENABLED": + return new string[] + { + "DD_EXCEPTION_DEBUGGING_ENABLED" + }; + case "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": + return new string[] + { + "DD_INTERNAL_RCM_POLL_INTERVAL" + }; + case "DD_SERVICE": + return new string[] + { + "DD_SERVICE_NAME" + }; + case "DD_TAGS": + return new string[] + { + "DD_TRACE_GLOBAL_TAGS" + }; + case "DD_TRACE_AGENT_PORT": + return new string[] + { + "DATADOG_TRACE_AGENT_PORT" + }; + case "DD_TRACE_CLIENT_IP_HEADER": + return new string[] + { + "DD_APPSEC_IPHEADER" + }; + case "DD_TRACE_CONFIG_FILE": + return new string[] + { + "DD_DOTNET_TRACER_CONFIG_FILE" + }; + case "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES": + return new string[] + { + "DD_HTTP_CLIENT_ERROR_STATUSES" + }; + case "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": + return new string[] + { + "DD_HTTP_SERVER_ERROR_STATUSES" + }; + case "DD_TRACE_OTEL_ENABLED": + return new string[] + { + "DD_TRACE_ACTIVITY_LISTENER_ENABLED" + }; + case "DD_TRACE_PROPAGATION_STYLE_EXTRACT": + return new string[] + { + "DD_PROPAGATION_STYLE_EXTRACT", + "DD_TRACE_PROPAGATION_STYLE" + }; + case "DD_TRACE_PROPAGATION_STYLE_INJECT": + return new string[] + { + "DD_PROPAGATION_STYLE_INJECT", + "DD_TRACE_PROPAGATION_STYLE" + }; + case "DD_TRACE_RATE_LIMIT": + return new string[] + { + "DD_MAX_TRACES_PER_SECOND" + }; + case "OTEL_EXPORTER_OTLP_LOGS_HEADERS": + return new string[] + { + "OTEL_EXPORTER_OTLP_HEADERS" + }; + case "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": + return new string[] + { + "OTEL_EXPORTER_OTLP_PROTOCOL" + }; + case "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT": + return new string[] + { + "OTEL_EXPORTER_OTLP_TIMEOUT" + }; + case "OTEL_EXPORTER_OTLP_METRICS_HEADERS": + return new string[] + { + "OTEL_EXPORTER_OTLP_HEADERS" + }; + case "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": + return new string[] + { + "OTEL_EXPORTER_OTLP_PROTOCOL" + }; + case "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": + return new string[] + { + "OTEL_EXPORTER_OTLP_TIMEOUT" + }; + default: + return Array.Empty(); + } + } +} \ No newline at end of file diff --git a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs new file mode 100644 index 000000000000..1fb9c6357152 --- /dev/null +++ b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs @@ -0,0 +1,140 @@ +using System; + +namespace Datadog.Trace.Configuration; +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + + +/// +/// Generated configuration key matcher that handles main keys and aliases. +/// This file is auto-generated from supported-configurations.json and supported-configurations-docs.yaml. Do not edit this file directly. The source generator will regenerate it on build. +/// +internal static partial class ConfigKeyAliasesSwitcher +{ + /// + /// Gets all aliases for the given configuration key. + /// + /// The configuration key. + /// An array of aliases for the key, or empty array if no aliases exist. + public static string[] GetAliases(string mainKey) + { + switch (mainKey) + { + case "DD_AGENT_HOST": + return new string[] + { + "DATADOG_TRACE_AGENT_HOSTNAME", + "DD_TRACE_AGENT_HOSTNAME" + }; + case "DD_API_SECURITY_ENABLED": + return new string[] + { + "DD_EXPERIMENTAL_API_SECURITY_ENABLED" + }; + case "DD_EXCEPTION_REPLAY_ENABLED": + return new string[] + { + "DD_EXCEPTION_DEBUGGING_ENABLED" + }; + case "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": + return new string[] + { + "DD_INTERNAL_RCM_POLL_INTERVAL" + }; + case "DD_SERVICE": + return new string[] + { + "DD_SERVICE_NAME" + }; + case "DD_TAGS": + return new string[] + { + "DD_TRACE_GLOBAL_TAGS" + }; + case "DD_TRACE_AGENT_PORT": + return new string[] + { + "DATADOG_TRACE_AGENT_PORT" + }; + case "DD_TRACE_CLIENT_IP_HEADER": + return new string[] + { + "DD_APPSEC_IPHEADER" + }; + case "DD_TRACE_CONFIG_FILE": + return new string[] + { + "DD_DOTNET_TRACER_CONFIG_FILE" + }; + case "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES": + return new string[] + { + "DD_HTTP_CLIENT_ERROR_STATUSES" + }; + case "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": + return new string[] + { + "DD_HTTP_SERVER_ERROR_STATUSES" + }; + case "DD_TRACE_OTEL_ENABLED": + return new string[] + { + "DD_TRACE_ACTIVITY_LISTENER_ENABLED" + }; + case "DD_TRACE_PROPAGATION_STYLE_EXTRACT": + return new string[] + { + "DD_PROPAGATION_STYLE_EXTRACT", + "DD_TRACE_PROPAGATION_STYLE" + }; + case "DD_TRACE_PROPAGATION_STYLE_INJECT": + return new string[] + { + "DD_PROPAGATION_STYLE_INJECT", + "DD_TRACE_PROPAGATION_STYLE" + }; + case "DD_TRACE_RATE_LIMIT": + return new string[] + { + "DD_MAX_TRACES_PER_SECOND" + }; + case "OTEL_EXPORTER_OTLP_LOGS_HEADERS": + return new string[] + { + "OTEL_EXPORTER_OTLP_HEADERS" + }; + case "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": + return new string[] + { + "OTEL_EXPORTER_OTLP_PROTOCOL" + }; + case "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT": + return new string[] + { + "OTEL_EXPORTER_OTLP_TIMEOUT" + }; + case "OTEL_EXPORTER_OTLP_METRICS_HEADERS": + return new string[] + { + "OTEL_EXPORTER_OTLP_HEADERS" + }; + case "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": + return new string[] + { + "OTEL_EXPORTER_OTLP_PROTOCOL" + }; + case "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": + return new string[] + { + "OTEL_EXPORTER_OTLP_TIMEOUT" + }; + default: + return Array.Empty(); + } + } +} \ No newline at end of file diff --git a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs new file mode 100644 index 000000000000..1fb9c6357152 --- /dev/null +++ b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigKeyAliasesSwitcherGenerator/ConfigKeyAliasesSwitcher.g.cs @@ -0,0 +1,140 @@ +using System; + +namespace Datadog.Trace.Configuration; +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + + +/// +/// Generated configuration key matcher that handles main keys and aliases. +/// This file is auto-generated from supported-configurations.json and supported-configurations-docs.yaml. Do not edit this file directly. The source generator will regenerate it on build. +/// +internal static partial class ConfigKeyAliasesSwitcher +{ + /// + /// Gets all aliases for the given configuration key. + /// + /// The configuration key. + /// An array of aliases for the key, or empty array if no aliases exist. + public static string[] GetAliases(string mainKey) + { + switch (mainKey) + { + case "DD_AGENT_HOST": + return new string[] + { + "DATADOG_TRACE_AGENT_HOSTNAME", + "DD_TRACE_AGENT_HOSTNAME" + }; + case "DD_API_SECURITY_ENABLED": + return new string[] + { + "DD_EXPERIMENTAL_API_SECURITY_ENABLED" + }; + case "DD_EXCEPTION_REPLAY_ENABLED": + return new string[] + { + "DD_EXCEPTION_DEBUGGING_ENABLED" + }; + case "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": + return new string[] + { + "DD_INTERNAL_RCM_POLL_INTERVAL" + }; + case "DD_SERVICE": + return new string[] + { + "DD_SERVICE_NAME" + }; + case "DD_TAGS": + return new string[] + { + "DD_TRACE_GLOBAL_TAGS" + }; + case "DD_TRACE_AGENT_PORT": + return new string[] + { + "DATADOG_TRACE_AGENT_PORT" + }; + case "DD_TRACE_CLIENT_IP_HEADER": + return new string[] + { + "DD_APPSEC_IPHEADER" + }; + case "DD_TRACE_CONFIG_FILE": + return new string[] + { + "DD_DOTNET_TRACER_CONFIG_FILE" + }; + case "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES": + return new string[] + { + "DD_HTTP_CLIENT_ERROR_STATUSES" + }; + case "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": + return new string[] + { + "DD_HTTP_SERVER_ERROR_STATUSES" + }; + case "DD_TRACE_OTEL_ENABLED": + return new string[] + { + "DD_TRACE_ACTIVITY_LISTENER_ENABLED" + }; + case "DD_TRACE_PROPAGATION_STYLE_EXTRACT": + return new string[] + { + "DD_PROPAGATION_STYLE_EXTRACT", + "DD_TRACE_PROPAGATION_STYLE" + }; + case "DD_TRACE_PROPAGATION_STYLE_INJECT": + return new string[] + { + "DD_PROPAGATION_STYLE_INJECT", + "DD_TRACE_PROPAGATION_STYLE" + }; + case "DD_TRACE_RATE_LIMIT": + return new string[] + { + "DD_MAX_TRACES_PER_SECOND" + }; + case "OTEL_EXPORTER_OTLP_LOGS_HEADERS": + return new string[] + { + "OTEL_EXPORTER_OTLP_HEADERS" + }; + case "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": + return new string[] + { + "OTEL_EXPORTER_OTLP_PROTOCOL" + }; + case "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT": + return new string[] + { + "OTEL_EXPORTER_OTLP_TIMEOUT" + }; + case "OTEL_EXPORTER_OTLP_METRICS_HEADERS": + return new string[] + { + "OTEL_EXPORTER_OTLP_HEADERS" + }; + case "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": + return new string[] + { + "OTEL_EXPORTER_OTLP_PROTOCOL" + }; + case "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": + return new string[] + { + "OTEL_EXPORTER_OTLP_TIMEOUT" + }; + default: + return Array.Empty(); + } + } +} \ No newline at end of file diff --git a/tracer/src/Datadog.Trace/Logging/Internal/DatadogLoggingFactory.cs b/tracer/src/Datadog.Trace/Logging/Internal/DatadogLoggingFactory.cs index 7102761718f9..e1d25e174b96 100644 --- a/tracer/src/Datadog.Trace/Logging/Internal/DatadogLoggingFactory.cs +++ b/tracer/src/Datadog.Trace/Logging/Internal/DatadogLoggingFactory.cs @@ -170,12 +170,15 @@ internal static string GetLogDirectory(IConfigurationTelemetry telemetry) private static string GetLogDirectory(IConfigurationSource source, IConfigurationTelemetry telemetry) { - var logDirectory = new ConfigurationBuilder(source, telemetry).WithKeys(ConfigurationKeys.LogDirectory).AsString(); + var configurationBuilder = new ConfigurationBuilder(source, telemetry); + var logDirectory = configurationBuilder.WithKeys(ConfigurationKeys.LogDirectory).AsString(); if (string.IsNullOrEmpty(logDirectory)) { -#pragma warning disable 618 // ProfilerLogPath is deprecated but still supported - var nativeLogFile = new ConfigurationBuilder(source, telemetry).WithKeys(ConfigurationKeys.ProfilerLogPath).AsString(); -#pragma warning restore 618 + // todo, handle in phase 2 with deprecations +// ProfilerLogPath is deprecated but still supported. For now, we bypass the WithKeys analyzer, but later we want to pull deprecations differently as part of centralized file +#pragma warning disable DD0008, 618 + var nativeLogFile = configurationBuilder.WithKeys(ConfigurationKeys.TraceLogPath).AsString(); +#pragma warning restore DD0008, 618 if (!string.IsNullOrEmpty(nativeLogFile)) { diff --git a/tracer/src/Datadog.Trace/RemoteConfigurationManagement/RemoteConfigurationSettings.cs b/tracer/src/Datadog.Trace/RemoteConfigurationManagement/RemoteConfigurationSettings.cs index a9d7ca01ad0a..b666f5b452c5 100644 --- a/tracer/src/Datadog.Trace/RemoteConfigurationManagement/RemoteConfigurationSettings.cs +++ b/tracer/src/Datadog.Trace/RemoteConfigurationManagement/RemoteConfigurationSettings.cs @@ -24,9 +24,7 @@ public RemoteConfigurationSettings(IConfigurationSource? configurationSource, IC TracerVersion = TracerConstants.ThreePartVersion; var pollInterval = new ConfigurationBuilder(configurationSource, telemetry) -#pragma warning disable CS0618 - .WithKeys(ConfigurationKeys.Rcm.PollInterval, ConfigurationKeys.Rcm.PollIntervalInternal) -#pragma warning restore CS0618 + .WithKeys(ConfigurationKeys.Rcm.PollInterval) .AsDouble(DefaultPollIntervalSeconds, pollInterval => pollInterval is > 0 and <= 5); PollInterval = TimeSpan.FromSeconds(pollInterval.Value); diff --git a/tracer/test/Datadog.Trace.ClrProfiler.Managed.Tests/DbScopeFactoryTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.Managed.Tests/DbScopeFactoryTests.cs index a5a3666c4fa7..4ee0f3ca6e10 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.Managed.Tests/DbScopeFactoryTests.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.Managed.Tests/DbScopeFactoryTests.cs @@ -102,8 +102,8 @@ public async Task CreateDbCommandScope_ReturnNullForAdoNetDisabledIntegration(Ty var tracerSettings = TracerSettings.Create(new() { - { string.Format(IntegrationSettings.IntegrationEnabled, integrationName), "true" }, - { string.Format(IntegrationSettings.IntegrationEnabled, nameof(IntegrationId.AdoNet)), "false" }, + { string.Format(IntegrationSettings.IntegrationEnabledKey, integrationName), "true" }, + { string.Format(IntegrationSettings.IntegrationEnabledKey, nameof(IntegrationId.AdoNet)), "false" }, }); await using var tracer = TracerHelper.CreateWithFakeAgent(tracerSettings); @@ -750,7 +750,7 @@ private static ScopedTracer CreateTracerWithIntegrationEnabled(string integratio // Set up tracer var tracerSettings = TracerSettings.Create(new() { - { string.Format(IntegrationSettings.IntegrationEnabled, integrationName), enabled }, + { string.Format(IntegrationSettings.IntegrationEnabledKey, integrationName), enabled }, }); return TracerHelper.Create(tracerSettings); } diff --git a/tracer/test/Datadog.Trace.SourceGenerators.Tests/ConfigKeyAliasesSwitcherGeneratorTests.cs b/tracer/test/Datadog.Trace.SourceGenerators.Tests/ConfigKeyAliasesSwitcherGeneratorTests.cs new file mode 100644 index 000000000000..8981a5ba7ffd --- /dev/null +++ b/tracer/test/Datadog.Trace.SourceGenerators.Tests/ConfigKeyAliasesSwitcherGeneratorTests.cs @@ -0,0 +1,224 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Linq; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +namespace Datadog.Trace.SourceGenerators.Tests; + +public class ConfigKeyAliasesSwitcherGeneratorTests +{ + [Fact] + public void GeneratesAliasSwitchWithLazyArrays() + { + const string supportedConfigJson = """ + { + "supportedConfigurations": { + "DD_AGENT_HOST": { + "version": "A" + }, + "DD_TRACE_AGENT_URL": { + "version": "A" + } + }, + "aliases": { + "DD_AGENT_HOST": ["DATADOG_TRACE_AGENT_HOSTNAME_OPTIMIZED", "DD_TRACE_AGENT_HOSTNAME"], + "DD_TRACE_AGENT_URL": ["DD_AGENT_URL", "DD_TRACE_AGENT_PORT"] + } + } + """; + + const string expectedOutput = """ +using System; + +namespace Datadog.Trace.Configuration; +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + + +/// +/// Generated configuration key matcher that handles main keys and aliases. +/// This file is auto-generated from supported-configurations.json and supported-configurations-docs.yaml. Do not edit this file directly. The source generator will regenerate it on build. +/// +internal static partial class ConfigKeyAliasesSwitcher +{ + /// + /// Gets all aliases for the given configuration key. + /// + /// The configuration key. + /// An array of aliases for the key, or empty array if no aliases exist. + public static string[] GetAliases(string mainKey) + { + switch (mainKey) + { + case "DD_AGENT_HOST": + return new string[] + { + "DATADOG_TRACE_AGENT_HOSTNAME_OPTIMIZED", + "DD_TRACE_AGENT_HOSTNAME" + }; + case "DD_TRACE_AGENT_URL": + return new string[] + { + "DD_AGENT_URL", + "DD_TRACE_AGENT_PORT" + }; + default: + return Array.Empty(); + } + } +} +"""; + + var (diagnostics, outputs) = TestHelpers.GetGeneratedTrees( + [], + [], + [("supported-configurations.json", supportedConfigJson)], + assertOutput: false); + var output = outputs.FirstOrDefault() ?? string.Empty; + + using var s = new AssertionScope(); + diagnostics.Should().BeEmpty(); + + AssertContains(output, expectedOutput); + } + + [Fact] + public void HandlesEmptyAliasesSection() + { + const string supportedConfigJson = """ + { + "supportedConfigurations": { + "DD_AGENT_HOST": { + "version": "A" + } + }, + "aliases": {} + } + """; + + const string expectedOutput = """ +using System; + +namespace Datadog.Trace.Configuration; +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + + +/// +/// Generated configuration key matcher that handles main keys and aliases. +/// This file is auto-generated from supported-configurations.json and supported-configurations-docs.yaml. Do not edit this file directly. The source generator will regenerate it on build. +/// +internal static partial class ConfigKeyAliasesSwitcher +{ + /// + /// Gets all aliases for the given configuration key. + /// + /// The configuration key. + /// An array of aliases for the key, or empty array if no aliases exist. + public static string[] GetAliases(string mainKey) + { + switch (mainKey) + { + default: + return Array.Empty(); + } + } +} +"""; + + var (diagnostics, outputs) = TestHelpers.GetGeneratedTrees( + [], + [], + [("supported-configurations.json", supportedConfigJson)], + assertOutput: false); + var output = outputs.FirstOrDefault() ?? string.Empty; + + using var s = new AssertionScope(); + diagnostics.Should().BeEmpty(); + + AssertContains(output, expectedOutput); + } + + [Fact] + public void HandlesMissingAliasesSection() + { + const string supportedConfigJson = """ + { + "supportedConfigurations": { + "DD_AGENT_HOST": { + "version": "A" + } + } + } + """; + + const string expectedOutput = """ +using System; + +namespace Datadog.Trace.Configuration; +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +// + +#nullable enable + + +/// +/// Generated configuration key matcher that handles main keys and aliases. +/// This file is auto-generated from supported-configurations.json and supported-configurations-docs.yaml. Do not edit this file directly. The source generator will regenerate it on build. +/// +internal static partial class ConfigKeyAliasesSwitcher +{ + /// + /// Gets all aliases for the given configuration key. + /// + /// The configuration key. + /// An array of aliases for the key, or empty array if no aliases exist. + public static string[] GetAliases(string mainKey) + { + switch (mainKey) + { + default: + return Array.Empty(); + } + } +} +"""; + + var (diagnostics, outputs) = TestHelpers.GetGeneratedTrees( + [], + [], + [("supported-configurations.json", supportedConfigJson)], + assertOutput: false); + var output = outputs.FirstOrDefault() ?? string.Empty; + + using var s = new AssertionScope(); + diagnostics.Should().BeEmpty(); + + AssertContains(output, expectedOutput); + } + + private static void AssertContains(string actual, string expected) + { + var normalizedActual = string.Join("\n", actual.Split('\n').Select(l => l.TrimEnd())); + var normalizedExpected = string.Join("\n", expected.Split('\n').Select(l => l.TrimEnd())); + normalizedActual.Should().Contain(normalizedExpected); + } +} diff --git a/tracer/test/Datadog.Trace.Tests/Configuration/IntegrationSettingsTests.cs b/tracer/test/Datadog.Trace.Tests/Configuration/IntegrationSettingsTests.cs index 79074ab882d7..35c0a10d9a1e 100644 --- a/tracer/test/Datadog.Trace.Tests/Configuration/IntegrationSettingsTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Configuration/IntegrationSettingsTests.cs @@ -90,7 +90,7 @@ public void SettingsRespectsOverride(bool initiallyEnabled) var name = nameof(IntegrationId.Kafka); var source = new NameValueConfigurationSource(new() { - { string.Format(IntegrationSettings.IntegrationEnabled, name), initiallyEnabled.ToString() }, + { string.Format(IntegrationSettings.IntegrationEnabledKey, name), initiallyEnabled.ToString() }, }); var settings = new IntegrationSettings(name, source: source, isExplicitlyDisabled: true); @@ -105,7 +105,7 @@ public void SettingsRespectsOriginalIfNotOverridden(bool initiallyEnabled) var name = nameof(IntegrationId.Kafka); var source = new NameValueConfigurationSource(new() { - { string.Format(IntegrationSettings.IntegrationEnabled, name), initiallyEnabled.ToString() }, + { string.Format(IntegrationSettings.IntegrationEnabledKey, name), initiallyEnabled.ToString() }, }); var settings = new IntegrationSettings(name, source: source, isExplicitlyDisabled: false); diff --git a/tracer/test/Datadog.Trace.Tests/Configuration/Telemetry/ConfigurationBuilderTests.cs b/tracer/test/Datadog.Trace.Tests/Configuration/Telemetry/ConfigurationBuilderTests.cs index db61d2e95271..b3b6252dcc66 100644 --- a/tracer/test/Datadog.Trace.Tests/Configuration/Telemetry/ConfigurationBuilderTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Configuration/Telemetry/ConfigurationBuilderTests.cs @@ -35,7 +35,8 @@ public IConfigurationSource GetSource(IDictionary values) var data = new NameValueCollection(); foreach (var kvp in values) { - data.Add(kvp.Key, kvp.Value?.ToString()); + // use FormattableString.Invariant as europeans might have 1.23.ToString()=1,23 which makes tests fail + data.Add(kvp.Key, kvp.Value is null ? null : FormattableString.Invariant($"{kvp.Value}")); } return new NameValueConfigurationSource(data); diff --git a/tracer/test/Datadog.Trace.Tests/Logging/DatadogLoggingFactoryTests.cs b/tracer/test/Datadog.Trace.Tests/Logging/DatadogLoggingFactoryTests.cs index 7ec11ae82ed5..be756a47b586 100644 --- a/tracer/test/Datadog.Trace.Tests/Logging/DatadogLoggingFactoryTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Logging/DatadogLoggingFactoryTests.cs @@ -32,7 +32,7 @@ public void UsesLogDirectoryWhenItExists(string obsoleteLogDirectory) { { ConfigurationKeys.LogDirectory, logDirectory }, #pragma warning disable CS0618 - { ConfigurationKeys.ProfilerLogPath, obsoleteLogDirectory }, + { ConfigurationKeys.TraceLogPath, obsoleteLogDirectory }, #pragma warning restore CS0618 }); @@ -55,7 +55,7 @@ public void UsesObsoleteLogDirectoryWhenAvailable(string logDirectory) { { ConfigurationKeys.LogDirectory, logDirectory }, #pragma warning disable CS0618 - { ConfigurationKeys.ProfilerLogPath, obsoleteLogPath }, + { ConfigurationKeys.TraceLogPath, obsoleteLogPath }, #pragma warning restore CS0618 }); @@ -76,7 +76,7 @@ public void UsesEnvironmentFallBackWhenBothNull(string logDirectory, string obso { { ConfigurationKeys.LogDirectory, logDirectory }, #pragma warning disable CS0618 - { ConfigurationKeys.ProfilerLogPath, obsoleteLogDirectory }, + { ConfigurationKeys.TraceLogPath, obsoleteLogDirectory }, #pragma warning restore CS0618 }); diff --git a/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/ManualInstrumentationLegacyConfigurationSourceTests.cs b/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/ManualInstrumentationLegacyConfigurationSourceTests.cs index 1dec3714089e..ccad1421e844 100644 --- a/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/ManualInstrumentationLegacyConfigurationSourceTests.cs +++ b/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/ManualInstrumentationLegacyConfigurationSourceTests.cs @@ -31,7 +31,7 @@ public void GetIntegrationEnabled_SupportedValues_ReturnsExpectedValues(int id) { var integrationId = (IntegrationId)id; var name = IntegrationRegistry.GetName(integrationId).ToUpperInvariant(); - var enabledKey = string.Format(IntegrationSettings.IntegrationEnabled, name); + var enabledKey = string.Format(IntegrationSettings.IntegrationEnabledKey, name); var actual = ManualInstrumentationLegacyConfigurationSource.GetIntegrationEnabled(enabledKey); @@ -57,7 +57,7 @@ public void GetIntegrationEnabled_ForUnsupportedValues_ReturnsNull(int id) { var integrationId = (IntegrationId)id; var name = IntegrationRegistry.GetName(integrationId).ToUpperInvariant(); - var enabledKey = string.Format(IntegrationSettings.IntegrationEnabled, name); + var enabledKey = string.Format(IntegrationSettings.IntegrationEnabledKey, name); var actual = ManualInstrumentationLegacyConfigurationSource.GetIntegrationEnabled(enabledKey); diff --git a/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/SettingsInstrumentationTests.cs b/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/SettingsInstrumentationTests.cs index 45afb8baad92..250976057a06 100644 --- a/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/SettingsInstrumentationTests.cs +++ b/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/SettingsInstrumentationTests.cs @@ -341,7 +341,7 @@ private static TracerSettings GetAndAssertAutomaticTracerSettings() { ConfigurationKeys.TraceEnabled, false }, { ConfigurationKeys.TracerMetricsEnabled, true }, { ConfigurationKeys.AgentUri, "http://localhost:1234" }, - { string.Format(IntegrationSettings.IntegrationEnabled, nameof(IntegrationId.Aerospike)), "false" }, + { string.Format(IntegrationSettings.IntegrationEnabledKey, nameof(IntegrationId.Aerospike)), "false" }, { string.Format(IntegrationSettings.AnalyticsEnabledKey, nameof(IntegrationId.Grpc)), "true" }, { string.Format(IntegrationSettings.AnalyticsSampleRateKey, nameof(IntegrationId.Couchbase)), 0.5 }, }); diff --git a/tracer/test/Datadog.Trace.Tests/Telemetry/ConfigurationTests.cs b/tracer/test/Datadog.Trace.Tests/Telemetry/ConfigurationTests.cs index 18a30161d180..44362da34ba3 100644 --- a/tracer/test/Datadog.Trace.Tests/Telemetry/ConfigurationTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Telemetry/ConfigurationTests.cs @@ -58,6 +58,8 @@ public class ConfigurationTests "DD_INSTRUMENTATION_INSTALL_TIME", "DD_INJECTION_ENABLED", "DD_INJECT_FORCE", + // deprecated alias + "DATADOG_TRACE_AGENT_HOSTNAME_OPTIMIZED" }; // These are the keys that are used in the integration registry which are _not_ sent to telemetry, so we can ignore them diff --git a/tracer/test/Datadog.Trace.Tools.Analyzers.Tests/ConfigurationAnalyzers/ConfigurationBuilderWithKeysAnalyzerTests.cs b/tracer/test/Datadog.Trace.Tools.Analyzers.Tests/ConfigurationAnalyzers/ConfigurationBuilderWithKeysAnalyzerTests.cs new file mode 100644 index 000000000000..62923cdca99a --- /dev/null +++ b/tracer/test/Datadog.Trace.Tools.Analyzers.Tests/ConfigurationAnalyzers/ConfigurationBuilderWithKeysAnalyzerTests.cs @@ -0,0 +1,538 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Threading.Tasks; +using Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers.ConfigurationBuilderWithKeysAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace Datadog.Trace.Tools.Analyzers.Tests.ConfigurationAnalyzers; + +public class ConfigurationBuilderWithKeysAnalyzerTests +{ + private const string DD0007 = "DD0007"; // Hardcoded string literal + private const string DD0008 = "DD0008"; // Variable or expression + + [Fact] + public async Task ValidWithKeysUsingConfigurationKeys_ShouldHaveNoDiagnostics() + { + var test = new Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest + { + TestState = + { + Sources = + { + """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + public HasKeys Or(string key) => default; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public static class ConfigurationKeys + { + public const string TraceEnabled = "DD_TRACE_ENABLED"; + public const string ServiceName = "DD_SERVICE"; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public class TestClass + { + public void TestMethod() + { + var builder = new Telemetry.ConfigurationBuilder(); + builder.WithKeys(ConfigurationKeys.TraceEnabled); + builder.WithKeys(ConfigurationKeys.ServiceName) + .Or(ConfigurationKeys.TraceEnabled); + } + } + """ + } + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task ValidWithKeysUsingPlatformKeys_ShouldHaveNoDiagnostics() + { + var test = new Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest + { + TestState = + { + Sources = + { + """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + public HasKeys Or(string key) => default; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public static class PlatformKeys + { + public const string CorProfilerPath = "CORECLR_PROFILER_PATH"; + public const string AwsLambdaFunctionName = "AWS_LAMBDA_FUNCTION_NAME"; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public class TestClass + { + public void TestMethod() + { + var builder = new Telemetry.ConfigurationBuilder(); + builder.WithKeys(PlatformKeys.CorProfilerPath); + builder.WithKeys(PlatformKeys.AwsLambdaFunctionName) + .Or(PlatformKeys.CorProfilerPath); + } + } + """ + } + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task ValidWithKeysUsingNestedClasses_ShouldHaveNoDiagnostics() + { + var test = new Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest + { + TestState = + { + Sources = + { + """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + public HasKeys Or(string key) => default; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public static class ConfigurationKeys + { + public static class CIVisibility + { + public const string Enabled = "DD_CIVISIBILITY_ENABLED"; + } + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public class TestClass + { + public void TestMethod() + { + var builder = new Telemetry.ConfigurationBuilder(); + builder.WithKeys(ConfigurationKeys.CIVisibility.Enabled); + } + } + """ + } + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task WithKeysUsingHardcodedString_ShouldReportDD0007() + { + var code = """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + } + + public class TestClass + { + public void TestMethod() + { + var builder = new ConfigurationBuilder(); + builder.WithKeys({|#0:"DD_TRACE_ENABLED"|}); + } + } + """; + + var expected = new DiagnosticResult(DD0007, DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("WithKeys", "DD_TRACE_ENABLED"); + + await Verifier.VerifyAnalyzerAsync(code, expected); + } + + [Fact] + public async Task OrMethodUsingHardcodedString_ShouldReportDD0007() + { + var test = new Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest + { + TestState = + { + Sources = + { + """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + public HasKeys Or(string key) => default; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public static class ConfigurationKeys + { + public const string TraceEnabled = "DD_TRACE_ENABLED"; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public class TestClass + { + public void TestMethod() + { + var builder = new Telemetry.ConfigurationBuilder(); + builder.WithKeys(ConfigurationKeys.TraceEnabled) + .Or({|#0:"DD_SERVICE"|}); + } + } + """ + }, + ExpectedDiagnostics = + { + new DiagnosticResult(DD0007, DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("Or", "DD_SERVICE") + } + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task WithKeysUsingVariable_ShouldReportDD0008() + { + var code = """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + } + + public class TestClass + { + public void TestMethod() + { + var builder = new ConfigurationBuilder(); + var myKey = "DD_TRACE_ENABLED"; + builder.WithKeys({|#0:myKey|}); + } + } + """; + + var expected = new DiagnosticResult(DD0008, DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("WithKeys", "myKey"); + + await Verifier.VerifyAnalyzerAsync(code, expected); + } + + [Fact] + public async Task WithKeysUsingMethodCall_ShouldReportDD0008() + { + var code = """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + } + + public class TestClass + { + public void TestMethod() + { + var builder = new ConfigurationBuilder(); + builder.WithKeys({|#0:GetKey()|}); + } + + private string GetKey() => "DD_TRACE_ENABLED"; + } + """; + + var expected = new DiagnosticResult(DD0008, DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("WithKeys", "GetKey()"); + + await Verifier.VerifyAnalyzerAsync(code, expected); + } + + [Fact] + public async Task WithKeysUsingStringInterpolation_ShouldReportDD0008() + { + var code = """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + } + + public class TestClass + { + public void TestMethod() + { + var builder = new ConfigurationBuilder(); + var prefix = "DD_"; + builder.WithKeys({|#0:$"{prefix}TRACE_ENABLED"|}); + } + } + """; + + var expected = new DiagnosticResult(DD0008, DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("WithKeys", "$\"{prefix}TRACE_ENABLED\""); + + await Verifier.VerifyAnalyzerAsync(code, expected); + } + + [Fact] + public async Task WithKeysUsingConstantFromWrongClass_ShouldReportDD0008() + { + var test = new Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest + { + TestState = + { + Sources = + { + """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + } + """, + """ + #nullable enable + namespace SomeOther.Namespace; + + public static class MyKeys + { + public const string TraceEnabled = "DD_TRACE_ENABLED"; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public class TestClass + { + public void TestMethod() + { + var builder = new ConfigurationBuilder(); + builder.WithKeys({|#0:SomeOther.Namespace.MyKeys.TraceEnabled|}); + } + } + """ + }, + ExpectedDiagnostics = + { + new DiagnosticResult(DD0008, DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("WithKeys", "SomeOther.Namespace.MyKeys.TraceEnabled") + } + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task MultipleViolations_ShouldReportMultipleDiagnostics() + { + var test = new Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest + { + TestState = + { + Sources = + { + """ + #nullable enable + namespace Datadog.Trace.Configuration.Telemetry; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + public HasKeys Or(string key) => default; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public static class ConfigurationKeys + { + public const string TraceEnabled = "DD_TRACE_ENABLED"; + } + """, + """ + #nullable enable + namespace Datadog.Trace.Configuration; + + public class TestClass + { + public void TestMethod() + { + var builder = new Telemetry.ConfigurationBuilder(); + var myKey = "DD_SERVICE"; + + builder.WithKeys({|#0:"DD_ENV"|}); + builder.WithKeys({|#1:myKey|}); + builder.WithKeys(ConfigurationKeys.TraceEnabled) + .Or({|#2:"DD_VERSION"|}); + } + } + """ + }, + ExpectedDiagnostics = + { + new DiagnosticResult(DD0007, DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("WithKeys", "DD_ENV"), + new DiagnosticResult(DD0008, DiagnosticSeverity.Error) + .WithLocation(1) + .WithArguments("WithKeys", "myKey"), + new DiagnosticResult(DD0007, DiagnosticSeverity.Error) + .WithLocation(2) + .WithArguments("Or", "DD_VERSION") + } + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task DifferentWithKeysMethodInDifferentNamespace_ShouldHaveNoDiagnostics() + { + var code = """ + #nullable enable + namespace SomeOther.Namespace; + + public struct ConfigurationBuilder + { + public HasKeys WithKeys(string key) => default; + } + + public struct HasKeys + { + public HasKeys Or(string key) => default; + } + + public class TestClass + { + public void TestMethod() + { + var builder = new ConfigurationBuilder(); + builder.WithKeys("DD_TRACE_ENABLED"); + builder.WithKeys("DD_SERVICE").Or("DD_ENV"); + } + } + """; + + await Verifier.VerifyAnalyzerAsync(code); + } +} diff --git a/tracer/test/Datadog.Trace.Tools.Analyzers.Tests/ConfigurationAnalyzers/PlatformKeysAnalyzerTests.cs b/tracer/test/Datadog.Trace.Tools.Analyzers.Tests/ConfigurationAnalyzers/PlatformKeysAnalyzerTests.cs new file mode 100644 index 000000000000..b85650802621 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tools.Analyzers.Tests/ConfigurationAnalyzers/PlatformKeysAnalyzerTests.cs @@ -0,0 +1,146 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers.PlatformKeysAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace Datadog.Trace.Tools.Analyzers.Tests.ConfigurationAnalyzers; + +public class PlatformKeysAnalyzerTests +{ + private const string DiagnosticId = Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers.PlatformKeysAnalyzer.DiagnosticId; + + [Fact] + public async Task ValidPlatformKeysAndEdgeCasesShouldNotHaveDiagnostics() + { + var code = """ + #nullable enable + namespace Datadog.Trace.Configuration; + + internal static partial class PlatformKeys + { + // Valid platform keys + public const string ValidKey1 = "CORECLR_PROFILER_PATH"; + public const string ValidKey2 = "AWS_LAMBDA_FUNCTION_NAME"; + public const string ValidKey3 = "WEBSITE_SITE_NAME"; + + // Non-const fields should be ignored + public static readonly string ReadOnlyField = "DD_TRACE_ENABLED"; + public static string StaticField = "OTEL_SERVICE_NAME"; + + // Non-string constants should be ignored + public const int IntConstant = 42; + public const bool BoolConstant = true; + + // Edge cases - prefixes in middle/end should NOT trigger + public const string OtelButNotPrefix = "SOMETHING_OTEL_VALUE"; + public const string DdButNotPrefix = "SOMETHING_DD_VALUE"; + + internal class Aws + { + public const string FunctionName = "AWS_LAMBDA_FUNCTION_NAME"; + public const string Region = "AWS_REGION"; + } + } + """; + + await Verifier.VerifyAnalyzerAsync(code); + } + + [Theory] + [InlineData("OTEL_RESOURCE_ATTRIBUTES", "OTEL")] // Uppercase + [InlineData("otel_service_name", "OTEL")] // Lowercase (case insensitive) + [InlineData("Otel_Exporter_Endpoint", "OTEL")] // Mixed case + [InlineData("DD_TRACE_ENABLED", "DD_")] // Uppercase + [InlineData("dd_agent_host", "DD_")] // Lowercase (case insensitive) + [InlineData("Dd_Version", "DD_")] // Mixed case + [InlineData("_DD_TRACE_DEBUG", "_DD_")] // Uppercase + [InlineData("_dd_profiler_enabled", "_DD_")] // Lowercase (case insensitive) + [InlineData("_Dd_Test_Config", "_DD_")] // Mixed case + public async Task InvalidPlatformKeysConstantsShouldHaveDiagnostics(string invalidValue, string expectedPrefix) + { + var code = $$""" + #nullable enable + namespace Datadog.Trace.Configuration; + + internal static partial class PlatformKeys + { + public const string {|#0:InvalidKey|} = "{{invalidValue}}"; + } + """; + + var expected = new DiagnosticResult(DiagnosticId, DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments(invalidValue, expectedPrefix); + + await Verifier.VerifyAnalyzerAsync(code, expected); + } + + [Fact] + public async Task MultipleInvalidConstantsIncludingNestedClassesShouldHaveMultipleDiagnostics() + { + var code = """ + #nullable enable + namespace Datadog.Trace.Configuration; + + internal static partial class PlatformKeys + { + public const string {|#0:InvalidOtelKey|} = "OTEL_SERVICE_NAME"; + public const string ValidKey = "AWS_LAMBDA_FUNCTION_NAME"; + public const string {|#1:InvalidDdKey|} = "dd_trace_enabled"; + + internal class TestPlatform + { + public const string {|#2:InvalidInternalKey|} = "_DD_PROFILER_ENABLED"; + public const string ValidNestedKey = "WEBSITE_SITE_NAME"; + } + } + """; + + var expected1 = new DiagnosticResult(DiagnosticId, DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("OTEL_SERVICE_NAME", "OTEL"); + + var expected2 = new DiagnosticResult(DiagnosticId, DiagnosticSeverity.Error) + .WithLocation(1) + .WithArguments("dd_trace_enabled", "DD_"); + + var expected3 = new DiagnosticResult(DiagnosticId, DiagnosticSeverity.Error) + .WithLocation(2) + .WithArguments("_DD_PROFILER_ENABLED", "_DD_"); + + await Verifier.VerifyAnalyzerAsync(code, expected1, expected2, expected3); + } + + [Fact] + public async Task DifferentNamespaceAndClassNameShouldNotHaveDiagnostics() + { + var code = """ + #nullable enable + namespace SomeOther.Namespace + { + internal static partial class PlatformKeys + { + public const string ShouldNotBeAnalyzed = "DD_TRACE_ENABLED"; + } + } + + namespace Datadog.Trace.Configuration + { + internal static partial class ConfigurationKeys + { + public const string AlsoNotAnalyzed = "OTEL_SERVICE_NAME"; + } + } + """; + + await Verifier.VerifyAnalyzerAsync(code); + } +}