diff --git a/src/StackExchange.Redis/LoggerExtensions.cs b/src/StackExchange.Redis/LoggerExtensions.cs index 69e117927..6617f3df6 100644 --- a/src/StackExchange.Redis/LoggerExtensions.cs +++ b/src/StackExchange.Redis/LoggerExtensions.cs @@ -71,6 +71,7 @@ internal static void LogWithThreadPoolStats(this ILogger? log, string message) // awkwardly they read. If a name genuinely must change, pin the old one via the attribute's EventName. [LoggerMessage( Level = LogLevel.Error, + EventId = 127, Message = "Connection failed: {EndPoint} ({ConnectionType}, {FailureType}): {ErrorMessage}")] internal static partial void LogErrorConnectionFailed(this ILogger logger, Exception? exception, EndPointLogValue endPoint, ConnectionType connectionType, ConnectionFailureType failureType, string errorMessage); @@ -768,7 +769,7 @@ internal static void LogWithThreadPoolStats(this ILogger? log, string message) // established" arrived under one id on netfx and another on modern TFMs, for no gain to anyone. [LoggerMessage( Level = LogLevel.Information, - EventId = 99, + EventId = 126, Message = "TLS connection established successfully using protocol: {SslProtocol}, cipher suite: {CipherSuite}")] internal static partial void LogInformationTLSConnectionEstablished(this ILogger logger, System.Security.Authentication.SslProtocols sslProtocol, System.Net.Security.TlsCipherSuite cipherSuite); #endif @@ -781,13 +782,13 @@ internal static void LogWithThreadPoolStats(this ILogger? log, string message) [LoggerMessage( Level = LogLevel.Information, - EventId = 116, + EventId = 124, Message = "{Server}: Requesting maintenance notifications ({Mode})")] internal static partial void LogInformationRequestingMaintenanceNotifications(this ILogger logger, ServerEndPointLogValue server, MaintenanceNotificationMode mode); [LoggerMessage( Level = LogLevel.Information, - EventId = 117, + EventId = 125, Message = "{Server}: Maintenance notifications accepted")] internal static partial void LogInformationMaintenanceNotificationsAccepted(this ILogger logger, ServerEndPointLogValue server); diff --git a/tests/StackExchange.Redis.Tests/LoggerEventIdTests.cs b/tests/StackExchange.Redis.Tests/LoggerEventIdTests.cs new file mode 100644 index 000000000..4c014a735 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/LoggerEventIdTests.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace StackExchange.Redis.Tests; + +/// +/// Guards the source-generated events in LoggerExtensions: +/// every event must carry an explicit EventId, and no two events may share one. Event ids are +/// what consumers filter and alert on, so a clash silently merges unrelated events downstream; this +/// has happened when parallel branches both took "the next free number". +/// +public class LoggerEventIdTests(ITestOutputHelper output) : TestBase(output) +{ + [Fact] + public void EventIdsAreExplicitAndUnique() + { + var events = ( + from method in typeof(LoggerExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + let attrib = method.GetCustomAttribute() + where attrib is not null + orderby attrib.EventId, method.Name + select (Method: method, Attrib: attrib)).ToList(); + + Assert.NotEmpty(events); // if reflection stops finding them, the test is vacuous + + var problems = new List(); + + // LoggerMessageAttribute.EventId defaults to -1 when not specified; the generator then emits 0, + // which is indistinguishable from "unknown" to a consumer + foreach (var (method, attrib) in events.Where(e => e.Attrib.EventId < 0)) + { + problems.Add($"{Describe(method)} has no explicit EventId"); + } + + foreach (var group in events.Where(e => e.Attrib.EventId >= 0).GroupBy(e => e.Attrib.EventId).Where(g => g.Count() > 1)) + { + problems.Add($"EventId {group.Key} is used {group.Count()} times: {string.Join(", ", group.Select(e => Describe(e.Method)))}"); + } + + int max = events.Max(e => e.Attrib.EventId); + Log($"{events.Count} events; highest EventId {max}; next free EventId {max + 1}"); + foreach (var problem in problems) + { + Log(problem); + } + + Assert.True(problems.Count == 0, string.Join(Environment.NewLine, problems)); + } + + private static string Describe(MethodInfo method) + { + // skip the "this ILogger" parameter; show the rest so overloads are distinguishable + var args = method.GetParameters().Skip(1).Select(p => p.ParameterType.Name); + return $"{method.Name}({string.Join(", ", args)})"; + } +}