up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,271 +1,271 @@
using System;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace StellaOps.Telemetry.Analyzers;
/// <summary>
/// Analyzes metric label usage to prevent high-cardinality labels and enforce naming conventions.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MetricLabelAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// Diagnostic ID for high-cardinality label patterns.
/// </summary>
public const string HighCardinalityDiagnosticId = "TELEM001";
/// <summary>
/// Diagnostic ID for invalid label key format.
/// </summary>
public const string InvalidLabelKeyDiagnosticId = "TELEM002";
/// <summary>
/// Diagnostic ID for dynamic label values.
/// </summary>
public const string DynamicLabelDiagnosticId = "TELEM003";
private static readonly LocalizableString HighCardinalityTitle = "Potential high-cardinality metric label detected";
private static readonly LocalizableString HighCardinalityMessage = "Label key '{0}' may cause high cardinality. Avoid using IDs, timestamps, or user-specific values as labels.";
private static readonly LocalizableString HighCardinalityDescription = "High-cardinality labels can cause memory exhaustion and poor query performance. Use bounded, categorical values instead.";
private static readonly LocalizableString InvalidKeyTitle = "Invalid metric label key format";
private static readonly LocalizableString InvalidKeyMessage = "Label key '{0}' should use snake_case and contain only lowercase letters, digits, and underscores.";
private static readonly LocalizableString InvalidKeyDescription = "Metric label keys should follow Prometheus naming conventions: lowercase snake_case with only [a-z0-9_] characters.";
private static readonly LocalizableString DynamicLabelTitle = "Dynamic metric label value detected";
private static readonly LocalizableString DynamicLabelMessage = "Metric label value appears to be dynamically generated. Consider using predefined constants or enums.";
private static readonly LocalizableString DynamicLabelDescription = "Dynamic label values can lead to unbounded cardinality. Use constants, enums, or validated bounded sets.";
private static readonly DiagnosticDescriptor HighCardinalityRule = new(
HighCardinalityDiagnosticId,
HighCardinalityTitle,
HighCardinalityMessage,
"Performance",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: HighCardinalityDescription);
private static readonly DiagnosticDescriptor InvalidKeyRule = new(
InvalidLabelKeyDiagnosticId,
InvalidKeyTitle,
InvalidKeyMessage,
"Naming",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: InvalidKeyDescription);
private static readonly DiagnosticDescriptor DynamicLabelRule = new(
DynamicLabelDiagnosticId,
DynamicLabelTitle,
DynamicLabelMessage,
"Performance",
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: DynamicLabelDescription);
// Patterns that suggest high-cardinality labels
private static readonly string[] HighCardinalityPatterns =
{
"id", "guid", "uuid", "user_id", "request_id", "session_id", "transaction_id",
"timestamp", "datetime", "time", "date",
"email", "username", "name", "ip", "address",
"path", "url", "uri", "query",
"message", "error_message", "description", "body", "content"
};
// Valid label key pattern: lowercase snake_case
private static readonly Regex ValidLabelKeyPattern = new(@"^[a-z][a-z0-9_]*$", RegexOptions.Compiled);
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(HighCardinalityRule, InvalidKeyRule, DynamicLabelRule);
/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
// Analyze invocations of metric methods
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
}
private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
{
if (context.Node is not InvocationExpressionSyntax invocation)
{
return;
}
var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation);
if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
{
return;
}
// Check if this is a metric recording method
if (!IsMetricMethod(methodSymbol))
{
return;
}
// Analyze the arguments for label-related patterns
foreach (var argument in invocation.ArgumentList.Arguments)
{
AnalyzeArgument(context, argument);
}
}
private static bool IsMetricMethod(IMethodSymbol method)
{
var containingType = method.ContainingType?.ToDisplayString();
// Check for GoldenSignalMetrics methods
if (containingType?.Contains("GoldenSignalMetrics") == true)
{
return method.Name is "RecordLatency" or "IncrementErrors" or "IncrementRequests" or "Tag";
}
// Check for System.Diagnostics.Metrics methods
if (containingType?.StartsWith("System.Diagnostics.Metrics.") == true)
{
return method.Name is "Record" or "Add" or "CreateCounter" or "CreateHistogram" or "CreateGauge";
}
// Check for OpenTelemetry methods
if (containingType?.Contains("OpenTelemetry") == true && containingType.Contains("Meter"))
{
return true;
}
return false;
}
private static void AnalyzeArgument(SyntaxNodeAnalysisContext context, ArgumentSyntax argument)
{
// Check for KeyValuePair creation (Tag method calls)
if (argument.Expression is InvocationExpressionSyntax tagInvocation)
{
var tagSymbol = context.SemanticModel.GetSymbolInfo(tagInvocation).Symbol as IMethodSymbol;
if (tagSymbol?.Name == "Tag" && tagInvocation.ArgumentList.Arguments.Count >= 2)
{
var keyArg = tagInvocation.ArgumentList.Arguments[0];
var valueArg = tagInvocation.ArgumentList.Arguments[1];
AnalyzeLabelKey(context, keyArg.Expression);
AnalyzeLabelValue(context, valueArg.Expression);
}
}
// Check for new KeyValuePair<string, object?>(key, value)
if (argument.Expression is ObjectCreationExpressionSyntax objectCreation)
{
var typeSymbol = context.SemanticModel.GetSymbolInfo(objectCreation.Type).Symbol as INamedTypeSymbol;
if (typeSymbol?.Name == "KeyValuePair" && objectCreation.ArgumentList?.Arguments.Count >= 2)
{
var keyArg = objectCreation.ArgumentList.Arguments[0];
var valueArg = objectCreation.ArgumentList.Arguments[1];
AnalyzeLabelKey(context, keyArg.Expression);
AnalyzeLabelValue(context, valueArg.Expression);
}
}
// Check for tuple-like implicit conversions
if (argument.Expression is TupleExpressionSyntax tuple && tuple.Arguments.Count >= 2)
{
AnalyzeLabelKey(context, tuple.Arguments[0].Expression);
AnalyzeLabelValue(context, tuple.Arguments[1].Expression);
}
}
private static void AnalyzeLabelKey(SyntaxNodeAnalysisContext context, ExpressionSyntax expression)
{
// Get the constant value if it's a literal or const
var constantValue = context.SemanticModel.GetConstantValue(expression);
if (!constantValue.HasValue || constantValue.Value is not string keyString)
{
return;
}
// Check for valid label key format
if (!ValidLabelKeyPattern.IsMatch(keyString))
{
var diagnostic = Diagnostic.Create(InvalidKeyRule, expression.GetLocation(), keyString);
context.ReportDiagnostic(diagnostic);
}
// Check for high-cardinality patterns
var keyLower = keyString.ToLowerInvariant();
foreach (var pattern in HighCardinalityPatterns)
{
if (keyLower.Contains(pattern))
{
var diagnostic = Diagnostic.Create(HighCardinalityRule, expression.GetLocation(), keyString);
context.ReportDiagnostic(diagnostic);
break;
}
}
}
private static void AnalyzeLabelValue(SyntaxNodeAnalysisContext context, ExpressionSyntax expression)
{
// If the value is a literal string or const, it's fine
var constantValue = context.SemanticModel.GetConstantValue(expression);
if (constantValue.HasValue)
{
return;
}
// Check if it's an enum member access - that's fine
var typeInfo = context.SemanticModel.GetTypeInfo(expression);
if (typeInfo.Type?.TypeKind == TypeKind.Enum)
{
return;
}
// Check if it's accessing a static/const field - that's fine
if (expression is MemberAccessExpressionSyntax memberAccess)
{
var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (symbol is IFieldSymbol { IsConst: true } or IFieldSymbol { IsStatic: true, IsReadOnly: true })
{
return;
}
}
// Check for .ToString() calls on enums - that's fine
if (expression is InvocationExpressionSyntax toStringInvocation)
{
if (toStringInvocation.Expression is MemberAccessExpressionSyntax toStringAccess &&
toStringAccess.Name.Identifier.Text == "ToString")
{
var targetTypeInfo = context.SemanticModel.GetTypeInfo(toStringAccess.Expression);
if (targetTypeInfo.Type?.TypeKind == TypeKind.Enum)
{
return;
}
}
}
// Flag potentially dynamic values
if (expression is IdentifierNameSyntax or
InvocationExpressionSyntax or
InterpolatedStringExpressionSyntax or
BinaryExpressionSyntax)
{
var diagnostic = Diagnostic.Create(DynamicLabelRule, expression.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
using System;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace StellaOps.Telemetry.Analyzers;
/// <summary>
/// Analyzes metric label usage to prevent high-cardinality labels and enforce naming conventions.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MetricLabelAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// Diagnostic ID for high-cardinality label patterns.
/// </summary>
public const string HighCardinalityDiagnosticId = "TELEM001";
/// <summary>
/// Diagnostic ID for invalid label key format.
/// </summary>
public const string InvalidLabelKeyDiagnosticId = "TELEM002";
/// <summary>
/// Diagnostic ID for dynamic label values.
/// </summary>
public const string DynamicLabelDiagnosticId = "TELEM003";
private static readonly LocalizableString HighCardinalityTitle = "Potential high-cardinality metric label detected";
private static readonly LocalizableString HighCardinalityMessage = "Label key '{0}' may cause high cardinality. Avoid using IDs, timestamps, or user-specific values as labels.";
private static readonly LocalizableString HighCardinalityDescription = "High-cardinality labels can cause memory exhaustion and poor query performance. Use bounded, categorical values instead.";
private static readonly LocalizableString InvalidKeyTitle = "Invalid metric label key format";
private static readonly LocalizableString InvalidKeyMessage = "Label key '{0}' should use snake_case and contain only lowercase letters, digits, and underscores.";
private static readonly LocalizableString InvalidKeyDescription = "Metric label keys should follow Prometheus naming conventions: lowercase snake_case with only [a-z0-9_] characters.";
private static readonly LocalizableString DynamicLabelTitle = "Dynamic metric label value detected";
private static readonly LocalizableString DynamicLabelMessage = "Metric label value appears to be dynamically generated. Consider using predefined constants or enums.";
private static readonly LocalizableString DynamicLabelDescription = "Dynamic label values can lead to unbounded cardinality. Use constants, enums, or validated bounded sets.";
private static readonly DiagnosticDescriptor HighCardinalityRule = new(
HighCardinalityDiagnosticId,
HighCardinalityTitle,
HighCardinalityMessage,
"Performance",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: HighCardinalityDescription);
private static readonly DiagnosticDescriptor InvalidKeyRule = new(
InvalidLabelKeyDiagnosticId,
InvalidKeyTitle,
InvalidKeyMessage,
"Naming",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: InvalidKeyDescription);
private static readonly DiagnosticDescriptor DynamicLabelRule = new(
DynamicLabelDiagnosticId,
DynamicLabelTitle,
DynamicLabelMessage,
"Performance",
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: DynamicLabelDescription);
// Patterns that suggest high-cardinality labels
private static readonly string[] HighCardinalityPatterns =
{
"id", "guid", "uuid", "user_id", "request_id", "session_id", "transaction_id",
"timestamp", "datetime", "time", "date",
"email", "username", "name", "ip", "address",
"path", "url", "uri", "query",
"message", "error_message", "description", "body", "content"
};
// Valid label key pattern: lowercase snake_case
private static readonly Regex ValidLabelKeyPattern = new(@"^[a-z][a-z0-9_]*$", RegexOptions.Compiled);
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(HighCardinalityRule, InvalidKeyRule, DynamicLabelRule);
/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
// Analyze invocations of metric methods
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
}
private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
{
if (context.Node is not InvocationExpressionSyntax invocation)
{
return;
}
var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation);
if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
{
return;
}
// Check if this is a metric recording method
if (!IsMetricMethod(methodSymbol))
{
return;
}
// Analyze the arguments for label-related patterns
foreach (var argument in invocation.ArgumentList.Arguments)
{
AnalyzeArgument(context, argument);
}
}
private static bool IsMetricMethod(IMethodSymbol method)
{
var containingType = method.ContainingType?.ToDisplayString();
// Check for GoldenSignalMetrics methods
if (containingType?.Contains("GoldenSignalMetrics") == true)
{
return method.Name is "RecordLatency" or "IncrementErrors" or "IncrementRequests" or "Tag";
}
// Check for System.Diagnostics.Metrics methods
if (containingType?.StartsWith("System.Diagnostics.Metrics.") == true)
{
return method.Name is "Record" or "Add" or "CreateCounter" or "CreateHistogram" or "CreateGauge";
}
// Check for OpenTelemetry methods
if (containingType?.Contains("OpenTelemetry") == true && containingType.Contains("Meter"))
{
return true;
}
return false;
}
private static void AnalyzeArgument(SyntaxNodeAnalysisContext context, ArgumentSyntax argument)
{
// Check for KeyValuePair creation (Tag method calls)
if (argument.Expression is InvocationExpressionSyntax tagInvocation)
{
var tagSymbol = context.SemanticModel.GetSymbolInfo(tagInvocation).Symbol as IMethodSymbol;
if (tagSymbol?.Name == "Tag" && tagInvocation.ArgumentList.Arguments.Count >= 2)
{
var keyArg = tagInvocation.ArgumentList.Arguments[0];
var valueArg = tagInvocation.ArgumentList.Arguments[1];
AnalyzeLabelKey(context, keyArg.Expression);
AnalyzeLabelValue(context, valueArg.Expression);
}
}
// Check for new KeyValuePair<string, object?>(key, value)
if (argument.Expression is ObjectCreationExpressionSyntax objectCreation)
{
var typeSymbol = context.SemanticModel.GetSymbolInfo(objectCreation.Type).Symbol as INamedTypeSymbol;
if (typeSymbol?.Name == "KeyValuePair" && objectCreation.ArgumentList?.Arguments.Count >= 2)
{
var keyArg = objectCreation.ArgumentList.Arguments[0];
var valueArg = objectCreation.ArgumentList.Arguments[1];
AnalyzeLabelKey(context, keyArg.Expression);
AnalyzeLabelValue(context, valueArg.Expression);
}
}
// Check for tuple-like implicit conversions
if (argument.Expression is TupleExpressionSyntax tuple && tuple.Arguments.Count >= 2)
{
AnalyzeLabelKey(context, tuple.Arguments[0].Expression);
AnalyzeLabelValue(context, tuple.Arguments[1].Expression);
}
}
private static void AnalyzeLabelKey(SyntaxNodeAnalysisContext context, ExpressionSyntax expression)
{
// Get the constant value if it's a literal or const
var constantValue = context.SemanticModel.GetConstantValue(expression);
if (!constantValue.HasValue || constantValue.Value is not string keyString)
{
return;
}
// Check for valid label key format
if (!ValidLabelKeyPattern.IsMatch(keyString))
{
var diagnostic = Diagnostic.Create(InvalidKeyRule, expression.GetLocation(), keyString);
context.ReportDiagnostic(diagnostic);
}
// Check for high-cardinality patterns
var keyLower = keyString.ToLowerInvariant();
foreach (var pattern in HighCardinalityPatterns)
{
if (keyLower.Contains(pattern))
{
var diagnostic = Diagnostic.Create(HighCardinalityRule, expression.GetLocation(), keyString);
context.ReportDiagnostic(diagnostic);
break;
}
}
}
private static void AnalyzeLabelValue(SyntaxNodeAnalysisContext context, ExpressionSyntax expression)
{
// If the value is a literal string or const, it's fine
var constantValue = context.SemanticModel.GetConstantValue(expression);
if (constantValue.HasValue)
{
return;
}
// Check if it's an enum member access - that's fine
var typeInfo = context.SemanticModel.GetTypeInfo(expression);
if (typeInfo.Type?.TypeKind == TypeKind.Enum)
{
return;
}
// Check if it's accessing a static/const field - that's fine
if (expression is MemberAccessExpressionSyntax memberAccess)
{
var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (symbol is IFieldSymbol { IsConst: true } or IFieldSymbol { IsStatic: true, IsReadOnly: true })
{
return;
}
}
// Check for .ToString() calls on enums - that's fine
if (expression is InvocationExpressionSyntax toStringInvocation)
{
if (toStringInvocation.Expression is MemberAccessExpressionSyntax toStringAccess &&
toStringAccess.Name.Identifier.Text == "ToString")
{
var targetTypeInfo = context.SemanticModel.GetTypeInfo(toStringAccess.Expression);
if (targetTypeInfo.Type?.TypeKind == TypeKind.Enum)
{
return;
}
}
}
// Flag potentially dynamic values
if (expression is IdentifierNameSyntax or
InvocationExpressionSyntax or
InterpolatedStringExpressionSyntax or
BinaryExpressionSyntax)
{
var diagnostic = Diagnostic.Create(DynamicLabelRule, expression.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}

View File

@@ -1,478 +1,478 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Xunit;
using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier<StellaOps.Telemetry.Analyzers.MetricLabelAnalyzer>;
namespace StellaOps.Telemetry.Analyzers.Tests;
public sealed class MetricLabelAnalyzerTests
{
[Fact]
public async Task ValidLabelKey_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status_code", "200"));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task InvalidLabelKey_UpperCase_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag({|#0:"StatusCode"|}, "200"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.InvalidLabelKeyDiagnosticId)
.WithLocation(0)
.WithArguments("StatusCode");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task HighCardinalityLabelKey_UserId_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag({|#0:"user_id"|}, "123"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId)
.WithLocation(0)
.WithArguments("user_id");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task HighCardinalityLabelKey_RequestId_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void IncrementRequests(params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.IncrementRequests(GoldenSignalMetrics.Tag({|#0:"request_id"|}, "abc-123"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId)
.WithLocation(0)
.WithArguments("request_id");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task HighCardinalityLabelKey_Email_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void IncrementErrors(params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.IncrementErrors(GoldenSignalMetrics.Tag({|#0:"user_email"|}, "test@example.com"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId)
.WithLocation(0)
.WithArguments("user_email");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task DynamicLabelValue_Variable_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod(string dynamicValue)
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("operation", {|#0:dynamicValue|}));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.DynamicLabelDiagnosticId)
.WithLocation(0);
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task DynamicLabelValue_InterpolatedString_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod(int code)
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", {|#0:$"code_{code}"|}));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.DynamicLabelDiagnosticId)
.WithLocation(0);
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task StaticLabelValue_Constant_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
private const string StatusOk = "ok";
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", StatusOk));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task EnumLabelValue_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public enum Status { Ok, Error }
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", Status.Ok));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task EnumToStringLabelValue_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public enum Status { Ok, Error }
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", Status.Ok.ToString()));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task TupleSyntax_ValidLabel_NoDiagnostic()
{
var test = """
using System;
using System.Diagnostics.Metrics;
namespace TestNamespace
{
public class TestClass
{
public void TestMethod(Counter<int> counter)
{
counter.Add(1, ("status_code", "200"));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task KeyValuePairCreation_HighCardinalityKey_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
namespace TestNamespace
{
public class TestClass
{
public void TestMethod(Counter<int> counter)
{
counter.Add(1, new KeyValuePair<string, object?>({|#0:"session_id"|}, "abc"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId)
.WithLocation(0)
.WithArguments("session_id");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task NonMetricMethod_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class RegularClass
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void SomeMethod(params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var obj = new RegularClass();
obj.SomeMethod(RegularClass.Tag("user_id", "123"));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task MultipleIssues_ReportsAllDiagnostics()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod(string dynamicValue)
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0,
GoldenSignalMetrics.Tag({|#0:"UserId"|}, "static"),
GoldenSignalMetrics.Tag("operation", {|#1:dynamicValue|}));
}
}
}
""";
var expected1 = Verifier.Diagnostic(MetricLabelAnalyzer.InvalidLabelKeyDiagnosticId)
.WithLocation(0)
.WithArguments("UserId");
var expected2 = Verifier.Diagnostic(MetricLabelAnalyzer.DynamicLabelDiagnosticId)
.WithLocation(1);
await Verifier.VerifyAnalyzerAsync(test, expected1, expected2);
}
[Fact]
public async Task StaticReadonlyField_LabelValue_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public static class Labels
{
public static readonly string StatusOk = "ok";
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", Labels.StatusOk));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
}
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Xunit;
using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier<StellaOps.Telemetry.Analyzers.MetricLabelAnalyzer>;
namespace StellaOps.Telemetry.Analyzers.Tests;
public sealed class MetricLabelAnalyzerTests
{
[Fact]
public async Task ValidLabelKey_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status_code", "200"));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task InvalidLabelKey_UpperCase_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag({|#0:"StatusCode"|}, "200"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.InvalidLabelKeyDiagnosticId)
.WithLocation(0)
.WithArguments("StatusCode");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task HighCardinalityLabelKey_UserId_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag({|#0:"user_id"|}, "123"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId)
.WithLocation(0)
.WithArguments("user_id");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task HighCardinalityLabelKey_RequestId_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void IncrementRequests(params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.IncrementRequests(GoldenSignalMetrics.Tag({|#0:"request_id"|}, "abc-123"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId)
.WithLocation(0)
.WithArguments("request_id");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task HighCardinalityLabelKey_Email_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void IncrementErrors(params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.IncrementErrors(GoldenSignalMetrics.Tag({|#0:"user_email"|}, "test@example.com"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId)
.WithLocation(0)
.WithArguments("user_email");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task DynamicLabelValue_Variable_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod(string dynamicValue)
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("operation", {|#0:dynamicValue|}));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.DynamicLabelDiagnosticId)
.WithLocation(0);
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task DynamicLabelValue_InterpolatedString_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod(int code)
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", {|#0:$"code_{code}"|}));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.DynamicLabelDiagnosticId)
.WithLocation(0);
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task StaticLabelValue_Constant_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
private const string StatusOk = "ok";
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", StatusOk));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task EnumLabelValue_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public enum Status { Ok, Error }
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", Status.Ok));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task EnumToStringLabelValue_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public enum Status { Ok, Error }
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", Status.Ok.ToString()));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task TupleSyntax_ValidLabel_NoDiagnostic()
{
var test = """
using System;
using System.Diagnostics.Metrics;
namespace TestNamespace
{
public class TestClass
{
public void TestMethod(Counter<int> counter)
{
counter.Add(1, ("status_code", "200"));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task KeyValuePairCreation_HighCardinalityKey_ReportsDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
namespace TestNamespace
{
public class TestClass
{
public void TestMethod(Counter<int> counter)
{
counter.Add(1, new KeyValuePair<string, object?>({|#0:"session_id"|}, "abc"));
}
}
}
""";
var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId)
.WithLocation(0)
.WithArguments("session_id");
await Verifier.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task NonMetricMethod_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class RegularClass
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void SomeMethod(params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod()
{
var obj = new RegularClass();
obj.SomeMethod(RegularClass.Tag("user_id", "123"));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
[Fact]
public async Task MultipleIssues_ReportsAllDiagnostics()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public class TestClass
{
public void TestMethod(string dynamicValue)
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0,
GoldenSignalMetrics.Tag({|#0:"UserId"|}, "static"),
GoldenSignalMetrics.Tag("operation", {|#1:dynamicValue|}));
}
}
}
""";
var expected1 = Verifier.Diagnostic(MetricLabelAnalyzer.InvalidLabelKeyDiagnosticId)
.WithLocation(0)
.WithArguments("UserId");
var expected2 = Verifier.Diagnostic(MetricLabelAnalyzer.DynamicLabelDiagnosticId)
.WithLocation(1);
await Verifier.VerifyAnalyzerAsync(test, expected1, expected2);
}
[Fact]
public async Task StaticReadonlyField_LabelValue_NoDiagnostic()
{
var test = """
using System;
using System.Collections.Generic;
namespace TestNamespace
{
public class GoldenSignalMetrics
{
public static KeyValuePair<string, object?> Tag(string key, object? value) => new(key, value);
public void RecordLatency(double value, params KeyValuePair<string, object?>[] tags) { }
}
public static class Labels
{
public static readonly string StatusOk = "ok";
}
public class TestClass
{
public void TestMethod()
{
var metrics = new GoldenSignalMetrics();
metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", Labels.StatusOk));
}
}
}
""";
await Verifier.VerifyAnalyzerAsync(test);
}
}