From d12182443b2f9df00f89c8810d01539a17f50d24 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:23:03 +0100 Subject: [PATCH 1/5] Use logger source generator, cleanup log messages --- .../src/AutoConfiguration/BootstrapScanner.cs | 112 ++++++++++--- src/Common/src/Common/Net/InetUtils.cs | 48 ++++-- .../CompositeConfigurationProvider.cs | 11 +- .../EurekaCloudFoundryPostProcessor.cs | 8 +- .../IdentityCloudFoundryPostProcessor.cs | 8 +- .../ConfigServerConfigurationProvider.cs | 148 +++++++++++++---- .../ConfigServerDiscoveryService.cs | 22 ++- .../ConfigServerHealthContributor.cs | 32 +++- .../DecryptionConfigurationSource.cs | 2 +- .../PlaceholderConfigurationSource.cs | 2 +- .../Placeholder/PropertyPlaceHolderHelper.cs | 7 +- .../src/RandomValue/RandomValueProvider.cs | 7 +- .../TestConfigurationSource.cs | 2 +- .../CosmosDb/CosmosDbHealthContributor.cs | 17 +- .../MongoDb/MongoDbHealthContributor.cs | 17 +- .../RabbitMQ/RabbitMQHealthContributor.cs | 17 +- .../Redis/RedisHealthContributor.cs | 17 +- .../RelationalDatabaseHealthContributor.cs | 17 +- .../MigrateDbContextTask.cs | 19 ++- src/Discovery/src/Consul/PeriodicHeartbeat.cs | 31 +++- .../Consul/Registry/ConsulServiceRegistrar.cs | 37 ++++- .../Consul/Registry/ConsulServiceRegistry.cs | 28 +++- src/Discovery/src/Consul/TtlScheduler.cs | 13 +- .../Configuration/EurekaInstanceOptions.cs | 34 +++- .../Eureka/EurekaApplicationInfoManager.cs | 37 ++++- src/Discovery/src/Eureka/EurekaClient.cs | 55 +++++-- .../src/Eureka/EurekaDiscoveryClient.cs | 155 ++++++++++++++---- .../Eureka/EurekaServiceUriStateManager.cs | 7 +- .../LoadBalancers/RandomLoadBalancer.cs | 17 +- .../LoadBalancers/RoundRobinLoadBalancer.cs | 17 +- .../LoadBalancers/ServiceInstancesResolver.cs | 17 +- .../Eureka.Test/Transport/EurekaClientTest.cs | 53 ++++-- src/Management/src/Endpoint/ActuatorMapper.cs | 19 ++- .../CloudFoundrySecurityMiddleware.cs | 19 ++- .../CloudFoundry/PermissionsProvider.cs | 23 ++- .../DbMigrationsEndpointHandler.cs | 12 +- .../Environment/EnvironmentEndpointHandler.cs | 7 +- .../Availability/ApplicationAvailability.cs | 7 +- .../AvailabilityStateHealthContributor.cs | 18 +- .../Actuators/Health/HealthEndpointHandler.cs | 16 +- .../Health/HealthEndpointMiddleware.cs | 12 +- .../HeapDump/HeapDumpEndpointHandler.cs | 7 +- .../HeapDump/HeapDumpEndpointMiddleware.cs | 7 +- .../Endpoint/Actuators/HeapDump/HeapDumper.cs | 17 +- .../Diagnostics/DiagnosticObserver.cs | 17 +- .../Diagnostics/DiagnosticsManager.cs | 7 +- .../Diagnostics/DiagnosticsService.cs | 12 +- .../HttpExchangesEndpointHandler.cs | 7 +- .../HttpExchanges/HttpExchangesRepository.cs | 7 +- .../Actuators/Hypermedia/HypermediaService.cs | 12 +- .../Info/Contributors/GitInfoContributor.cs | 7 +- .../Actuators/Info/InfoEndpointHandler.cs | 8 +- .../Loggers/LoggersEndpointHandler.cs | 12 +- .../Loggers/LoggersEndpointMiddleware.cs | 17 +- .../Refresh/RefreshEndpointHandler.cs | 7 +- .../RouteMappings/AspNetEndpointProvider.cs | 7 +- .../ThreadDump/EventPipeThreadDumper.cs | 36 ++-- .../ThreadDump/ThreadDumpEndpointHandler.cs | 7 +- .../ManagementPortMiddleware.cs | 26 +-- .../Endpoint/Middleware/EndpointMiddleware.cs | 28 +++- .../SpringBootAdminPeriodicRefresh.cs | 40 +++-- .../SpringBootAdminRefreshRunner.cs | 32 +++- .../src/Prometheus/PrometheusExtensions.cs | 27 ++- .../src/Tasks/TaskHostExtensions.cs | 7 +- ...dFoundrySecurityMiddlewareTestScenarios.cs | 75 +++++---- .../CloudFoundry/PermissionsProviderTest.cs | 2 +- .../CertificateAuthorizationHandler.cs | 15 +- .../CertificateHttpClientBuilderExtensions.cs | 14 +- ...nfigureCertificateAuthenticationOptions.cs | 8 +- 69 files changed, 1193 insertions(+), 422 deletions(-) diff --git a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs index 2c1b85a862..a3c471e64d 100644 --- a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs +++ b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs @@ -38,7 +38,7 @@ namespace Steeltoe.Bootstrap.AutoConfiguration; -internal sealed class BootstrapScanner +internal sealed partial class BootstrapScanner { private readonly HostBuilderWrapper _wrapper; private readonly AssemblyLoader _loader; @@ -93,21 +93,21 @@ private void WireConfigServer() { _wrapper.AddConfigServer(_loggerFactory); - _logger.LogInformation("Configured Config Server configuration provider"); + LogConfigServerConfigured(); } private void WireCloudFoundryConfiguration() { _wrapper.AddCloudFoundryConfiguration(_loggerFactory); - _logger.LogInformation("Configured Cloud Foundry configuration provider"); + LogCloudFoundryConfigured(); } private void WireRandomValueProvider() { _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddRandomValueSource(_loggerFactory)); - _logger.LogInformation("Configured random value configuration provider"); + LogRandomValueConfigured(); } private void WireSpringBootProvider() @@ -117,21 +117,21 @@ private void WireSpringBootProvider() string[] args = Environment.GetCommandLineArgs().Skip(1).ToArray(); _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddSpringBootFromCommandLine(args, _loggerFactory)); - _logger.LogInformation("Configured Spring Boot configuration provider"); + LogSpringBootConfigured(); } private void WireDecryptionProvider() { _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddDecryption(_loggerFactory)); - _logger.LogInformation("Configured decryption configuration provider"); + LogDecryptionConfigured(); } private void WirePlaceholderResolver() { _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddPlaceholderResolver(_loggerFactory)); - _logger.LogInformation("Configured placeholder configuration provider"); + LogPlaceholderConfigured(); } private void WireConnectors() @@ -150,7 +150,7 @@ private void WireCosmosDbConnector() _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureCosmosDb()); _wrapper.ConfigureServices((host, services) => services.AddCosmosDb(host.Configuration)); - _logger.LogInformation("Configured CosmosDB connector"); + LogCosmosDbConfigured(); } private void WireMongoDbConnector() @@ -158,7 +158,7 @@ private void WireMongoDbConnector() _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureMongoDb()); _wrapper.ConfigureServices((host, services) => services.AddMongoDb(host.Configuration)); - _logger.LogInformation("Configured MongoDB connector"); + LogMongoDbConfigured(); } private void WireMySqlConnector() @@ -166,7 +166,7 @@ private void WireMySqlConnector() _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureMySql()); _wrapper.ConfigureServices((host, services) => services.AddMySql(host.Configuration)); - _logger.LogInformation("Configured MySQL connector"); + LogMySqlConfigured(); } private void WirePostgreSqlConnector() @@ -174,7 +174,7 @@ private void WirePostgreSqlConnector() _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigurePostgreSql()); _wrapper.ConfigureServices((host, services) => services.AddPostgreSql(host.Configuration)); - _logger.LogInformation("Configured PostgreSQL connector"); + LogPostgreSqlConfigured(); } private void WireRabbitMQConnector() @@ -182,7 +182,7 @@ private void WireRabbitMQConnector() _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureRabbitMQ()); _wrapper.ConfigureServices((host, services) => services.AddRabbitMQ(host.Configuration)); - _logger.LogInformation("Configured RabbitMQ connector"); + LogRabbitMQConfigured(); } private void WireRedisConnector() @@ -190,12 +190,12 @@ private void WireRedisConnector() _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureRedis()); _wrapper.ConfigureServices((host, services) => services.AddRedis(host.Configuration)); - _logger.LogInformation("Configured StackExchange Redis connector"); + LogRedisConfigured(); // Intentionally ignoring excluded assemblies here. if (MicrosoftRedisPackageResolver.Default.IsAvailable()) { - _logger.LogInformation("Configured Redis distributed cache connector"); + LogRedisDistributedCacheConfigured(); } } @@ -204,63 +204,63 @@ private void WireSqlServerConnector() _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureSqlServer()); _wrapper.ConfigureServices((host, services) => services.AddSqlServer(host.Configuration)); - _logger.LogInformation("Configured SQL Server connector"); + LogSqlServerConfigured(); } private void WireDynamicSerilog() { _wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddDynamicSerilog()); - _logger.LogInformation("Configured dynamic console logger for Serilog"); + LogDynamicSerilogConfigured(); } private void WireDynamicConsole() { _wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddDynamicConsole()); - _logger.LogInformation("Configured dynamic console logger"); + LogDynamicConsoleConfigured(); } private void WireDiscoveryConfiguration() { _wrapper.ConfigureServices(services => services.AddConfigurationDiscoveryClient()); - _logger.LogInformation("Configured configuration discovery client"); + LogConfigurationDiscoveryConfigured(); } private void WireDiscoveryConsul() { _wrapper.ConfigureServices(services => services.AddConsulDiscoveryClient()); - _logger.LogInformation("Configured Consul discovery client"); + LogConsulDiscoveryConfigured(); } private void WireDiscoveryEureka() { _wrapper.ConfigureServices(services => services.AddEurekaDiscoveryClient()); - _logger.LogInformation("Configured Eureka discovery client"); + LogEurekaDiscoveryConfigured(); } private void WireAllActuators() { _wrapper.ConfigureServices(services => services.AddAllActuators()); - _logger.LogInformation("Configured actuators"); + LogActuatorsConfigured(); } private void WirePrometheus() { _wrapper.ConfigureServices(services => services.AddPrometheusActuator()); - _logger.LogInformation("Configured Prometheus"); + LogPrometheusConfigured(); } private void WireDistributedTracingLogProcessor() { _wrapper.ConfigureServices(services => services.AddTracingLogProcessor()); - _logger.LogInformation("Configured distributed tracing log processor"); + LogDistributedTracingConfigured(); } private bool WireIfLoaded(Action wireAction, string assemblyName) @@ -281,4 +281,70 @@ private void WireIfAnyLoaded(Action wireAction, params PackageResolver[] package wireAction(); } } + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured Config Server configuration provider.")] + private partial void LogConfigServerConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured Cloud Foundry configuration provider.")] + private partial void LogCloudFoundryConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured random value configuration provider.")] + private partial void LogRandomValueConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured Spring Boot configuration provider.")] + private partial void LogSpringBootConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured decryption configuration provider.")] + private partial void LogDecryptionConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured placeholder configuration provider.")] + private partial void LogPlaceholderConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured CosmosDB connector.")] + private partial void LogCosmosDbConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured MongoDB connector.")] + private partial void LogMongoDbConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured MySQL connector.")] + private partial void LogMySqlConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured PostgreSQL connector.")] + private partial void LogPostgreSqlConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured RabbitMQ connector.")] + private partial void LogRabbitMQConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured StackExchange Redis connector.")] + private partial void LogRedisConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured Redis distributed cache connector.")] + private partial void LogRedisDistributedCacheConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured SQL Server connector.")] + private partial void LogSqlServerConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured dynamic console logger for Serilog.")] + private partial void LogDynamicSerilogConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured dynamic console logger.")] + private partial void LogDynamicConsoleConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured configuration discovery client.")] + private partial void LogConfigurationDiscoveryConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured Consul discovery client.")] + private partial void LogConsulDiscoveryConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured Eureka discovery client.")] + private partial void LogEurekaDiscoveryConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured actuators.")] + private partial void LogActuatorsConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured Prometheus.")] + private partial void LogPrometheusConfigured(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Configured distributed tracing log processor.")] + private partial void LogDistributedTracingConfigured(); } diff --git a/src/Common/src/Common/Net/InetUtils.cs b/src/Common/src/Common/Net/InetUtils.cs index e821256786..939aa19dd2 100644 --- a/src/Common/src/Common/Net/InetUtils.cs +++ b/src/Common/src/Common/Net/InetUtils.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Common.Net; // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global // Non-sealed because this type is mocked by tests. -internal class InetUtils +internal partial class InetUtils { private readonly IDomainNameResolver _domainNameResolver; private readonly IOptionsMonitor _optionsMonitor; @@ -62,7 +62,7 @@ public virtual HostInfo FindFirstNonLoopbackHostInfo() { if (networkInterface is { OperationalStatus: OperationalStatus.Up, IsReceiveOnly: false }) { - _logger.LogTrace("Testing interface: {Name}, {Id}", networkInterface.Name, networkInterface.Id); + LogTestingInterface(networkInterface.Name, networkInterface.Id); IPInterfaceProperties properties = networkInterface.GetIPProperties(); IPv4InterfaceProperties iPv4Properties = properties.GetIPv4Properties(); @@ -84,7 +84,7 @@ public virtual HostInfo FindFirstNonLoopbackHostInfo() if (IsInet4Address(address) && !IsLoopbackAddress(address) && IsPreferredAddress(address, inetOptions)) { - _logger.LogTrace("Found non-loopback interface: {Name}", networkInterface.Name); + LogNonLoopbackInterfaceFound(networkInterface.Name); result = address; } } @@ -94,7 +94,7 @@ public virtual HostInfo FindFirstNonLoopbackHostInfo() } catch (Exception exception) { - _logger.LogError(exception, "Cannot get first non-loopback address"); + LogCannotGetNonLoopbackAddress(exception); } if (result != null) @@ -123,7 +123,7 @@ internal bool IsPreferredAddress(IPAddress address, InetOptions inetOptions) if (!siteLocalAddress) { - _logger.LogTrace("Ignoring address: {Address} [UseOnlySiteLocalInterfaces=true, this address is not]", address); + LogIgnoringNonSiteLocalAddress(address); } return siteLocalAddress; @@ -147,7 +147,7 @@ internal bool IsPreferredAddress(IPAddress address, InetOptions inetOptions) } } - _logger.LogTrace("Ignoring address: {Address}", address); + LogIgnoringAddress(address); return false; } @@ -164,7 +164,7 @@ internal bool IgnoreInterface(string interfaceName, InetOptions inetOptions) if (matcher.IsMatch(interfaceName)) { - _logger.LogTrace("Ignoring interface: {Name}", interfaceName); + LogIgnoringInterface(interfaceName); return true; } } @@ -186,7 +186,7 @@ internal HostInfo ConvertAddress(IPAddress address, InetOptions inetOptions) } catch (Exception exception) { - _logger.LogInformation(exception, "Cannot determine local hostname."); + LogCannotDetermineHostname(exception); hostname = "localhost"; } } @@ -220,7 +220,7 @@ internal HostInfo ConvertAddress(IPAddress address, InetOptions inetOptions) } catch (Exception exception) { - _logger.LogWarning(exception, "Unable to resolve host address."); + LogUnableToResolveHostAddress(exception); } return result; @@ -234,7 +234,7 @@ internal HostInfo ConvertAddress(IPAddress address, InetOptions inetOptions) } catch (Exception exception) { - _logger.LogWarning(exception, "Unable to resolve hostname."); + LogUnableToResolveHostname(exception); return null; } } @@ -252,4 +252,32 @@ private static bool IsSiteLocalAddress(IPAddress address) return text.StartsWith("10.", StringComparison.Ordinal) || text.StartsWith("172.16.", StringComparison.Ordinal) || text.StartsWith("192.168.", StringComparison.Ordinal); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Testing interface {Name} with ID {Id}.")] + private partial void LogTestingInterface(string name, string id); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Found non-loopback interface {Name}.")] + private partial void LogNonLoopbackInterfaceFound(string name); + + [LoggerMessage(Level = LogLevel.Error, Message = "Cannot get first non-loopback address.")] + private partial void LogCannotGetNonLoopbackAddress(Exception exception); + + [LoggerMessage(Level = LogLevel.Trace, + Message = "Ignoring address {Address} because UseOnlySiteLocalInterfaces is true and this address is not site-local.")] + private partial void LogIgnoringNonSiteLocalAddress(IPAddress address); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Ignoring address {Address}.")] + private partial void LogIgnoringAddress(IPAddress address); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Ignoring interface {Name}.")] + private partial void LogIgnoringInterface(string name); + + [LoggerMessage(Level = LogLevel.Information, Message = "Cannot determine local hostname.")] + private partial void LogCannotDetermineHostname(Exception exception); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Unable to resolve host address.")] + private partial void LogUnableToResolveHostAddress(Exception exception); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Unable to resolve hostname.")] + private partial void LogUnableToResolveHostname(Exception exception); } diff --git a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs index 0215563c03..f7296f00d4 100644 --- a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs +++ b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs @@ -58,7 +58,11 @@ public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? ArgumentNullException.ThrowIfNull(earlierKeysArray, nameof(earlierKeys)); #pragma warning restore S3236 // Caller information arguments should not be provided explicitly - LogGetChildKeys(GetType().Name, earlierKeysArray, parentPath); + if (_logger.IsEnabled(LogLevel.Trace)) + { + string earlierKeyNames = string.Join(", ", earlierKeysArray.Select(key => $"'{key}'")); + LogGetChildKeys(GetType().Name, earlierKeyNames, parentPath); + } IConfiguration? section = parentPath == null ? ConfigurationRoot : ConfigurationRoot?.GetSection(parentPath); @@ -125,8 +129,9 @@ protected virtual void Dispose(bool disposing) [LoggerMessage(Level = LogLevel.Trace, Message = "CreateConfigurationRoot from {Type} with {ProviderCount} providers.")] private partial void LogCreateConfigurationRoot(string type, int providerCount); - [LoggerMessage(Level = LogLevel.Trace, Message = "GetChildKeys from {Type} with earlierKeys [{EarlierKeys}] and parentPath '{ParentPath}'.")] - private partial void LogGetChildKeys(string type, string[] earlierKeys, string? parentPath); + [LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true, + Message = "GetChildKeys from {Type} with earlierKeys [{EarlierKeys}] and parentPath '{ParentPath}'.")] + private partial void LogGetChildKeys(string type, string earlierKeys, string? parentPath); [LoggerMessage(Level = LogLevel.Trace, Message = "TryGet from {Type} with key '{Key}'.")] private partial void LogTryGet(string type, string key); diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/EurekaCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/EurekaCloudFoundryPostProcessor.cs index ae87402a15..c7713086af 100644 --- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/EurekaCloudFoundryPostProcessor.cs +++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/EurekaCloudFoundryPostProcessor.cs @@ -6,7 +6,7 @@ namespace Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors; -internal sealed class EurekaCloudFoundryPostProcessor : CloudFoundryPostProcessor +internal sealed partial class EurekaCloudFoundryPostProcessor : CloudFoundryPostProcessor { internal const string BindingType = "eureka"; internal const string EurekaConfigurationKeyPrefix = "eureka:client"; @@ -27,7 +27,7 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider { if (hasMapped) { - _logger.LogWarning("Multiple Eureka service bindings found, which is not supported. Using the first binding from VCAP_SERVICES."); + LogMultipleEurekaBindings(); break; } @@ -52,4 +52,8 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider hasMapped = true; } } + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Multiple Eureka service bindings found, which is not supported. Using the first binding from VCAP_SERVICES.")] + private partial void LogMultipleEurekaBindings(); } diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/IdentityCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/IdentityCloudFoundryPostProcessor.cs index 93a96da8e7..068ff27384 100644 --- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/IdentityCloudFoundryPostProcessor.cs +++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/IdentityCloudFoundryPostProcessor.cs @@ -6,7 +6,7 @@ namespace Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors; -internal sealed class IdentityCloudFoundryPostProcessor : CloudFoundryPostProcessor +internal sealed partial class IdentityCloudFoundryPostProcessor : CloudFoundryPostProcessor { internal const string BindingType = "p-identity"; internal const string AuthenticationConfigurationKeyPrefix = "Authentication:Schemes"; @@ -34,7 +34,7 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider { if (hasMapped) { - _logger.LogWarning("Multiple identity service bindings found, which is not supported. Using the first binding from VCAP_SERVICES."); + LogMultipleIdentityBindings(); break; } @@ -50,4 +50,8 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider hasMapped = true; } } + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Multiple identity service bindings found, which is not supported. Using the first binding from VCAP_SERVICES.")] + private partial void LogMultipleIdentityBindings(); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 7963604378..36be91f368 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -25,7 +25,7 @@ namespace Steeltoe.Configuration.ConfigServer; /// /// A Spring Cloud Config Server based . /// -internal sealed class ConfigServerConfigurationProvider : ConfigurationProvider, IDisposable +internal sealed partial class ConfigServerConfigurationProvider : ConfigurationProvider, IDisposable { private const string VaultRenewPath = "vault/v1/auth/token/renew-self"; private const string VaultTokenHeader = "X-Vault-Token"; @@ -149,7 +149,7 @@ private void OnSettingsChanged() /// private async Task DoPolledLoadAsync() { - _logger.LogTrace("Entering timer cycle"); + LogEnteringTimerCycle(); bool lockTaken = false; try @@ -165,23 +165,23 @@ private async Task DoPolledLoadAsync() { if (lockTaken) { - _logger.LogTrace("Exclusive lock obtained"); + LogExclusiveLockObtained(); await DoLoadAsync(true, CancellationToken.None); } else { - _logger.LogTrace("Previous cycle is still running, or already disposed; skipping this cycle"); + LogSkippingCycle(); } } catch (Exception exception) { - _logger.LogWarning(exception, "Could not reload configuration during polling"); + LogCouldNotReloadDuringPolling(exception); } finally { if (lockTaken) { - _logger.LogTrace("Timer cycle completed, releasing exclusive lock"); + LogTimerCycleCompleted(); try { @@ -210,7 +210,7 @@ public override void Load() { if (!ClientOptions.Enabled) { - _logger.LogInformation("Config Server client disabled, did not fetch configuration!"); + LogConfigServerClientDisabled(); return null; } @@ -230,7 +230,7 @@ public override void Load() do { - _logger.LogDebug("Fetching configuration from server(s)."); + LogFetchingConfiguration(); try { @@ -238,7 +238,7 @@ public override void Load() } catch (ConfigServerException exception) { - _logger.LogWarning(exception, "Failed fetching configuration from server(s)."); + LogFailedFetchingConfiguration(exception); attempts++; if (attempts < ClientOptions.Retry.MaxAttempts) @@ -256,7 +256,7 @@ public override void Load() while (true); } - _logger.LogDebug("Fetching configuration from server(s)."); + LogFetchingConfiguration(); return await DoLoadAsync(updateDictionary, cancellationToken); } @@ -271,11 +271,11 @@ public override void Load() { foreach (string label in GetLabels()) { - _logger.LogTrace("Processing label '{Label}'", label); + LogProcessingLabel(label); if (uris.Count > 1) { - _logger.LogDebug("Multiple Config Server Uris listed."); + LogMultipleConfigServerUris(); } // Invoke Config Servers @@ -284,8 +284,7 @@ public override void Load() // Update configuration Data dictionary with any results if (env != null) { - _logger.LogDebug("Located environment name: {Name}, profiles: {Profiles}, labels: {Label}, version: {Version}, state: {State}", env.Name, - env.Profiles, env.Label, env.Version, env.State); + LogEnvironmentLocated(env.Name, string.Join(", ", env.Profiles.Select(p => $"'{p}'")), env.Label, env.Version, env.State); if (updateDictionary) { @@ -314,13 +313,13 @@ public override void Load() if (!AreDictionariesEqual(Data, data)) { - _logger.LogTrace("Data has changed, raising configuration reload"); + LogDataChanged(); Data = data; OnReload(); } else { - _logger.LogTrace("Data has not changed"); + LogDataNotChanged(); } } @@ -333,11 +332,11 @@ public override void Load() error = exception; } - _logger.LogWarning(error, "Could not locate PropertySource"); + LogCouldNotLocatePropertySource(error); if (ClientOptions.FailFast) { - _logger.LogTrace(error, "Failure with FailFast enabled, throwing ConfigServerException"); + LogFailFastEnabled(error); throw new ConfigServerException("Could not locate PropertySource, fail fast property is set, failing", error); } @@ -454,7 +453,7 @@ internal async Task GetRequestMessageAsync(Uri requestUri, C if (requestUri.TryGetUsernamePassword(out string? username, out string? password) && password.Length > 0) { - _logger.LogDebug("Adding credentials from '{RequestUri}' to Authorization header.", requestUri.ToMaskedString()); + LogAddingCredentials(requestUri.ToMaskedString()); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"))); @@ -469,7 +468,7 @@ internal async Task GetRequestMessageAsync(Uri requestUri, C string accessToken = await httpClient.GetAccessTokenAsync(accessTokenUri, ClientOptions.ClientId, ClientOptions.ClientSecret, cancellationToken); - _logger.LogDebug("Fetched access token from '{AccessTokenUri}'.", accessTokenUri.ToMaskedString()); + LogAccessTokenFetched(accessTokenUri.ToMaskedString()); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } } @@ -543,7 +542,7 @@ private void AddConfigServerClientOptions(Dictionary data) internal async Task RemoteLoadAsync(List requestUris, string? label, CancellationToken cancellationToken) { - _logger.LogTrace("Entered {Method}", nameof(RemoteLoadAsync)); + LogRemoteLoadEntered(nameof(RemoteLoadAsync)); // Get client if not already set using HttpClient httpClient = CreateHttpClient(ClientOptions); @@ -555,19 +554,19 @@ private void AddConfigServerClientOptions(Dictionary data) // Make Config Server URI from settings Uri uri = BuildConfigServerUri(requestUri, label); - _logger.LogDebug("Trying to connect to Config Server at {RequestUri}", uri.ToMaskedString()); + LogTryingToConnect(uri.ToMaskedString()); // Get the request message - _logger.LogTrace("Building HTTP request message"); + LogBuildingHttpRequest(); HttpRequestMessage request = await GetRequestMessageAsync(uri, cancellationToken); // Invoke Config Server try { - _logger.LogTrace("Sending HTTP request"); + LogSendingHttpRequest(); using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); - _logger.LogDebug("Config Server returned status: {StatusCode} invoking path: {RequestUri}", response.StatusCode, uri.ToMaskedString()); + LogConfigServerReturnedStatus(response.StatusCode, uri.ToMaskedString()); if (response.StatusCode != HttpStatusCode.OK) { @@ -585,7 +584,7 @@ private void AddConfigServerClientOptions(Dictionary data) return null; } - _logger.LogTrace("Parsing JSON response"); + LogParsingJsonResponse(); return await response.Content.ReadFromJsonAsync(SerializerOptions, cancellationToken); } catch (Exception exception) when (!exception.IsCancellation()) @@ -594,7 +593,7 @@ private void AddConfigServerClientOptions(Dictionary data) if (IsSocketError(exception)) { - _logger.LogTrace(exception, "Socket error detected"); + LogSocketError(exception); continue; } @@ -689,7 +688,7 @@ private void AddPropertySource(PropertySource? source, Dictionary GetVaultRenewRequestMessageAsync(Uri requ string accessToken = await httpClient.GetAccessTokenAsync(accessTokenUri, ClientOptions.ClientId, ClientOptions.ClientSecret, cancellationToken); - _logger.LogDebug("Fetched access token from '{AccessTokenUri}'.", accessTokenUri.ToMaskedString()); + LogAccessTokenFetched(accessTokenUri.ToMaskedString()); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } @@ -838,4 +837,89 @@ public void Dispose() _httpClientHandler = null; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Entering timer cycle.")] + private partial void LogEnteringTimerCycle(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Exclusive lock obtained.")] + private partial void LogExclusiveLockObtained(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Previous cycle is still running, or already disposed; skipping this cycle.")] + private partial void LogSkippingCycle(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Could not reload configuration during polling.")] + private partial void LogCouldNotReloadDuringPolling(Exception exception); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Timer cycle completed, releasing exclusive lock.")] + private partial void LogTimerCycleCompleted(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Config Server client disabled, not fetching configuration.")] + private partial void LogConfigServerClientDisabled(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Fetching configuration from server(s).")] + private partial void LogFetchingConfiguration(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed fetching configuration from server(s).")] + private partial void LogFailedFetchingConfiguration(Exception exception); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Processing label '{Label}'.")] + private partial void LogProcessingLabel(string? label); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Multiple Config Server Uris listed.")] + private partial void LogMultipleConfigServerUris(); + + [LoggerMessage(Level = LogLevel.Debug, + Message = "Located environment with name {Name}, profiles {Profiles}, label {Label}, version {Version} and state {State}.")] + private partial void LogEnvironmentLocated(string? name, string profiles, string? label, string? version, string? state); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Data has changed, raising configuration reload.")] + private partial void LogDataChanged(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Data has not changed.")] + private partial void LogDataNotChanged(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Could not locate property source.")] + private partial void LogCouldNotLocatePropertySource(Exception? error); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Failure with FailFast enabled, throwing ConfigServerException.")] + private partial void LogFailFastEnabled(Exception? error); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Adding credentials from '{RequestUri}' to Authorization header.")] + private partial void LogAddingCredentials(string requestUri); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Fetched access token from {AccessTokenUri}.")] + private partial void LogAccessTokenFetched(string accessTokenUri); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Entered {Method}.")] + private partial void LogRemoteLoadEntered(string method); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Trying to connect to Config Server at {RequestUri}.")] + private partial void LogTryingToConnect(string requestUri); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Building HTTP request message.")] + private partial void LogBuildingHttpRequest(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Sending HTTP request.")] + private partial void LogSendingHttpRequest(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Config Server returned status {StatusCode} for path {RequestUri}.")] + private partial void LogConfigServerReturnedStatus(HttpStatusCode statusCode, string requestUri); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Parsing JSON response.")] + private partial void LogParsingJsonResponse(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Socket error detected.")] + private partial void LogSocketError(Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Config Server exception for property {Key} of type {Type}.")] + private partial void LogConfigServerPropertyException(Exception exception, string key, Type type); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Renewing Vault token {Token} for {Ttl} milliseconds at Uri {Uri}.")] + private partial void LogRenewingVaultToken(string token, int ttl, string uri); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Renewing Vault token {Token} returned status {Status}.")] + private partial void LogVaultTokenRenewalStatus(string token, HttpStatusCode status); + + [LoggerMessage(Level = LogLevel.Error, Message = "Unable to renew Vault token {Token}. The token is likely invalid or has expired.")] + private partial void LogUnableToRenewVaultToken(Exception exception, string token); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs index 5bc32ebaf1..a6b86ee428 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs @@ -15,7 +15,7 @@ namespace Steeltoe.Configuration.ConfigServer; -internal sealed class ConfigServerDiscoveryService +internal sealed partial class ConfigServerDiscoveryService { private static readonly AssemblyLoader AssemblyLoader = new(); private readonly IConfiguration _configuration; @@ -99,7 +99,7 @@ private IDiscoveryClient[] GetDiscoveryClientsFromServiceCollection(ServiceColle foreach (IDiscoveryClient discoveryClient in discoveryClients) { - _logger.LogDebug("Found discovery client of type {DiscoveryClientType}", discoveryClient.GetType()); + LogDiscoveryClientFound(discoveryClient.GetType()); } return discoveryClients; @@ -113,7 +113,7 @@ internal async Task> GetConfigServerInstancesAsync do { - _logger.LogDebug("Locating ConfigServer {ServiceId} via discovery", _options.Discovery.ServiceId); + LogLocatingConfigServer(_options.Discovery.ServiceId); if (_options.Discovery.ServiceId != null) { @@ -126,7 +126,7 @@ internal async Task> GetConfigServerInstancesAsync } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogError(exception, "Failed to get instances during ConfigServer lookup from {DiscoveryClient}.", discoveryClient.GetType()); + LogFailedToGetInstances(exception, discoveryClient.GetType()); } } } @@ -158,7 +158,7 @@ internal async Task ProvideRuntimeReplacementsAsync(ICollection _logger; @@ -30,7 +30,7 @@ public ConfigServerHealthContributor(IConfiguration configuration, TimeProvider if (Provider == null) { - _logger.LogWarning("Unable to find ConfigServerConfigurationProvider, health check disabled"); + LogHealthCheckDisabled(); } } @@ -40,7 +40,7 @@ public ConfigServerHealthContributor(IConfiguration configuration, TimeProvider if (Provider == null) { - _logger.LogDebug("No Config Server provider found"); + LogNoProviderFound(); health.Status = HealthStatus.Unknown; health.Details.Add("error", "No Config Server provider found"); return health; @@ -55,7 +55,7 @@ public ConfigServerHealthContributor(IConfiguration configuration, TimeProvider if (sources == null || sources.Count == 0) { - _logger.LogDebug("No property sources found"); + LogNoPropertySourcesFound(); health.Status = HealthStatus.Unknown; health.Details.Add("error", "No property sources found"); return health; @@ -67,14 +67,14 @@ public ConfigServerHealthContributor(IConfiguration configuration, TimeProvider internal void UpdateHealth(HealthCheckResult health, IList sources) { - _logger.LogDebug("Config Server health check returning UP"); + LogHealthCheckReturningUp(); health.Status = HealthStatus.Up; List names = []; foreach (PropertySource source in sources) { - _logger.LogDebug("Returning property source: {PropertySource}", source.Name); + LogReturningPropertySource(source.Name); names.Add(source.Name); } @@ -88,7 +88,7 @@ internal void UpdateHealth(HealthCheckResult health, IList sourc if (IsCacheStale(currentTime)) { LastAccess = currentTime; - _logger.LogDebug("Cache stale, fetching config server health"); + LogCacheStale(); Cached = await provider.LoadInternalAsync(false, cancellationToken); } @@ -114,4 +114,22 @@ internal long GetTimeToLive() { return Provider != null ? Provider.ClientOptions.Health.TimeToLive : long.MaxValue; } + + [LoggerMessage(Level = LogLevel.Warning, Message = "No Config Server provider found, health check disabled.")] + private partial void LogHealthCheckDisabled(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "No Config Server provider found.")] + private partial void LogNoProviderFound(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "No property sources found.")] + private partial void LogNoPropertySourcesFound(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Config Server health check returning UP.")] + private partial void LogHealthCheckReturningUp(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Returning property source {PropertySource}.")] + private partial void LogReturningPropertySource(string? propertySource); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Cache stale, fetching config server health.")] + private partial void LogCacheStale(); } diff --git a/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs b/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs index a55cbc8aee..d703d9e247 100644 --- a/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs +++ b/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs @@ -35,5 +35,5 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) } [LoggerMessage(Level = LogLevel.Trace, Message = "Build for {SourceCount} sources and {PropertyCount} properties.")] - private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount); + private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount); } diff --git a/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs b/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs index 0b6a35b2f9..29ecbb202a 100644 --- a/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs +++ b/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs @@ -32,5 +32,5 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) } [LoggerMessage(Level = LogLevel.Trace, Message = "Build for {SourceCount} sources and {PropertyCount} properties.")] - private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount); + private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount); } diff --git a/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs b/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs index 4cca8c32f1..9753c42830 100644 --- a/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs +++ b/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs @@ -22,7 +22,7 @@ namespace Steeltoe.Configuration.Placeholder; /// . /// /// -internal sealed class PropertyPlaceholderHelper +internal sealed partial class PropertyPlaceholderHelper { private const string Prefix = "${"; private const string Suffix = "}"; @@ -110,7 +110,7 @@ public PropertyPlaceholderHelper(ILogger logger) propertyValue = ParseStringValue(propertyValue, configuration, visitedPlaceholders); Replace(result, startIndex, endIndex + Suffix.Length, propertyValue); - _logger.LogDebug("Resolved placeholder '{Placeholder}' to '{Value}'", innerPlaceholder, propertyValue); + LogPlaceholderResolved(innerPlaceholder, propertyValue); startIndex = IndexOf(result, Prefix, startIndex + propertyValue.Length); } else @@ -202,6 +202,9 @@ private static string Substring(StringBuilder builder, int start, int end) return builder.ToString()[start..end]; } + [LoggerMessage(Level = LogLevel.Debug, Message = "Resolved placeholder '{Placeholder}' to '{Value}'.")] + private partial void LogPlaceholderResolved(string placeholder, string value); + private readonly struct PlaceholderExpression(string key, string? defaultValue) : IEquatable { public string Key { get; } = key; diff --git a/src/Configuration/src/RandomValue/RandomValueProvider.cs b/src/Configuration/src/RandomValue/RandomValueProvider.cs index 8eed55e1c4..dbbebe0195 100644 --- a/src/Configuration/src/RandomValue/RandomValueProvider.cs +++ b/src/Configuration/src/RandomValue/RandomValueProvider.cs @@ -12,7 +12,7 @@ namespace Steeltoe.Configuration.RandomValue; /// /// Configuration provider that provides random values. Note: This code was inspired by the Spring Boot equivalent class. /// -internal sealed class RandomValueProvider : ConfigurationProvider +internal sealed partial class RandomValueProvider : ConfigurationProvider { private readonly ILogger _logger; private readonly string _prefix; @@ -60,7 +60,7 @@ public override bool TryGet(string key, out string? value) } value = GetRandomValue(key[_prefix.Length..]); - _logger.LogDebug("Generated random value {Value} for '{Key}'", value, key); + LogRandomValueGenerated(value, key); return true; } @@ -199,4 +199,7 @@ private string GetRandomBytes() Random.Shared.NextBytes(bytes); return Convert.ToHexString(bytes); } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Generated random value '{Value}' for '{Key}'.")] + private partial void LogRandomValueGenerated(string? value, string key); } diff --git a/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs b/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs index ca6ce53fb3..572dccca30 100644 --- a/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs +++ b/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs @@ -25,5 +25,5 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) } [LoggerMessage(Level = LogLevel.Trace, Message = "Build ({Name}).")] - private static partial void LogBuild(ILogger logger, string name); + private static partial void LogBuild(ILogger logger, string name); } diff --git a/src/Connectors/src/Connectors/CosmosDb/CosmosDbHealthContributor.cs b/src/Connectors/src/Connectors/CosmosDb/CosmosDbHealthContributor.cs index e1b1993a33..ffdcf04cab 100644 --- a/src/Connectors/src/Connectors/CosmosDb/CosmosDbHealthContributor.cs +++ b/src/Connectors/src/Connectors/CosmosDb/CosmosDbHealthContributor.cs @@ -12,7 +12,7 @@ namespace Steeltoe.Connectors.CosmosDb; -internal sealed class CosmosDbHealthContributor : IHealthContributor, IDisposable +internal sealed partial class CosmosDbHealthContributor : IHealthContributor, IDisposable { private readonly CosmosClientShimFactory _clientFactory; private readonly ILogger _logger; @@ -40,7 +40,7 @@ public CosmosDbHealthContributor(string serviceName, IServiceProvider servicePro public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -64,7 +64,7 @@ public CosmosDbHealthContributor(string serviceName, IServiceProvider servicePro result.Status = HealthStatus.Up; - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -75,7 +75,7 @@ public CosmosDbHealthContributor(string serviceName, IServiceProvider servicePro ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -91,6 +91,15 @@ public void Dispose() _cosmosClientShim = null; } + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); + private sealed class CosmosClientShimFactory { private readonly ConnectorShim _connectorShim; diff --git a/src/Connectors/src/Connectors/MongoDb/MongoDbHealthContributor.cs b/src/Connectors/src/Connectors/MongoDb/MongoDbHealthContributor.cs index 311e6d0457..ed0b0f9f0a 100644 --- a/src/Connectors/src/Connectors/MongoDb/MongoDbHealthContributor.cs +++ b/src/Connectors/src/Connectors/MongoDb/MongoDbHealthContributor.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Connectors.MongoDb; -internal sealed class MongoDbHealthContributor : IHealthContributor +internal sealed partial class MongoDbHealthContributor : IHealthContributor { private readonly MongoClientInterfaceShimFactory _clientFactory; private readonly ILogger _logger; @@ -36,7 +36,7 @@ public MongoDbHealthContributor(string serviceName, IServiceProvider serviceProv public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -59,7 +59,7 @@ public MongoDbHealthContributor(string serviceName, IServiceProvider serviceProv result.Status = HealthStatus.Up; - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -70,7 +70,7 @@ public MongoDbHealthContributor(string serviceName, IServiceProvider serviceProv ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -80,6 +80,15 @@ public MongoDbHealthContributor(string serviceName, IServiceProvider serviceProv return result; } + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); + private sealed class MongoClientInterfaceShimFactory { private readonly ConnectorShim _connectorShim; diff --git a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHealthContributor.cs b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHealthContributor.cs index b534510529..a324a4ca11 100644 --- a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHealthContributor.cs +++ b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHealthContributor.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Connectors.RabbitMQ; -internal sealed class RabbitMQHealthContributor : IHealthContributor, IDisposable +internal sealed partial class RabbitMQHealthContributor : IHealthContributor, IDisposable { private readonly ILogger _logger; private readonly ConnectionFactoryInterfaceShim _connectionFactoryInterfaceShim; @@ -34,7 +34,7 @@ public RabbitMQHealthContributor(object connectionFactory, string host, ILogger< public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -68,7 +68,7 @@ public RabbitMQHealthContributor(object connectionFactory, string host, ILogger< result.Status = HealthStatus.Up; - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -79,7 +79,7 @@ public RabbitMQHealthContributor(object connectionFactory, string host, ILogger< ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -94,4 +94,13 @@ public void Dispose() _connectionInterfaceShim?.Dispose(); _connectionInterfaceShim = null; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); } diff --git a/src/Connectors/src/Connectors/Redis/RedisHealthContributor.cs b/src/Connectors/src/Connectors/Redis/RedisHealthContributor.cs index 9a2e0eb98c..efa9496da0 100644 --- a/src/Connectors/src/Connectors/Redis/RedisHealthContributor.cs +++ b/src/Connectors/src/Connectors/Redis/RedisHealthContributor.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Connectors.Redis; -internal sealed class RedisHealthContributor : IHealthContributor, IDisposable +internal sealed partial class RedisHealthContributor : IHealthContributor, IDisposable { private readonly ILogger _logger; private readonly string? _connectionString; @@ -54,7 +54,7 @@ internal void SetConnectionMultiplexer(object connectionMultiplexer) public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -80,7 +80,7 @@ internal void SetConnectionMultiplexer(object connectionMultiplexer) result.Status = HealthStatus.Up; result.Details.Add("ping", roundTripTime.TotalMilliseconds); - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -91,7 +91,7 @@ internal void SetConnectionMultiplexer(object connectionMultiplexer) ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -106,4 +106,13 @@ public void Dispose() _connectionMultiplexerInterfaceShim?.Dispose(); _connectionMultiplexerInterfaceShim = null; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); } diff --git a/src/Connectors/src/Connectors/RelationalDatabaseHealthContributor.cs b/src/Connectors/src/Connectors/RelationalDatabaseHealthContributor.cs index 04b1773a41..a2fcfd0dc7 100644 --- a/src/Connectors/src/Connectors/RelationalDatabaseHealthContributor.cs +++ b/src/Connectors/src/Connectors/RelationalDatabaseHealthContributor.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Connectors; -internal sealed class RelationalDatabaseHealthContributor : IHealthContributor, IDisposable +internal sealed partial class RelationalDatabaseHealthContributor : IHealthContributor, IDisposable { private readonly DbConnection _connection; private readonly ILogger _logger; @@ -32,7 +32,7 @@ public RelationalDatabaseHealthContributor(DbConnection connection, string? host public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -56,7 +56,7 @@ public RelationalDatabaseHealthContributor(DbConnection connection, string? host result.Status = HealthStatus.Up; - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -67,7 +67,7 @@ public RelationalDatabaseHealthContributor(DbConnection connection, string? host ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -96,4 +96,13 @@ public void Dispose() { _connection.Dispose(); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); } diff --git a/src/Connectors/src/EntityFrameworkCore/MigrateDbContextTask.cs b/src/Connectors/src/EntityFrameworkCore/MigrateDbContextTask.cs index c4b899b49e..c127054e38 100644 --- a/src/Connectors/src/EntityFrameworkCore/MigrateDbContextTask.cs +++ b/src/Connectors/src/EntityFrameworkCore/MigrateDbContextTask.cs @@ -18,7 +18,7 @@ namespace Steeltoe.Connectors.EntityFrameworkCore; /// /// The to run migrations from. /// -public sealed class MigrateDbContextTask : IApplicationTask +public sealed partial class MigrateDbContextTask : IApplicationTask where TDbContext : DbContext { public const string Name = "migrate"; @@ -50,7 +50,7 @@ public async Task RunAsync(CancellationToken cancellationToken) isNewDatabase = true; } - _logger.LogInformation("Starting database migration..."); + LogStartingMigration(); await _dbContext.Database.MigrateAsync(cancellationToken); if (isNewDatabase) @@ -60,12 +60,21 @@ public async Task RunAsync(CancellationToken cancellationToken) if (migrations.Length > 0) { - string migrationNames = string.Join(", ", migrations); - _logger.LogInformation("The following migrations have been successfully applied: {MigrationNames}.", migrationNames); + string migrationNames = string.Join(", ", migrations.Select(migration => $"'{migration}'")); + LogMigrationsApplied(migrationNames); } else { - _logger.LogInformation("Database is already up to date."); + LogAlreadyUpToDate(); } } + + [LoggerMessage(Level = LogLevel.Information, Message = "Starting database migration.")] + private partial void LogStartingMigration(); + + [LoggerMessage(Level = LogLevel.Information, Message = "The following migrations have been successfully applied: {MigrationNames}.")] + private partial void LogMigrationsApplied(string migrationNames); + + [LoggerMessage(Level = LogLevel.Information, Message = "Database is already up to date.")] + private partial void LogAlreadyUpToDate(); } diff --git a/src/Discovery/src/Consul/PeriodicHeartbeat.cs b/src/Discovery/src/Consul/PeriodicHeartbeat.cs index 66a6379932..95d4c3c398 100644 --- a/src/Discovery/src/Consul/PeriodicHeartbeat.cs +++ b/src/Discovery/src/Consul/PeriodicHeartbeat.cs @@ -5,10 +5,11 @@ using Consul; using Microsoft.Extensions.Logging; using Steeltoe.Common.Extensions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Steeltoe.Discovery.Consul; -internal sealed class PeriodicHeartbeat : IAsyncDisposable +internal sealed partial class PeriodicHeartbeat : IAsyncDisposable { private readonly string _serviceId; private readonly PeriodicTimer _periodicTimer; @@ -37,11 +38,11 @@ private async Task TimerLoopAsync() { try { - _logger.LogDebug("Start sending periodic Consul heartbeats for '{ServiceId}' with interval {Interval}.", _serviceId, Interval); + LogStartSendingHeartbeats(_serviceId, Interval); while (await _periodicTimer.WaitForNextTickAsync(_cancellationTokenSource.Token)) { - _logger.LogDebug("Sending Consul heartbeat for '{ServiceId}'.", _serviceId); + LogSendingHeartbeat(_serviceId); try { @@ -49,16 +50,13 @@ private async Task TimerLoopAsync() } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogError(exception, "Failed to send Consul heartbeat for '{ServiceId}'.", _serviceId); + LogFailedToSendHeartbeat(exception, _serviceId); } } } catch (OperationCanceledException) { -#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. - // Justification: The exception contains no useful information. Logging it suggests something crashed, while this is expected behavior. - _logger.LogDebug("Stop sending periodic Consul heartbeats for '{ServiceId}'.", _serviceId); -#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + LogSendingHeartbeatsStopped(_serviceId); } } @@ -68,7 +66,7 @@ public void ChangeInterval(TimeSpan interval) { _periodicTimer.Period = interval; Interval = interval; - _logger.LogDebug("Periodic Consul heartbeat interval for '{ServiceId}' changed to {Interval}.", _serviceId, interval); + LogHeartbeatIntervalChanged(_serviceId, interval); } } @@ -80,4 +78,19 @@ public async ValueTask DisposeAsync() _task.Dispose(); _periodicTimer.Dispose(); } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Start sending periodic Consul heartbeats for '{ServiceId}' with interval {Interval}.")] + private partial void LogStartSendingHeartbeats(string serviceId, TimeSpan interval); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending Consul heartbeat for '{ServiceId}'.")] + private partial void LogSendingHeartbeat(string serviceId); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to send Consul heartbeat for '{ServiceId}'.")] + private partial void LogFailedToSendHeartbeat(Exception exception, string serviceId); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Stopped sending periodic Consul heartbeats for '{ServiceId}'.")] + private partial void LogSendingHeartbeatsStopped(string serviceId); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Periodic Consul heartbeat interval for '{ServiceId}' changed to {Interval}.")] + private partial void LogHeartbeatIntervalChanged(string serviceId, TimeSpan interval); } diff --git a/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs b/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs index e7d264f9a2..ef0d9d392e 100644 --- a/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs +++ b/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Discovery.Consul.Registry; /// /// A registrar used to register a service in a Consul server. /// -internal sealed class ConsulServiceRegistrar : IAsyncDisposable +internal sealed partial class ConsulServiceRegistrar : IAsyncDisposable { private const int NotRunning = 0; private const int Running = 1; @@ -73,7 +73,7 @@ public async Task StartAsync(CancellationToken cancellationToken) { if (!Options.Enabled) { - _logger.LogDebug("Consul discovery client is turned off."); + LogDiscoveryClientTurnedOff(); return; } @@ -94,7 +94,7 @@ private async Task RegisterAsync(CancellationToken cancellationToken) { if (!Options.Register) { - _logger.LogDebug("Consul registration is turned off."); + LogRegistrationTurnedOff(); return; } @@ -105,7 +105,7 @@ private async Task DeregisterAsync(CancellationToken cancellationToken) { if (!Options.Register || !Options.Deregister) { - _logger.LogDebug("Consul deregistration is turned off."); + LogDeregistrationTurnedOff(); return; } @@ -116,7 +116,7 @@ private async Task DoWithRetryAsync(Func retryable, Con { ArgumentNullException.ThrowIfNull(retryable); - _logger.LogDebug("Starting retryable action."); + LogStartingRetryableAction(); int attempts = 0; int backOff = options.InitialInterval; @@ -126,7 +126,7 @@ private async Task DoWithRetryAsync(Func retryable, Con try { await retryable(cancellationToken); - _logger.LogDebug("Finished retryable action."); + LogRetryableActionFinished(); return; } catch (Exception exception) when (!exception.IsCancellation()) @@ -135,14 +135,14 @@ private async Task DoWithRetryAsync(Func retryable, Con if (attempts < options.MaxAttempts) { - _logger.LogError(exception, "Exception during {Attempt} attempts of retryable action, will retry", attempts); + LogStartingRetry(exception, attempts); Thread.CurrentThread.Join(backOff); int nextBackOff = (int)(backOff * options.Multiplier); backOff = Math.Min(nextBackOff, options.MaxInterval); } else { - _logger.LogError(exception, "Exception during {Attempt} attempts of retryable action, done with retry", attempts); + LogRetryFailed(exception, attempts); throw; } } @@ -164,4 +164,25 @@ public async ValueTask DisposeAsync() _isDisposed = true; } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Consul discovery client is turned off.")] + private partial void LogDiscoveryClientTurnedOff(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Consul registration is turned off.")] + private partial void LogRegistrationTurnedOff(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Consul deregistration is turned off.")] + private partial void LogDeregistrationTurnedOff(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Starting retryable action.")] + private partial void LogStartingRetryableAction(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Finished retryable action.")] + private partial void LogRetryableActionFinished(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Exception during {Attempt} attempts of retryable action, will retry.")] + private partial void LogStartingRetry(Exception exception, int attempt); + + [LoggerMessage(Level = LogLevel.Error, Message = "Exception during {Attempt} attempts of retryable action, done with retries.")] + private partial void LogRetryFailed(Exception exception, int attempt); } diff --git a/src/Discovery/src/Consul/Registry/ConsulServiceRegistry.cs b/src/Discovery/src/Consul/Registry/ConsulServiceRegistry.cs index 2809e500ee..a18b270df8 100644 --- a/src/Discovery/src/Consul/Registry/ConsulServiceRegistry.cs +++ b/src/Discovery/src/Consul/Registry/ConsulServiceRegistry.cs @@ -8,13 +8,14 @@ using Microsoft.Extensions.Options; using Steeltoe.Common.Extensions; using Steeltoe.Discovery.Consul.Configuration; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Steeltoe.Discovery.Consul.Registry; /// /// A service registry that uses Consul. /// -internal sealed class ConsulServiceRegistry : IAsyncDisposable +internal sealed partial class ConsulServiceRegistry : IAsyncDisposable { private const string Up = "UP"; private const string OutOfService = "OUT_OF_SERVICE"; @@ -67,7 +68,7 @@ public async Task RegisterAsync(ConsulRegistration registration, CancellationTok { ArgumentNullException.ThrowIfNull(registration); - _logger.LogInformation("Registering service {ServiceId} with Consul.", registration.ServiceId); + LogRegistering(registration.ServiceId); try { @@ -82,11 +83,11 @@ public async Task RegisterAsync(ConsulRegistration registration, CancellationTok { if (Options.FailFast) { - _logger.LogError(exception, "Error registering service {ServiceId} with Consul.", registration.ServiceId); + LogRegisterFailed(exception, registration.ServiceId); throw; } - _logger.LogWarning(exception, "FailFast is false. Error registering service {ServiceId} with Consul.", registration.ServiceId); + LogWarnForRegisterFailed(exception, registration.ServiceId); } } @@ -110,12 +111,12 @@ public async Task DeregisterAsync(ConsulRegistration registration, CancellationT await _scheduler.RemoveAsync(registration.InstanceId); } - _logger.LogInformation("Deregistering service {InstanceId} with Consul.", registration.InstanceId); + LogDeregistering(registration.InstanceId); await _client.Agent.ServiceDeregister(registration.InstanceId, cancellationToken); } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogError(exception, "Error deregistering service {ServiceId} with Consul.", registration.ServiceId); + LogDeregisterFailed(exception, registration.ServiceId); } } @@ -184,4 +185,19 @@ public async ValueTask DisposeAsync() await _scheduler.DisposeAsync(); } } + + [LoggerMessage(Level = LogLevel.Information, Message = "Registering service {ServiceId} with Consul.")] + private partial void LogRegistering(string serviceId); + + [LoggerMessage(Level = LogLevel.Error, Message = "Error registering service {ServiceId} with Consul.")] + private partial void LogRegisterFailed(Exception exception, string serviceId); + + [LoggerMessage(Level = LogLevel.Warning, Message = "FailFast is false. Error registering service {ServiceId} with Consul.")] + private partial void LogWarnForRegisterFailed(Exception exception, string serviceId); + + [LoggerMessage(Level = LogLevel.Information, Message = "Deregistering service {InstanceId} with Consul.")] + private partial void LogDeregistering(string instanceId); + + [LoggerMessage(Level = LogLevel.Error, Message = "Error deregistering service {ServiceId} with Consul.")] + private partial void LogDeregisterFailed(Exception exception, string serviceId); } diff --git a/src/Discovery/src/Consul/TtlScheduler.cs b/src/Discovery/src/Consul/TtlScheduler.cs index 972a06342f..792c5a3a0d 100644 --- a/src/Discovery/src/Consul/TtlScheduler.cs +++ b/src/Discovery/src/Consul/TtlScheduler.cs @@ -8,13 +8,14 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Steeltoe.Discovery.Consul.Configuration; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Steeltoe.Discovery.Consul; /// /// Scheduler used to issue TTL (time-to-live) requests to the Consul server. /// -internal sealed class TtlScheduler : IAsyncDisposable +internal sealed partial class TtlScheduler : IAsyncDisposable { private const string InstancePrefix = "service:"; @@ -80,7 +81,7 @@ public void Add(string instanceId) private void AddOrUpdate(string instanceId, ConsulHeartbeatOptions heartbeatOptions) { - _schedulerLogger.LogDebug("Adding/updating instance '{InstanceId}'.", instanceId); + LogAddingOrUpdatingInstance(_schedulerLogger, instanceId); TimeSpan interval = heartbeatOptions.ComputeHeartbeatInterval(); string checkId = instanceId; @@ -109,7 +110,7 @@ public async Task RemoveAsync(string instanceId) if (ServiceHeartbeats.TryRemove(instanceId, out PeriodicHeartbeat? heartbeat)) { - _schedulerLogger.LogDebug("Removing instance '{InstanceId}'.", instanceId); + LogRemovingInstance(_schedulerLogger, instanceId); await heartbeat.DisposeAsync(); } } @@ -130,4 +131,10 @@ public async ValueTask DisposeAsync() _isDisposed = true; } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Adding/updating instance '{InstanceId}'.")] + private static partial void LogAddingOrUpdatingInstance(ILogger logger, string instanceId); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Removing instance '{InstanceId}'.")] + private static partial void LogRemovingInstance(ILogger logger, string instanceId); } diff --git a/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs b/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs index ef18b9fb65..08e0f3f51e 100644 --- a/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs +++ b/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs @@ -14,7 +14,7 @@ namespace Steeltoe.Discovery.Eureka.Configuration; -public sealed class EurekaInstanceOptions +public sealed partial class EurekaInstanceOptions { internal const string ConfigurationPrefix = "eureka:instance"; internal const string DefaultStatusPageUrlPath = "/info"; @@ -257,14 +257,13 @@ internal void SetPortsFromListenAddresses(IEnumerable listenOnAddresses, { if (listenHttpPort != null) { - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression if (nonSecurePort == null) { - logger.LogDebug("Activating non-secure port {NonSecurePort} from {Source}.", listenHttpPort, source); + LogActivatingNonSecurePort(logger, listenHttpPort, source); } else { - logger.LogDebug("Changing non-secure port to {NonSecurePort} from {Source}.", listenHttpPort, source); + LogChangingNonSecurePort(logger, listenHttpPort, source); } NonSecurePort = listenHttpPort.Value; @@ -272,7 +271,7 @@ internal void SetPortsFromListenAddresses(IEnumerable listenOnAddresses, } else if (nonSecurePort != null) { - logger.LogDebug("Deactivating non-secure port from {Source}.", source); + LogDeactivatingNonSecurePort(logger, source); IsNonSecurePortEnabled = false; } } @@ -281,14 +280,13 @@ internal void SetPortsFromListenAddresses(IEnumerable listenOnAddresses, { if (listenHttpsPort != null) { - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression if (securePort == null) { - logger.LogDebug("Activating secure port {SecurePort} from {Source}.", listenHttpsPort, source); + LogActivatingSecurePort(logger, listenHttpsPort, source); } else { - logger.LogDebug("Changing secure port to {SecurePort} from {Source}.", listenHttpsPort, source); + LogChangingSecurePort(logger, listenHttpsPort, source); } SecurePort = listenHttpsPort.Value; @@ -296,10 +294,28 @@ internal void SetPortsFromListenAddresses(IEnumerable listenOnAddresses, } else if (securePort != null) { - logger.LogDebug("Deactivating secure port from {Source}.", source); + LogDeactivatingSecurePort(logger, source); IsSecurePortEnabled = false; } } } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Activating non-secure port {NonSecurePort} from {Source}.")] + private static partial void LogActivatingNonSecurePort(ILogger logger, int? nonSecurePort, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Changing non-secure port to {NonSecurePort} from {Source}.")] + private static partial void LogChangingNonSecurePort(ILogger logger, int? nonSecurePort, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Deactivating non-secure port from {Source}.")] + private static partial void LogDeactivatingNonSecurePort(ILogger logger, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Activating secure port {SecurePort} from {Source}.")] + private static partial void LogActivatingSecurePort(ILogger logger, int? securePort, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Changing secure port to {SecurePort} from {Source}.")] + private static partial void LogChangingSecurePort(ILogger logger, int? securePort, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Deactivating secure port from {Source}.")] + private static partial void LogDeactivatingSecurePort(ILogger logger, string source); } diff --git a/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs b/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs index d76ae11a4b..3bf7b05b3c 100644 --- a/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs +++ b/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs @@ -19,7 +19,7 @@ namespace Steeltoe.Discovery.Eureka; /// /// Provides access to the Eureka instance that represents the currently running application. /// -public sealed class EurekaApplicationInfoManager : IDisposable +public sealed partial class EurekaApplicationInfoManager : IDisposable { private readonly IOptionsMonitor _clientOptionsMonitor; private readonly IOptionsMonitor _instanceOptionsMonitor; @@ -69,7 +69,7 @@ public EurekaApplicationInfoManager(IOptionsMonitor clientO private void HandleInstanceOptionsChanged(EurekaInstanceOptions instanceOptions) { - _logger.LogDebug("Responding to changed configuration."); + LogRespondingToChangedConfiguration(); try { @@ -77,7 +77,7 @@ private void HandleInstanceOptionsChanged(EurekaInstanceOptions instanceOptions) } catch (Exception exception) { - _logger.LogError(exception, "Failed to update Eureka instance from changed configuration."); + LogFailedToUpdateInstance(exception); } } @@ -121,7 +121,7 @@ private void InnerUpdateInstance(EurekaInstanceOptions newInstanceOptions, bool } catch (Exception exception) { - _logger.LogError(exception, "Failed to adapt to configuration changes. Discarding updated configuration."); + LogFailedToAdaptConfiguration(exception); newInstance = previousInstance; } @@ -143,13 +143,13 @@ private void InnerUpdateInstance(EurekaInstanceOptions newInstanceOptions, bool if (newInstance.IsDirty) { - _logger.LogDebug("Instance has changed."); + LogInstanceHasChanged(); _instance = newInstance; eventArgs = new InstanceChangedEventArgs(newInstance, previousInstance); } else { - _logger.LogDebug("Instance has not changed."); + LogInstanceHasNotChanged(); } } @@ -164,14 +164,14 @@ private InstanceInfo MergeInstanceWithConfiguration(EurekaInstanceOptions instan if (instanceOptions.InstanceId != previousInstance.InstanceId) { // A change of InstanceId would require unregister, then re-register. - _logger.LogWarning("Discarding change of InstanceId, which is not supported."); + LogDiscardingInstanceIdChange(); instanceOptions.InstanceId = previousInstance.InstanceId; } if (!string.Equals(instanceOptions.AppName, previousInstance.AppName, StringComparison.OrdinalIgnoreCase)) { // A change of AppName would require unregister, then re-register. - _logger.LogWarning("Discarding change of AppName, which is not supported."); + LogDiscardingAppNameChange(); instanceOptions.AppName = previousInstance.AppName; } @@ -198,4 +198,25 @@ public void Dispose() { _instanceOptionsChangeToken?.Dispose(); } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Responding to changed configuration.")] + private partial void LogRespondingToChangedConfiguration(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to update Eureka instance from changed configuration.")] + private partial void LogFailedToUpdateInstance(Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to adapt to configuration changes. Discarding updated configuration.")] + private partial void LogFailedToAdaptConfiguration(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Instance has changed.")] + private partial void LogInstanceHasChanged(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Instance has not changed.")] + private partial void LogInstanceHasNotChanged(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Discarding change of InstanceId, which is not supported.")] + private partial void LogDiscardingInstanceIdChange(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Discarding change of AppName, which is not supported.")] + private partial void LogDiscardingAppNameChange(); } diff --git a/src/Discovery/src/Eureka/EurekaClient.cs b/src/Discovery/src/Eureka/EurekaClient.cs index 77d73754b5..47fa8b7de6 100644 --- a/src/Discovery/src/Eureka/EurekaClient.cs +++ b/src/Discovery/src/Eureka/EurekaClient.cs @@ -25,7 +25,7 @@ namespace Steeltoe.Discovery.Eureka; /// /// Sends HTTP requests to Eureka servers. /// -public sealed class EurekaClient +public sealed partial class EurekaClient { // HTTP endpoints are described at: https://github.com/Netflix/eureka/wiki/Eureka-REST-operations // Self preservation is described at: https://www.baeldung.com/eureka-self-preservation-renewal @@ -92,8 +92,7 @@ public async Task RegisterAsync(InstanceInfo instance, CancellationToken cancell if ((Platform.IsContainerized || Platform.IsCloudHosted) && string.Equals(instance.HostName, "localhost", StringComparison.OrdinalIgnoreCase)) { - _logger.LogWarning("Registering with hostname 'localhost' in containerized or cloud environments may not be valid. " + - "Please configure Eureka:Instance:HostName with a non-localhost address."); + LogHostNamePotentiallyInvalid(); } string requestBody = JsonSerializer.Serialize(new JsonInstanceInfoRoot @@ -257,20 +256,18 @@ private async Task ExecuteRequestAsync(HttpMethod method, stri if (!string.IsNullOrEmpty(requestBody)) { - _logger.LogDebug("Sending {RequestMethod} request to '{RequestUri}' with body: {RequestBody}.", request.Method, requestUri.ToMaskedString(), - requestBody); + LogSendingRequestWithBody(request.Method, requestUri.ToMaskedString(), requestBody); } else { - _logger.LogDebug("Sending {RequestMethod} request to '{RequestUri}' without request body.", request.Method, requestUri.ToMaskedString()); + LogSendingRequestWithoutBody(request.Method, requestUri.ToMaskedString()); } try { using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); - _logger.LogDebug("HTTP {RequestMethod} request to '{RequestUri}' returned status {StatusCode} in attempt {Attempt}.", request.Method, - requestUri.ToMaskedString(), (int)response.StatusCode, attempt); + LogRequestReturnedStatus(request.Method, requestUri.ToMaskedString(), (int)response.StatusCode, attempt); if (response.IsSuccessStatusCode) { @@ -282,22 +279,19 @@ private async Task ExecuteRequestAsync(HttpMethod method, stri } catch (JsonException exception) when (!exception.IsCancellation()) { - _logger.LogDebug(exception, "Failed to deserialize HTTP response from {RequestMethod} '{RequestUri}'.", request.Method, - requestUri.ToMaskedString()); + LogFailedToDeserializeResponse(exception, request.Method, requestUri.ToMaskedString()); } } else { string responseBody = await response.Content.ReadAsStringAsync(cancellationToken); - _logger.LogInformation("HTTP {RequestMethod} request to '{RequestUri}' failed with status {StatusCode}: {ResponseBody}", request.Method, - requestUri.ToMaskedString(), (int)response.StatusCode, responseBody); + LogRequestFailed(request.Method, requestUri.ToMaskedString(), (int)response.StatusCode, responseBody); } } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogWarning(exception, "Failed to execute HTTP {RequestMethod} request to '{RequestUri}' in attempt {Attempt}.", request.Method, - requestUri.ToMaskedString(), attempt); + LogAttemptFailed(exception, request.Method, requestUri.ToMaskedString(), attempt); } _eurekaServiceUriStateManager.MarkFailingServiceUri(serviceUri); @@ -335,7 +329,7 @@ private async Task GetRequestMessageAsync(HttpMethod method, if (requestUri.TryGetUsernamePassword(out string? username, out string? password) && password.Length > 0) { - _logger.LogDebug("Adding credentials from '{RequestUri}' to Authorization header.", requestUri.ToMaskedString()); + LogAddingCredentials(requestUri.ToMaskedString()); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"))); @@ -352,7 +346,7 @@ private async Task GetRequestMessageAsync(HttpMethod method, string accessToken = await httpClient.GetAccessTokenAsync(accessTokenUri, clientOptions.ClientId, clientOptions.ClientSecret, cancellationToken); - _logger.LogDebug("Fetched access token from '{AccessTokenUri}'.", accessTokenUri.ToMaskedString()); + LogAccessTokenFetched(accessTokenUri.ToMaskedString()); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } } @@ -364,4 +358,33 @@ private async Task GetRequestMessageAsync(HttpMethod method, return requestMessage; } + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Registering with hostname 'localhost' in containerized or cloud environments may not be valid. " + + "Please configure Eureka:Instance:HostName with a non-localhost address.")] + private partial void LogHostNamePotentiallyInvalid(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending {RequestMethod} request to '{RequestUri}' with body: '{RequestBody}'.")] + private partial void LogSendingRequestWithBody(HttpMethod requestMethod, string requestUri, string? requestBody); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending {RequestMethod} request to '{RequestUri}' without request body.")] + private partial void LogSendingRequestWithoutBody(HttpMethod requestMethod, string requestUri); + + [LoggerMessage(Level = LogLevel.Debug, Message = "HTTP {RequestMethod} request to '{RequestUri}' returned status {StatusCode} in attempt {Attempt}.")] + private partial void LogRequestReturnedStatus(HttpMethod requestMethod, string requestUri, int statusCode, int attempt); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to deserialize HTTP response from {RequestMethod} '{RequestUri}'.")] + private partial void LogFailedToDeserializeResponse(Exception exception, HttpMethod requestMethod, string requestUri); + + [LoggerMessage(Level = LogLevel.Information, Message = "HTTP {RequestMethod} request to '{RequestUri}' failed with status {StatusCode}: '{ResponseBody}'.")] + private partial void LogRequestFailed(HttpMethod requestMethod, string requestUri, int statusCode, string responseBody); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to execute HTTP {RequestMethod} request to '{RequestUri}' in attempt {Attempt}.")] + private partial void LogAttemptFailed(Exception exception, HttpMethod requestMethod, string requestUri, int attempt); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Adding credentials from '{RequestUri}' to Authorization header.")] + private partial void LogAddingCredentials(string requestUri); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Fetched access token from '{AccessTokenUri}'.")] + private partial void LogAccessTokenFetched(string accessTokenUri); } diff --git a/src/Discovery/src/Eureka/EurekaDiscoveryClient.cs b/src/Discovery/src/Eureka/EurekaDiscoveryClient.cs index dbb50b9480..ac6852233d 100644 --- a/src/Discovery/src/Eureka/EurekaDiscoveryClient.cs +++ b/src/Discovery/src/Eureka/EurekaDiscoveryClient.cs @@ -23,7 +23,7 @@ namespace Steeltoe.Discovery.Eureka; /// /// . /// -public sealed class EurekaDiscoveryClient : IDiscoveryClient +public sealed partial class EurekaDiscoveryClient : IDiscoveryClient { private readonly EurekaApplicationInfoManager _appInfoManager; private readonly EurekaClient _eurekaClient; @@ -115,10 +115,10 @@ public EurekaDiscoveryClient(EurekaApplicationInfoManager appInfoManager, Eureka } catch (Exception exception) { - _logger.LogInformation(exception, "Initial registration failed."); + LogInitialRegistrationFailed(exception); } - _logger.LogInformation("Starting heartbeat timer."); + LogStartingHeartbeatTimer(); _heartbeatTimer = StartTimer(leaseRenewalInterval.Value, HeartbeatAsyncTask); _appInfoManager.InstanceChanged += AppInfoManagerOnInstanceChanged; @@ -136,10 +136,10 @@ public EurekaDiscoveryClient(EurekaApplicationInfoManager appInfoManager, Eureka } catch (Exception exception) { - _logger.LogInformation(exception, "Initial fetch registry failed."); + LogInitialFetchRegistryFailed(exception); } - _logger.LogInformation("Starting applications cache refresh timer."); + LogStartingCacheRefreshTimer(); _cacheRefreshTimer = StartTimer(clientOptions.RegistryFetchInterval, CacheRefreshAsyncTask); _clientOptionsChangeToken = _clientOptionsMonitor.OnChange(options => @@ -198,7 +198,7 @@ public async Task ShutdownAsync(CancellationToken cancellationToken) } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogWarning(exception, "Deregister failed during shutdown."); + LogDeregisterFailedDuringShutdown(exception); } _appInfoManager.Dispose(); @@ -216,7 +216,7 @@ private async void AppInfoManagerOnInstanceChanged(object? sender, InstanceChang try { - _logger.LogDebug("Instance changed event handler: New={NewInstance}, Previous={PreviousInstance}", args.NewInstance, args.PreviousInstance); + LogInstanceChangedEvent(args.NewInstance, args.PreviousInstance); if (args.NewInstance.LeaseInfo?.RenewalInterval != args.PreviousInstance.LeaseInfo?.RenewalInterval) { @@ -230,7 +230,7 @@ private async void AppInfoManagerOnInstanceChanged(object? sender, InstanceChang { if (!exception.IsCancellation()) { - _logger.LogError(exception, "Failed to handle {EventName} event.", nameof(EurekaApplicationInfoManager.InstanceChanged)); + LogFailedToHandleEvent(exception, nameof(EurekaApplicationInfoManager.InstanceChanged)); } } } @@ -259,9 +259,9 @@ internal async Task RegisterAsync(bool requireDirtyInstance, CancellationToken c if (!requireDirtyInstance || snapshot.IsDirty) { - _logger.LogDebug("Registering {Application}/{Instance}.", snapshot.AppName, snapshot.InstanceId); + LogRegistering(snapshot.AppName, snapshot.InstanceId); await _eurekaClient.RegisterAsync(snapshot, cancellationToken); - _logger.LogDebug("Register {Application}/{Instance} succeeded.", snapshot.AppName, snapshot.InstanceId); + LogRegistrationSucceeded(snapshot.AppName, snapshot.InstanceId); snapshot.IsDirty = false; } @@ -280,9 +280,9 @@ internal async Task DeregisterAsync(CancellationToken cancellationToken) { InstanceInfo snapshot = _appInfoManager.Instance; - _logger.LogDebug("Deregistering {Application}/{Instance}.", snapshot.AppName, snapshot.InstanceId); + LogDeregistering(snapshot.AppName, snapshot.InstanceId); await _eurekaClient.DeregisterAsync(snapshot.AppName, snapshot.InstanceId, cancellationToken); - _logger.LogDebug("Deregister {Application}/{Instance} succeeded.", snapshot.AppName, snapshot.InstanceId); + LogDeregistrationSucceeded(snapshot.AppName, snapshot.InstanceId); } finally { @@ -299,16 +299,15 @@ internal async Task RenewAsync(CancellationToken cancellationToken) { InstanceInfo snapshot = _appInfoManager.Instance; - _logger.LogDebug("Sending heartbeat for {Application}/{Instance}.", snapshot.AppName, snapshot.InstanceId); + LogSendingHeartbeat(snapshot.AppName, snapshot.InstanceId); await _eurekaClient.HeartbeatAsync(snapshot.AppName, snapshot.InstanceId, snapshot.LastDirtyTimeUtc, cancellationToken); - _logger.LogDebug("Heartbeat for {Application}/{Instance} succeeded.", snapshot.AppName, snapshot.InstanceId); + LogHeartbeatSucceeded(snapshot.AppName, snapshot.InstanceId); _lastGoodHeartbeatTimeUtc = new NullableValueWrapper(_timeProvider.GetUtcNow().UtcDateTime); } catch (EurekaTransportException exception) { - _logger.LogWarning(exception, - "Eureka heartbeat failed. This could happen if Eureka was offline during app startup. Attempting to (re)register now."); + LogHeartbeatFailed(exception); await RegisterAsync(false, cancellationToken); } @@ -368,7 +367,7 @@ private void OnApplicationsFetched(ApplicationsFetchedEventArgs? args) } catch (Exception exception) { - _logger.LogError(exception, "Failed to handle {EventName} event.", nameof(ApplicationsFetched)); + LogFailedToHandleEvent(exception, nameof(ApplicationsFetched)); } }); } @@ -378,13 +377,13 @@ internal async Task FetchFullRegistryAsync(Cancellati { EurekaClientOptions clientOptions = _clientOptionsMonitor.CurrentValue; - _logger.LogDebug("Sending request to fetch applications."); + LogFetchingApplications(); ApplicationInfoCollection applications = string.IsNullOrWhiteSpace(clientOptions.RegistryRefreshSingleVipAddress) ? await _eurekaClient.GetApplicationsAsync(cancellationToken) : await _eurekaClient.GetByVipAsync(clientOptions.RegistryRefreshSingleVipAddress, cancellationToken); - _logger.LogDebug("Full registry fetch succeeded with {Count} applications.", applications.Count); + LogFullRegistryFetchSucceeded(applications.Count); return applications; } @@ -394,29 +393,28 @@ internal async Task FetchRegistryDeltaAsync(Cancellat try { - _logger.LogDebug("Sending request to fetch applications delta."); + LogFetchingApplicationsDelta(); delta = await _eurekaClient.GetDeltaAsync(cancellationToken); } catch (EurekaTransportException exception) { - _logger.LogDebug(exception, "Failed to fetch registry delta. Trying full fetch."); + LogFailedToFetchDelta(exception); return await FetchFullRegistryAsync(cancellationToken); } - _logger.LogDebug("Registry delta fetched, updating local cache."); + LogRegistryDeltaFetched(); _remoteApps.UpdateFromDelta(delta); string hashCode = _remoteApps.ComputeHashCode(); if (hashCode != delta.AppsHashCode) { - _logger.LogWarning("Discarding fetched registry delta due to hash codes mismatch (Local={HashLocal}, Remote={HashRemote}). Trying full fetch.", - hashCode, delta.AppsHashCode); + LogDeltaHashCodeMismatch(hashCode, delta.AppsHashCode); return await FetchFullRegistryAsync(cancellationToken); } - _logger.LogDebug("Registry delta fetch succeeded with {Count} changes.", delta.Count); + LogDeltaFetchSucceeded(delta.Count); _remoteApps.AppsHashCode = delta.AppsHashCode; return _remoteApps; } @@ -427,26 +425,26 @@ internal async Task RunHealthChecksAsync(CancellationToken cancellationToken) { if (_appInfoManager.Instance.Status == InstanceStatus.Starting) { - _logger.LogDebug("Skipping health check handler in starting state."); + LogSkippingHealthCheck(); return; } try { InstanceStatus aggregatedStatus = await HealthCheckHandler.GetStatusAsync(_hasFirstHeartbeatCompleted, cancellationToken); - _logger.LogDebug("Health check handler returned status {Status}.", aggregatedStatus); + LogHealthCheckStatus(aggregatedStatus); InstanceInfo snapshot = _appInfoManager.Instance; if (aggregatedStatus != snapshot.Status) { - _logger.LogDebug("Changing instance status from {LocalStatus} to {RemoteStatus}.", snapshot.Status, aggregatedStatus); + LogChangingInstanceStatus(snapshot.Status, aggregatedStatus); _appInfoManager.UpdateStatusWithoutRaisingEvent(aggregatedStatus); } } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogError(exception, "Failed to determine health status."); + LogFailedToDetermineHealthStatus(exception); } } } @@ -461,8 +459,7 @@ private void UpdateLastRemoteInstanceStatusFromCache() { if (remoteInstance.EffectiveStatus != snapshot.EffectiveStatus) { - _logger.LogWarning("Remote instance status {RemoteStatus} differs from local status {LocalStatus}.", remoteInstance.EffectiveStatus, - snapshot.EffectiveStatus); + LogRemoteStatusDiffers(remoteInstance.EffectiveStatus, snapshot.EffectiveStatus); } // We have ownership of the local instance, so don't take the remote status. @@ -486,7 +483,7 @@ private async void HeartbeatAsyncTask() { if (!exception.IsCancellation()) { - _logger.LogError(exception, "Periodic renew failed."); + LogPeriodicRenewFailed(exception); } } } @@ -507,7 +504,7 @@ private async void CacheRefreshAsyncTask() { if (!exception.IsCancellation()) { - _logger.LogError(exception, "Periodic fetch of applications failed."); + LogPeriodicFetchFailed(exception); } } } @@ -539,7 +536,7 @@ public Task> GetInstancesAsync(string serviceId, Cancell if (_logger.IsEnabled(LogLevel.Debug)) { string instanceNames = string.Join(", ", serviceInstances.Select(FormatServiceInstance)); - _logger.LogDebug("Returning {Count} service instances for '{ServiceId}': {ServiceInstances}", serviceInstances.Length, serviceId, instanceNames); + LogReturningServiceInstances(serviceInstances.Length, serviceId, instanceNames); } return Task.FromResult>(serviceInstances); @@ -572,4 +569,94 @@ public IServiceInstance GetLocalServiceInstance() { return new EurekaServiceInstance(_appInfoManager.Instance); } + + [LoggerMessage(Level = LogLevel.Information, Message = "Initial registration failed.")] + private partial void LogInitialRegistrationFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Information, Message = "Starting heartbeat timer.")] + private partial void LogStartingHeartbeatTimer(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Initial fetch registry failed.")] + private partial void LogInitialFetchRegistryFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Information, Message = "Starting applications cache refresh timer.")] + private partial void LogStartingCacheRefreshTimer(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Deregister failed during shutdown.")] + private partial void LogDeregisterFailedDuringShutdown(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, + Message = "Instance changed event handler invoked with new instance {NewInstance} and previous instance {PreviousInstance}.")] + private partial void LogInstanceChangedEvent(InstanceInfo newInstance, InstanceInfo previousInstance); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to handle {EventName} event.")] + private partial void LogFailedToHandleEvent(Exception exception, string eventName); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Registering {Application}/{Instance}.")] + private partial void LogRegistering(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Register {Application}/{Instance} succeeded.")] + private partial void LogRegistrationSucceeded(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Deregistering {Application}/{Instance}.")] + private partial void LogDeregistering(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Deregister {Application}/{Instance} succeeded.")] + private partial void LogDeregistrationSucceeded(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending heartbeat for {Application}/{Instance}.")] + private partial void LogSendingHeartbeat(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Heartbeat for {Application}/{Instance} succeeded.")] + private partial void LogHeartbeatSucceeded(string application, string instance); + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Eureka heartbeat failed. This could happen if Eureka was offline during app startup. Attempting to (re)register now.")] + private partial void LogHeartbeatFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending request to fetch applications.")] + private partial void LogFetchingApplications(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Full registry fetch succeeded with {Count} applications.")] + private partial void LogFullRegistryFetchSucceeded(int count); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending request to fetch applications delta.")] + private partial void LogFetchingApplicationsDelta(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to fetch registry delta. Trying full fetch.")] + private partial void LogFailedToFetchDelta(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Registry delta fetched, updating local cache.")] + private partial void LogRegistryDeltaFetched(); + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Discarding fetched registry delta due to hash code mismatch between local {HashLocal} and remote {HashRemote}. Trying full fetch.")] + private partial void LogDeltaHashCodeMismatch(string hashLocal, string? hashRemote); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Registry delta fetch succeeded with {Count} changes.")] + private partial void LogDeltaFetchSucceeded(int count); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Skipping health check handler in starting state.")] + private partial void LogSkippingHealthCheck(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Health check handler returned status {Status}.")] + private partial void LogHealthCheckStatus(InstanceStatus status); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Changing instance status from {LocalStatus} to {RemoteStatus}.")] + private partial void LogChangingInstanceStatus(InstanceStatus? localStatus, InstanceStatus remoteStatus); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to determine health status.")] + private partial void LogFailedToDetermineHealthStatus(Exception exception); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Remote instance status {RemoteStatus} differs from local status {LocalStatus}.")] + private partial void LogRemoteStatusDiffers(InstanceStatus remoteStatus, InstanceStatus localStatus); + + [LoggerMessage(Level = LogLevel.Error, Message = "Periodic renew failed.")] + private partial void LogPeriodicRenewFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Periodic fetch of applications failed.")] + private partial void LogPeriodicFetchFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Returning {Count} service instances for '{ServiceId}': {ServiceInstances}.")] + private partial void LogReturningServiceInstances(int count, string serviceId, string serviceInstances); } diff --git a/src/Discovery/src/Eureka/EurekaServiceUriStateManager.cs b/src/Discovery/src/Eureka/EurekaServiceUriStateManager.cs index 85b1113930..d1dfaeae0a 100644 --- a/src/Discovery/src/Eureka/EurekaServiceUriStateManager.cs +++ b/src/Discovery/src/Eureka/EurekaServiceUriStateManager.cs @@ -19,7 +19,7 @@ namespace Steeltoe.Discovery.Eureka; /// /// Keeps track of working and broken Eureka service URIs that are configured, with stickiness to the last working server. /// -public sealed class EurekaServiceUriStateManager +public sealed partial class EurekaServiceUriStateManager { private readonly IOptionsMonitor _optionsMonitor; private readonly ILogger _logger; @@ -62,7 +62,7 @@ private Uri[] GetAvailableServiceUris() if (_failedServiceUris.Count > 0 && _failedServiceUris.Count >= threshold) { - _logger.LogDebug("Clearing quarantined list of size {Count}.", _failedServiceUris.Count); + LogClearingQuarantinedList(_failedServiceUris.Count); _failedServiceUris.Clear(); } @@ -134,6 +134,9 @@ internal void MarkFailingServiceUri(Uri serviceUri) } } + [LoggerMessage(Level = LogLevel.Debug, Message = "Clearing quarantined list of size {Count}.")] + private partial void LogClearingQuarantinedList(int count); + /// /// Provides a method to sequentially try all available Eureka servers. /// diff --git a/src/Discovery/src/HttpClients/LoadBalancers/RandomLoadBalancer.cs b/src/Discovery/src/HttpClients/LoadBalancers/RandomLoadBalancer.cs index 82c6477d3b..5f9ca42822 100644 --- a/src/Discovery/src/HttpClients/LoadBalancers/RandomLoadBalancer.cs +++ b/src/Discovery/src/HttpClients/LoadBalancers/RandomLoadBalancer.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Discovery.HttpClients.LoadBalancers; /// /// Returns random service instances. /// -public sealed class RandomLoadBalancer : ILoadBalancer +public sealed partial class RandomLoadBalancer : ILoadBalancer { private readonly ServiceInstancesResolver _serviceInstancesResolver; private readonly ILogger _logger; @@ -40,13 +40,13 @@ public async Task ResolveServiceInstanceAsync(Uri requestUri, CancellationT ArgumentNullException.ThrowIfNull(requestUri); string serviceName = requestUri.Host; - _logger.LogTrace("Resolving service instance for '{ServiceName}'.", serviceName); + LogResolvingServiceInstance(serviceName); IList availableServiceInstances = await _serviceInstancesResolver.ResolveInstancesAsync(serviceName, cancellationToken); if (availableServiceInstances.Count == 0) { - _logger.LogWarning("No service instances are available for '{ServiceName}'.", serviceName); + LogNoServiceInstances(serviceName); return requestUri; } @@ -54,7 +54,7 @@ public async Task ResolveServiceInstanceAsync(Uri requestUri, CancellationT int index = Random.Shared.Next(availableServiceInstances.Count); IServiceInstance serviceInstance = availableServiceInstances[index]; - _logger.LogDebug("Resolved '{ServiceName}' to '{ServiceInstance}'.", serviceName, serviceInstance.Uri); + LogServiceInstanceResolved(serviceName, serviceInstance.Uri); return new Uri(serviceInstance.Uri, requestUri.PathAndQuery); } @@ -64,4 +64,13 @@ public Task UpdateStatisticsAsync(Uri requestUri, Uri serviceInstanceUri, TimeSp cancellationToken.ThrowIfCancellationRequested(); return Task.CompletedTask; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Resolving service instance for '{ServiceName}'.")] + private partial void LogResolvingServiceInstance(string serviceName); + + [LoggerMessage(Level = LogLevel.Warning, Message = "No service instances are available for '{ServiceName}'.")] + private partial void LogNoServiceInstances(string serviceName); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Resolved '{ServiceName}' to '{ServiceInstance}'.")] + private partial void LogServiceInstanceResolved(string serviceName, Uri serviceInstance); } diff --git a/src/Discovery/src/HttpClients/LoadBalancers/RoundRobinLoadBalancer.cs b/src/Discovery/src/HttpClients/LoadBalancers/RoundRobinLoadBalancer.cs index e0a913d367..4b459df508 100644 --- a/src/Discovery/src/HttpClients/LoadBalancers/RoundRobinLoadBalancer.cs +++ b/src/Discovery/src/HttpClients/LoadBalancers/RoundRobinLoadBalancer.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Discovery.HttpClients.LoadBalancers; /// /// Returns service instances in round-robin fashion, optionally using distributed caching for determining the next instance. /// -public sealed class RoundRobinLoadBalancer : ILoadBalancer +public sealed partial class RoundRobinLoadBalancer : ILoadBalancer { private const string CacheKeyPrefix = "Steeltoe-LoadBalancerIndex-"; private readonly ServiceInstancesResolver _serviceInstancesResolver; @@ -69,20 +69,20 @@ public async Task ResolveServiceInstanceAsync(Uri requestUri, CancellationT ArgumentNullException.ThrowIfNull(requestUri); string serviceName = requestUri.Host; - _logger.LogTrace("Resolving service instance for '{ServiceName}'.", serviceName); + LogResolvingServiceInstance(serviceName); IList availableServiceInstances = await _serviceInstancesResolver.ResolveInstancesAsync(serviceName, cancellationToken); if (availableServiceInstances.Count == 0) { - _logger.LogWarning("No service instances are available for '{ServiceName}'.", serviceName); + LogNoServiceInstances(serviceName); return requestUri; } int instanceIndex = await GetNextInstanceIndexAsync(serviceName, availableServiceInstances.Count, cancellationToken); IServiceInstance serviceInstance = availableServiceInstances[instanceIndex]; - _logger.LogDebug("Resolved '{ServiceName}' to '{ServiceInstance}'.", serviceName, serviceInstance.Uri); + LogServiceInstanceResolved(serviceName, serviceInstance.Uri); return new Uri(serviceInstance.Uri, requestUri.PathAndQuery); } @@ -128,4 +128,13 @@ public Task UpdateStatisticsAsync(Uri requestUri, Uri serviceInstanceUri, TimeSp cancellationToken.ThrowIfCancellationRequested(); return Task.CompletedTask; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Resolving service instance for '{ServiceName}'.")] + private partial void LogResolvingServiceInstance(string serviceName); + + [LoggerMessage(Level = LogLevel.Warning, Message = "No service instances are available for '{ServiceName}'.")] + private partial void LogNoServiceInstances(string serviceName); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Resolved '{ServiceName}' to '{ServiceInstance}'.")] + private partial void LogServiceInstanceResolved(string serviceName, Uri serviceInstance); } diff --git a/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesResolver.cs b/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesResolver.cs index c6e6be70c6..6a13c24690 100644 --- a/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesResolver.cs +++ b/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesResolver.cs @@ -14,7 +14,7 @@ namespace Steeltoe.Discovery.HttpClients.LoadBalancers; /// /// Queries all discovery clients for service instances, optionally caching the results using . /// -public sealed class ServiceInstancesResolver +public sealed partial class ServiceInstancesResolver { private readonly IDiscoveryClient[] _discoveryClients; private readonly IDistributedCache? _distributedCache; @@ -66,7 +66,7 @@ public ServiceInstancesResolver(IEnumerable discoveryClients, if (_discoveryClients.Length == 0) { - _logger.LogWarning("No discovery clients are registered."); + LogNoDiscoveryClients(); } } @@ -83,7 +83,7 @@ public async Task> ResolveInstancesAsync(string serviceI if (instancesFromCache != null) { - _logger.LogDebug("Returning {Count} instances from cache.", instancesFromCache.Count); + LogReturningInstancesFromCache(instancesFromCache.Count); return instancesFromCache; } } @@ -99,7 +99,7 @@ public async Task> ResolveInstancesAsync(string serviceI } catch (Exception exception) { - _logger.LogError(exception, "Failed to get instances from {DiscoveryClient}.", discoveryClient.GetType()); + LogFailedToGetInstances(exception, discoveryClient.GetType()); } } @@ -133,6 +133,15 @@ private static byte[] ToCacheValue(IEnumerable instances) return JsonSerializer.SerializeToUtf8Bytes(serializableInstances); } + [LoggerMessage(Level = LogLevel.Warning, Message = "No discovery clients are registered.")] + private partial void LogNoDiscoveryClients(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Returning {Count} instances from cache.")] + private partial void LogReturningInstancesFromCache(int count); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to get instances from {DiscoveryClient}.")] + private partial void LogFailedToGetInstances(Exception exception, Type discoveryClient); + private sealed class JsonSerializableServiceInstance : IServiceInstance { // Trust that deserialized instances meet the IServiceInstance contract, so suppress nullability warnings. diff --git a/src/Discovery/test/Eureka.Test/Transport/EurekaClientTest.cs b/src/Discovery/test/Eureka.Test/Transport/EurekaClientTest.cs index b653071dea..9e5958cacf 100644 --- a/src/Discovery/test/Eureka.Test/Transport/EurekaClientTest.cs +++ b/src/Discovery/test/Eureka.Test/Transport/EurekaClientTest.cs @@ -128,6 +128,35 @@ public sealed class EurekaClientTest } """; + private static readonly string ExpectedJsonRequestBody = """ + { + "instance": { + "instanceId": "some", + "app": "FOOBAR", + "ipAddr": "127.0.0.1", + "port": { + "@enabled": "true", + "$": 8080 + }, + "securePort": { + "@enabled": "false", + "$": 9090 + }, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "hostName": "localhost", + "overriddenstatus": "UNKNOWN", + "metadata": { + "@class": "java.util.Collections$EmptyMap" + }, + "lastUpdatedTimestamp": "1708427732823", + "lastDirtyTimestamp": "1708427732823" + } + } + """.ReplaceLineEndings(string.Empty).Replace(" ", string.Empty, StringComparison.Ordinal); + [Fact] public async Task RegisterAsync_ThrowsOnUnreachableServer() { @@ -161,8 +190,7 @@ public async Task RegisterAsync_ThrowsOnUnreachableServer() IList logMessages = capturingLoggerProvider.GetAll(); logMessages.Should().BeEquivalentTo( - $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://host-that-does-not-exist.net:9999/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://host-that-does-not-exist.net:9999/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", $"WARN {typeof(EurekaClient)}: Failed to execute HTTP POST request to 'http://host-that-does-not-exist.net:9999/apps/FOOBAR' in attempt 1."); } @@ -208,10 +236,9 @@ public async Task RegisterAsync_ThrowsOnErrorResponse() logMessages.Should().BeEquivalentTo( [ - $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' returned status 404 in attempt 1.", - $"INFO {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' failed with status 404: Sorry!" + $"INFO {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' failed with status 404: 'Sorry!'." ], options => options.WithStrictOrdering()); } @@ -254,10 +281,9 @@ public async Task RegisterAsync_ThrowsOnRetryLimitReached() logMessages.Should().BeEquivalentTo( [ - $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' returned status 404 in attempt 1.", - $"INFO {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' failed with status 404: " + $"INFO {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' failed with status 404: ''." ], options => options.WithStrictOrdering()); } @@ -364,8 +390,7 @@ public async Task RegisterAsync_SendsRequestToServer() logMessages.Should().BeEquivalentTo( [ - $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' returned status 204 in attempt 1." ], options => options.WithStrictOrdering()); } @@ -408,12 +433,10 @@ public async Task RegisterAsync_TriesSecondServerIfFirstOneFails() logMessages.Should().BeEquivalentTo( [ - $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://server1:8761/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://server1:8761/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://server1:8761/apps/FOOBAR' returned status 404 in attempt 1.", - $"INFO {typeof(EurekaClient)}: HTTP POST request to 'http://server1:8761/apps/FOOBAR' failed with status 404: ", - $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://server2:8761/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", + $"INFO {typeof(EurekaClient)}: HTTP POST request to 'http://server1:8761/apps/FOOBAR' failed with status 404: ''.", + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://server2:8761/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://server2:8761/apps/FOOBAR' returned status 204 in attempt 2." ], options => options.WithStrictOrdering()); } diff --git a/src/Management/src/Endpoint/ActuatorMapper.cs b/src/Management/src/Endpoint/ActuatorMapper.cs index 6f65dabf8c..12dc2242ed 100644 --- a/src/Management/src/Endpoint/ActuatorMapper.cs +++ b/src/Management/src/Endpoint/ActuatorMapper.cs @@ -12,7 +12,7 @@ namespace Steeltoe.Management.Endpoint; -internal abstract class ActuatorMapper +internal abstract partial class ActuatorMapper { private readonly IOptionsMonitor _managementOptionsMonitor; private readonly ILogger _logger; @@ -47,9 +47,8 @@ protected ActuatorMapper(IEnumerable middlewares, IOptionsM { if (managementOptions is { IsCloudFoundryEnabled: true, HasCloudFoundrySecurity: false }) { - _logger.LogWarning( - $"Actuators at the {ConfigureManagementOptions.DefaultCloudFoundryPath} endpoint are disabled because the Cloud Foundry security middleware is not active. " + - $"Call {nameof(EndpointApplicationBuilderExtensions.UseCloudFoundrySecurity)}() from your custom middleware pipeline to enable them."); + LogCloudFoundryActuatorsDisabled(ConfigureManagementOptions.DefaultCloudFoundryPath, + nameof(EndpointApplicationBuilderExtensions.UseCloudFoundrySecurity)); } foreach (IEndpointMiddleware middleware in _middlewares.Where(middleware => middleware is not HypermediaEndpointMiddleware)) @@ -62,7 +61,15 @@ protected ActuatorMapper(IEnumerable middlewares, IOptionsM protected void LogErrorForDuplicateRoute(string routePattern, IEndpointMiddleware existingMiddleware, IEndpointMiddleware duplicateMiddleware) { - _logger.LogError("Skipping over duplicate route '{Route}' from {DuplicateMiddlewareType}, which was already added by {ExistingMiddlewareType}", - routePattern, duplicateMiddleware.GetType(), existingMiddleware.GetType()); + LogSkippingDuplicateRoute(routePattern, duplicateMiddleware.GetType(), existingMiddleware.GetType()); } + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Actuators at the {CloudFoundryPath} endpoint are disabled because the Cloud Foundry security middleware is not active. " + + "Call {MethodName}() from your custom middleware pipeline to enable them.")] + private partial void LogCloudFoundryActuatorsDisabled(string cloudFoundryPath, string methodName); + + [LoggerMessage(Level = LogLevel.Error, + Message = "Skipping over duplicate route '{Route}' from {DuplicateMiddlewareType}, which was already added by {ExistingMiddlewareType}.")] + private partial void LogSkippingDuplicateRoute(string route, Type duplicateMiddlewareType, Type existingMiddlewareType); } diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs index 629ea0a4a1..c8734d6ed0 100644 --- a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs @@ -18,7 +18,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.CloudFoundry; -internal sealed class CloudFoundrySecurityMiddleware +internal sealed partial class CloudFoundrySecurityMiddleware { private const string BearerTokenPrefix = "Bearer "; private readonly IOptionsMonitor _managementOptionsMonitor; @@ -53,7 +53,7 @@ public async Task InvokeAsync(HttpContext context) { ArgumentNullException.ThrowIfNull(context); - _logger.LogDebug("InvokeAsync({RequestPath})", context.Request.Path.Value); + LogEntering(context.Request.Path.Value); ManagementOptions managementOptions = _managementOptionsMonitor.CurrentValue; if (Platform.IsCloudFoundry && managementOptions.IsCloudFoundryEnabled && PermissionsProvider.IsCloudFoundryRequest(context.Request.Path)) @@ -62,8 +62,7 @@ public async Task InvokeAsync(HttpContext context) if (string.IsNullOrEmpty(endpointOptions.ApplicationId)) { - _logger.LogError( - "The Application Id could not be found. Make sure the Cloud Foundry Configuration Provider has been added to the application configuration."); + LogApplicationIdMissing(); await ReturnErrorAsync(context, new SecurityResult(HttpStatusCode.ServiceUnavailable, PermissionsProvider.Messages.ApplicationIdMissing)); return; @@ -161,7 +160,7 @@ internal async Task GetPermissionsAsync(HttpContext context) private async Task ReturnErrorAsync(HttpContext context, SecurityResult error) { - _logger.LogError("Actuator Security Error: {Code} - {Message}", error.Code, error.Message); + LogSecurityError(error.Code, error.Message); context.Response.Headers.Append("Content-Type", "application/json;charset=UTF-8"); // UseStatusCodeFromResponse was added to prevent IIS/HWC from blocking the response body on 500-level errors. @@ -174,4 +173,14 @@ private async Task ReturnErrorAsync(HttpContext context, SecurityResult error) await JsonSerializer.SerializeAsync(context.Response.Body, error, cancellationToken: context.RequestAborted); } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Entering Cloud Foundry Security middleware at path {RequestPath}.")] + private partial void LogEntering(string? requestPath); + + [LoggerMessage(Level = LogLevel.Error, + Message = "The Application Id could not be found. Make sure the Cloud Foundry Configuration Provider has been added to the application configuration.")] + private partial void LogApplicationIdMissing(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Actuator security error with status {Code}: '{Message}'.")] + private partial void LogSecurityError(HttpStatusCode code, string? message); } diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/PermissionsProvider.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/PermissionsProvider.cs index 7b9d77c571..c44bfd35e9 100644 --- a/src/Management/src/Endpoint/Actuators/CloudFoundry/PermissionsProvider.cs +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/PermissionsProvider.cs @@ -17,7 +17,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.CloudFoundry; -internal sealed class PermissionsProvider +internal sealed partial class PermissionsProvider { private const string ReadSensitiveDataJsonPropertyName = "read_sensitive_data"; public const string HttpClientName = "CloudFoundrySecurity"; @@ -59,14 +59,13 @@ public async Task GetPermissionsAsync(string accessToken, Cancel try { - _logger.LogDebug("GetPermissionsAsync({Uri})", checkPermissionsUri); + LogGetPermissions(checkPermissionsUri); using HttpClient httpClient = CreateHttpClient(); using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); if (response.StatusCode != HttpStatusCode.OK) { - _logger.LogInformation("Cloud Foundry returned status: {HttpStatus} while obtaining permissions from: {PermissionsUri}", response.StatusCode, - checkPermissionsUri); + LogResponseStatus(response.StatusCode, checkPermissionsUri); if (response.StatusCode is HttpStatusCode.Forbidden) { @@ -103,7 +102,7 @@ public async Task ParsePermissionsResponseAsync(HttpRespons { json = await response.Content.ReadAsStringAsync(cancellationToken); - _logger.LogDebug("GetPermissionsAsync returned json: {Json}", SecurityUtilities.SanitizeInput(json)); + LogResponseJson(SecurityUtilities.SanitizeInput(json)); var result = JsonSerializer.Deserialize>(json); @@ -118,7 +117,7 @@ public async Task ParsePermissionsResponseAsync(HttpRespons throw new SecurityException($"Exception extracting permissions from json: {SecurityUtilities.SanitizeInput(json)}", exception); } - _logger.LogDebug("GetPermissionsAsync returning: {Permissions}", permissions); + LogPermissions(permissions); return permissions; } @@ -129,6 +128,18 @@ private HttpClient CreateHttpClient() return httpClient; } + [LoggerMessage(Level = LogLevel.Debug, Message = "Fetching permissions from {PermissionsUri}.")] + private partial void LogGetPermissions(string permissionsUri); + + [LoggerMessage(Level = LogLevel.Information, Message = "Cloud Foundry returned status {HttpStatus} while obtaining permissions from {PermissionsUri}.")] + private partial void LogResponseStatus(HttpStatusCode httpStatus, string permissionsUri); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Permissions response returned JSON: {Json}")] + private partial void LogResponseJson(string json); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Resolved permissions to {Permissions}.")] + private partial void LogPermissions(EndpointPermissions permissions); + internal static class Messages { public const string AccessDenied = "Access denied"; diff --git a/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointHandler.cs index 073aea1ee1..8352763605 100644 --- a/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointHandler.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.DbMigrations; -internal sealed class DbMigrationsEndpointHandler : IDbMigrationsEndpointHandler +internal sealed partial class DbMigrationsEndpointHandler : IDbMigrationsEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IServiceProvider _serviceProvider; @@ -41,7 +41,7 @@ public async Task> InvokeAsync(object if (dbContextType is null) { - _logger.LogError("The Microsoft.EntityFrameworkCore.DbContext type is unavailable. Are you missing a package reference?"); + LogDbContextTypeUnavailable(); } else { @@ -83,7 +83,7 @@ public async Task> InvokeAsync(object } catch (DbException exception) when (exception.Message.Contains("exist", StringComparison.Ordinal)) { - _logger.LogWarning(exception, "Failed to load pending/applied migrations, returning all migrations."); + LogFailedToLoadMigrations(exception); AddRange(descriptor.PendingMigrations, _scanner.GetMigrations(dbContext)); } } @@ -106,4 +106,10 @@ private static void AddRange(IList source, IEnumerable items) } } } + + [LoggerMessage(Level = LogLevel.Error, Message = "The Microsoft.EntityFrameworkCore.DbContext type is unavailable. Are you missing a package reference?")] + private partial void LogDbContextTypeUnavailable(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to load pending/applied migrations, returning all migrations.")] + private partial void LogFailedToLoadMigrations(Exception exception); } diff --git a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs index 7a72857137..5dfca27f23 100644 --- a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Environment; -internal sealed class EnvironmentEndpointHandler : IEnvironmentEndpointHandler +internal sealed partial class EnvironmentEndpointHandler : IEnvironmentEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IConfiguration _configuration; @@ -36,7 +36,7 @@ public EnvironmentEndpointHandler(IOptionsMonitor op public Task InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogTrace("Fetching property sources."); + LogFetchingPropertySources(); List activeProfiles = [_environment.EnvironmentName]; IList propertySources = GetPropertySources(); @@ -114,4 +114,7 @@ private static HashSet GetFullKeyNames(IConfigurationProvider provider, return initialKeys; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Fetching property sources.")] + private partial void LogFetchingPropertySources(); } diff --git a/src/Management/src/Endpoint/Actuators/Health/Availability/ApplicationAvailability.cs b/src/Management/src/Endpoint/Actuators/Health/Availability/ApplicationAvailability.cs index 63c1cee1c4..eb48e6fcc0 100644 --- a/src/Management/src/Endpoint/Actuators/Health/Availability/ApplicationAvailability.cs +++ b/src/Management/src/Endpoint/Actuators/Health/Availability/ApplicationAvailability.cs @@ -6,7 +6,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Health.Availability; -public sealed class ApplicationAvailability +public sealed partial class ApplicationAvailability { public const string LivenessKey = "Liveness"; public const string ReadinessKey = "Readiness"; @@ -58,7 +58,7 @@ public void SetAvailabilityState(string availabilityType, AvailabilityState newS throw new InvalidOperationException($"{availabilityType} state can only be of type {availabilityType}State"); } - _logger.LogTrace("{StateKey} availability has been set to {NewState} by {Caller}", availabilityType, newState, caller ?? "unspecified"); + LogAvailabilityStateChanged(availabilityType, newState, caller ?? "unspecified"); _availabilityStates[availabilityType] = newState; if (availabilityType == LivenessKey) @@ -71,4 +71,7 @@ public void SetAvailabilityState(string availabilityType, AvailabilityState newS ReadinessChanged?.Invoke(this, new AvailabilityEventArgs(newState)); } } + + [LoggerMessage(Level = LogLevel.Trace, Message = "{StateKey} availability has been set to {NewState} by {Caller}.")] + private partial void LogAvailabilityStateChanged(string stateKey, AvailabilityState newState, string caller); } diff --git a/src/Management/src/Endpoint/Actuators/Health/Contributors/AvailabilityStateHealthContributor.cs b/src/Management/src/Endpoint/Actuators/Health/Contributors/AvailabilityStateHealthContributor.cs index 6552652017..ec7e703b6b 100644 --- a/src/Management/src/Endpoint/Actuators/Health/Contributors/AvailabilityStateHealthContributor.cs +++ b/src/Management/src/Endpoint/Actuators/Health/Contributors/AvailabilityStateHealthContributor.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Health.Contributors; -internal abstract class AvailabilityStateHealthContributor : IHealthContributor +internal abstract partial class AvailabilityStateHealthContributor : IHealthContributor { private readonly IDictionary _stateMappings; private readonly ILogger _logger; @@ -39,18 +39,18 @@ private HealthCheckResult Health() if (currentHealth == null) { - _logger.LogError("Failed to get current availability state"); + LogFailedToGetState(); health.Description = "Failed to get current availability state"; } else { - try + if (_stateMappings.TryGetValue(currentHealth, out HealthStatus status)) { - health.Status = _stateMappings[currentHealth]; + health.Status = status; } - catch (Exception exception) + else { - _logger.LogError(exception, "Failed to map current availability state"); + LogFailedToMapState(currentHealth); health.Description = "Failed to map current availability state"; } } @@ -59,4 +59,10 @@ private HealthCheckResult Health() } protected abstract AvailabilityState? GetState(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to get current availability state.")] + private partial void LogFailedToGetState(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to map availability state {State}.")] + private partial void LogFailedToMapState(AvailabilityState state); } diff --git a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointHandler.cs index e7b3cf8770..426c39ade0 100644 --- a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointHandler.cs @@ -12,7 +12,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Health; -internal sealed class HealthEndpointHandler : IHealthEndpointHandler +internal sealed partial class HealthEndpointHandler : IHealthEndpointHandler { private readonly IOptionsMonitor _endpointOptionsMonitor; private readonly IHealthAggregator _healthAggregator; @@ -115,8 +115,7 @@ private void CleanResponse(HealthEndpointResponse response, HealthEndpointOption if (ShouldClear(showComponents, healthRequest)) { - _logger.LogTrace("Clearing health check components. ShowComponents={ShowComponents}, HasClaim={HasClaimForHealth}.", showComponents, - healthRequest.HasClaim); + LogClearingComponents(showComponents, healthRequest.HasClaim); response.Components.Clear(); } @@ -126,8 +125,7 @@ private void CleanResponse(HealthEndpointResponse response, HealthEndpointOption if (ShouldClear(showDetails, healthRequest)) { - _logger.LogTrace("Clearing health check component details. ShowDetails={ShowDetails}, HasClaim={HasClaimForHealth}.", showDetails, - healthRequest.HasClaim); + LogClearingDetails(showDetails, healthRequest.HasClaim); foreach (HealthCheckResult component in response.Components.Values) { @@ -141,4 +139,12 @@ private static bool ShouldClear(ShowValues showValues, HealthEndpointRequest hea { return showValues == ShowValues.Never || (showValues == ShowValues.WhenAuthorized && !healthRequest.HasClaim); } + + [LoggerMessage(Level = LogLevel.Trace, + Message = "Clearing health check components because ShowComponents is {ShowComponents} and HasClaim is {HasClaimForHealth}.")] + private partial void LogClearingComponents(ShowValues showComponents, bool hasClaimForHealth); + + [LoggerMessage(Level = LogLevel.Trace, + Message = "Clearing health check component details because ShowDetails is {ShowDetails} and HasClaim is {HasClaimForHealth}.")] + private partial void LogClearingDetails(ShowValues showDetails, bool hasClaimForHealth); } diff --git a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs index 657e861364..7737c50b1a 100644 --- a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Health; -internal sealed class HealthEndpointMiddleware : EndpointMiddleware +internal sealed partial class HealthEndpointMiddleware : EndpointMiddleware { private readonly IOptionsMonitor _endpointOptionsMonitor; private readonly ILogger _logger; @@ -59,11 +59,11 @@ private string GetRequestedHealthGroup(PathString requestPath, HealthEndpointOpt if (requestComponents.Length > 0 && requestComponents[^1] != endpointOptions.Id) { - _logger.LogTrace("Found group '{HealthGroup}' in the request path.", requestComponents[^1]); + LogGroupFound(requestComponents[^1]); return requestComponents[^1]; } - _logger.LogTrace("Did not find a health group in the request path."); + LogNoGroupFound(); return string.Empty; } @@ -127,4 +127,10 @@ private static int GetStatusCode(HealthEndpointResponse response) { return response.Status is HealthStatus.Down or HealthStatus.OutOfService ? 503 : 200; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Found group '{HealthGroup}' in the request path.")] + private partial void LogGroupFound(string healthGroup); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Did not find a health group in the request path.")] + private partial void LogNoGroupFound(); } diff --git a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointHandler.cs index d6e8d19cad..1c8b1a70e8 100644 --- a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointHandler.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HeapDump; -internal sealed class HeapDumpEndpointHandler : IHeapDumpEndpointHandler +internal sealed partial class HeapDumpEndpointHandler : IHeapDumpEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IHeapDumper _heapDumper; @@ -29,8 +29,11 @@ public HeapDumpEndpointHandler(IOptionsMonitor optionsM public Task InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogTrace("Invoking the heap dumper"); + LogInvokingHeapDumper(); string filePath = _heapDumper.DumpHeapToFile(cancellationToken); return Task.FromResult(filePath); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Invoking the heap dumper.")] + private partial void LogInvokingHeapDumper(); } diff --git a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs index c03d1b552e..9fe78539db 100644 --- a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HeapDump; -internal sealed class HeapDumpEndpointMiddleware( +internal sealed partial class HeapDumpEndpointMiddleware( IHeapDumpEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { @@ -28,7 +28,7 @@ protected override async Task WriteResponseAsync(string? fileName, HttpContext h { ArgumentNullException.ThrowIfNull(httpContext); - _logger.LogDebug("Returning: {FileName}", fileName); + LogReturning(fileName); if (!File.Exists(fileName)) { @@ -51,4 +51,7 @@ protected override async Task WriteResponseAsync(string? fileName, HttpContext h File.Delete(fileName); } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Returning heap dump file '{FileName}'.")] + private partial void LogReturning(string? fileName); } diff --git a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumper.cs b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumper.cs index 06bbd0c286..6e21c41e2d 100644 --- a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumper.cs +++ b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumper.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HeapDump; -internal sealed class HeapDumper : IHeapDumper +internal sealed partial class HeapDumper : IHeapDumper { private readonly IOptionsMonitor _optionsMonitor; private readonly TimeProvider _timeProvider; @@ -43,7 +43,7 @@ public string DumpHeapToFile(CancellationToken cancellationToken) _ => "full dump" }; - _logger.LogInformation("Attempting to create a {DumpType}.", dumpDescription); + LogStart(dumpDescription); try { @@ -64,7 +64,7 @@ public string DumpHeapToFile(CancellationToken cancellationToken) throw; } - _logger.LogInformation("Successfully created a {DumpType}.", dumpDescription); + LogSucceeded(dumpDescription); return outputPath; } @@ -159,7 +159,7 @@ internal void CaptureLogOutput(Func action, string dumpDescrip throw new InvalidOperationException($"Failed to create a {dumpDescription}. Captured log:{System.Environment.NewLine}{logOutput}", error); } - _logger.LogTrace("Captured log from {DumpType}:{LineBreak}{DumpLog}", dumpDescription, System.Environment.NewLine, logOutput); + LogDumpLogCaptured(dumpDescription, System.Environment.NewLine, logOutput); } private static void SafeDelete(string? outputPath) @@ -179,4 +179,13 @@ private static void SafeDelete(string? outputPath) } } } + + [LoggerMessage(Level = LogLevel.Information, Message = "Attempting to create a {DumpType}.")] + private partial void LogStart(string dumpType); + + [LoggerMessage(Level = LogLevel.Information, Message = "Successfully created a {DumpType}.")] + private partial void LogSucceeded(string dumpType); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Captured log from {DumpType}:{LineBreak}{DumpLog}")] + private partial void LogDumpLogCaptured(string dumpType, string lineBreak, string dumpLog); } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticObserver.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticObserver.cs index e6cdf32e39..5a1ff13705 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticObserver.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticObserver.cs @@ -7,7 +7,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges.Diagnostics; -internal abstract class DiagnosticObserver : IObserver>, IDisposable +internal abstract partial class DiagnosticObserver : IObserver>, IDisposable { private readonly string _observerName; private readonly string _listenerName; @@ -38,7 +38,7 @@ protected virtual void Dispose(bool disposing) _subscription?.Dispose(); _subscription = null; - _logger.LogTrace("DiagnosticObserver {Observer} disposed", _observerName); + LogObserverDisposed(_observerName); } } @@ -54,7 +54,7 @@ public void Subscribe(DiagnosticListener listener) } _subscription = listener.Subscribe(this); - _logger.LogTrace("DiagnosticObserver {Observer} subscribed to {Listener}", _observerName, listener.Name); + LogObserverSubscribed(_observerName, listener.Name); } } @@ -76,9 +76,18 @@ public virtual void OnNext(KeyValuePair value) } catch (Exception exception) { - _logger.LogError(exception, "Failed to process event {Id}", value.Key); + LogFailedToProcessEvent(exception, value.Key); } } public abstract void ProcessEvent(string eventName, object? value); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Diagnostic observer {Observer} disposed.")] + private partial void LogObserverDisposed(string observer); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Diagnostic observer {Observer} subscribed to {Listener}.")] + private partial void LogObserverSubscribed(string observer, string listener); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to process event {Id}.")] + private partial void LogFailedToProcessEvent(Exception exception, string id); } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsManager.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsManager.cs index 7be3cc71a7..1b92b98b3c 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsManager.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsManager.cs @@ -7,7 +7,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges.Diagnostics; -internal sealed class DiagnosticsManager : IObserver, IDisposable +internal sealed partial class DiagnosticsManager : IObserver, IDisposable { private readonly ILogger _logger; private readonly List _observers; @@ -57,7 +57,7 @@ public void Start() if (_listenersSubscription != null) { - _logger.LogTrace("Subscribed to Diagnostic Listener"); + LogSubscribed(); } } } @@ -82,4 +82,7 @@ public void Dispose() _isDisposed = true; } } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Subscribed to diagnostic listener.")] + private partial void LogSubscribed(); } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsService.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsService.cs index 275d92dc4d..1e834e8bfa 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsService.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsService.cs @@ -7,7 +7,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges.Diagnostics; -internal sealed class DiagnosticsService : IHostedService +internal sealed partial class DiagnosticsService : IHostedService { private readonly ILogger _logger; private readonly DiagnosticsManager _observerManager; @@ -23,15 +23,21 @@ public DiagnosticsService(DiagnosticsManager observerManager, ILogger _optionsMonitor; private readonly HttpExchangesRepository _httpExchangesRepository; @@ -30,8 +30,11 @@ public HttpExchangesEndpointHandler(IOptionsMonitor InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogTrace("Fetching Http Exchanges"); + LogFetchingHttpExchanges(); HttpExchangesResult result = _httpExchangesRepository.GetHttpExchanges(); return Task.FromResult(result); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Fetching HTTP exchanges.")] + private partial void LogFetchingHttpExchanges(); } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesRepository.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesRepository.cs index 994ab4272e..63107c0121 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesRepository.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesRepository.cs @@ -9,7 +9,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; -internal sealed class HttpExchangesRepository +internal sealed partial class HttpExchangesRepository { private const string RedactedText = "******"; @@ -34,7 +34,7 @@ private void OnRecord(HttpExchange exchange) { ArgumentNullException.ThrowIfNull(exchange); - _logger.LogDebug("Incoming exchange for {Url}.", exchange.Request.Uri); + LogIncomingExchange(exchange.Request.Uri); _queue.Enqueue(exchange); if (_queue.Count > _optionsMonitor.CurrentValue.Capacity) @@ -104,4 +104,7 @@ private static bool HeaderShouldBeRedacted(string currentHeader, HashSet { return !options.IncludeTimeTaken ? null : timeTaken; } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Incoming exchange for {Url}.")] + private partial void LogIncomingExchange(Uri url); } diff --git a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs index ba0b3c7890..6e1df986b2 100644 --- a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs +++ b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs @@ -12,7 +12,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Hypermedia; -internal sealed class HypermediaService +internal sealed partial class HypermediaService { private readonly IOptionsMonitor _managementOptionsMonitor; private readonly EndpointOptions _endpointOptions; @@ -70,7 +70,7 @@ public Links Invoke(Uri baseUrl) return links; } - _logger.LogTrace("Processing hypermedia for {ManagementOptions}", managementOptions); + LogProcessingHypermedia(); Link? selfLink = null; bool skipExposureCheck = PermissionsProvider.IsCloudFoundryRequest(baseUrl.PathAndQuery); @@ -101,7 +101,7 @@ public Links Invoke(Uri baseUrl) { if (links.Entries.ContainsKey(endpointOptions.Id)) { - _logger.LogWarning("Duplicate endpoint with ID '{DuplicateEndpointId}' detected.", endpointOptions.Id); + LogDuplicateEndpoint(endpointOptions.Id); } else { @@ -129,4 +129,10 @@ private static Link CreateLink(Uri baseUrl, string? basePath, EndpointOptions en string href = builder.Uri.ToString(); return new Link(href, false); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Processing hypermedia.")] + private partial void LogProcessingHypermedia(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Duplicate endpoint with ID '{DuplicateEndpointId}' detected.")] + private partial void LogDuplicateEndpoint(string? duplicateEndpointId); } diff --git a/src/Management/src/Endpoint/Actuators/Info/Contributors/GitInfoContributor.cs b/src/Management/src/Endpoint/Actuators/Info/Contributors/GitInfoContributor.cs index 75c62dcfad..71e1a065de 100644 --- a/src/Management/src/Endpoint/Actuators/Info/Contributors/GitInfoContributor.cs +++ b/src/Management/src/Endpoint/Actuators/Info/Contributors/GitInfoContributor.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Info.Contributors; -internal sealed class GitInfoContributor : ConfigurationContributor, IInfoContributor +internal sealed partial class GitInfoContributor : ConfigurationContributor, IInfoContributor { private const string GitSettingsPrefix = "git"; private const string GitPropertiesFileName = "git.properties"; @@ -78,7 +78,7 @@ public async Task ContributeAsync(InfoBuilder builder, CancellationToken cancell } else { - _logger.LogWarning("File '{Path}' does not exist.", _propertiesPath); + LogFileNotFound(_propertiesPath); } return null; @@ -99,4 +99,7 @@ protected override void AddKeyValue(IDictionary dictionary, str dictionary[key] = valueToInsert; } + + [LoggerMessage(Level = LogLevel.Warning, Message = "File '{Path}' does not exist.")] + private partial void LogFileNotFound(string path); } diff --git a/src/Management/src/Endpoint/Actuators/Info/InfoEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Info/InfoEndpointHandler.cs index 8c1a65223b..38a942e86d 100644 --- a/src/Management/src/Endpoint/Actuators/Info/InfoEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Info/InfoEndpointHandler.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Info; -internal sealed class InfoEndpointHandler : IInfoEndpointHandler +internal sealed partial class InfoEndpointHandler : IInfoEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IInfoContributor[] _contributors; @@ -44,11 +44,13 @@ public InfoEndpointHandler(IOptionsMonitor optionsMonitor, } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogWarning(exception, "Exception thrown by contributor '{ContributorTypeName}' while contributing to info endpoint.", - contributor.GetType()); + LogContributorError(exception, contributor.GetType()); } } return builder.Build(); } + + [LoggerMessage(Level = LogLevel.Warning, Message = "Exception thrown by contributor '{ContributorTypeName}' while contributing to info endpoint.")] + private partial void LogContributorError(Exception exception, Type contributorTypeName); } diff --git a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointHandler.cs index d0412ee6d2..c0afe42906 100644 --- a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointHandler.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Loggers; -internal sealed class LoggersEndpointHandler : ILoggersEndpointHandler +internal sealed partial class LoggersEndpointHandler : ILoggersEndpointHandler { private const string SpringDefaultCategoryName = "Default"; @@ -48,7 +48,7 @@ public LoggersEndpointHandler(IOptionsMonitor optionsMon { ArgumentNullException.ThrowIfNull(request); - _logger.LogDebug("Invoke({Request})", SecurityUtilities.SanitizeInput(request.ToString())); + LogEntering(SecurityUtilities.SanitizeInput(request.ToString())); LoggersResponse? response; @@ -72,7 +72,7 @@ private LoggersResponse GetLogLevels() foreach (DynamicLoggerState loggerState in loggerStates.OrderBy(entry => entry.CategoryName)) { - _logger.LogTrace("Adding {LoggerState}", loggerState); + LogAddingLoggerState(loggerState); string categoryName = loggerState.CategoryName.Length == 0 ? SpringDefaultCategoryName : loggerState.CategoryName; var levels = new LoggerLevels(loggerState.BackupMinLevel, loggerState.EffectiveMinLevel); @@ -89,4 +89,10 @@ private void SetLogLevel(string name, string? level) _dynamicLoggerProvider.SetLogLevel(categoryName, logLevel); } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Invoking loggers endpoint handler with request {Request}.")] + private partial void LogEntering(string request); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Adding state {LoggerState}.")] + private partial void LogAddingLoggerState(DynamicLoggerState loggerState); } diff --git a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs index a29774a5ae..30427ca776 100644 --- a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Loggers; -internal sealed class LoggersEndpointMiddleware( +internal sealed partial class LoggersEndpointMiddleware( ILoggersEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { @@ -37,13 +37,13 @@ internal sealed class LoggersEndpointMiddleware( change.TryGetValue("configuredLevel", out string? level); - _logger.LogDebug("Change Request: {Name}, {Level}", loggerName, level ?? "RESET"); + LogChangeRequest(loggerName, level ?? "RESET"); if (!string.IsNullOrEmpty(loggerName)) { if (!string.IsNullOrEmpty(level) && LoggerLevels.StringToLogLevel(level) == null) { - _logger.LogDebug("Invalid LogLevel specified: {Level}", level); + LogInvalidLevel(level); return null; } @@ -68,7 +68,7 @@ private async Task> DeserializeRequestAsync(Stream st } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogError(exception, "Exception deserializing loggers endpoint request."); + LogDeserializationFailed(exception); } return []; @@ -99,4 +99,13 @@ protected override async Task WriteResponseAsync(LoggersResponse? response, Http await JsonSerializer.SerializeAsync(httpContext.Response.Body, response, options, cancellationToken); } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Received request to change logger '{Name}' to level {Level}.")] + private partial void LogChangeRequest(string name, string level); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Invalid log level {Level} specified.")] + private partial void LogInvalidLevel(string level); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to deserialize loggers endpoint request.")] + private partial void LogDeserializationFailed(Exception exception); } diff --git a/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointHandler.cs index 47b6fe4517..3b6a909399 100644 --- a/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointHandler.cs @@ -9,7 +9,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Refresh; -internal sealed class RefreshEndpointHandler : IRefreshEndpointHandler +internal sealed partial class RefreshEndpointHandler : IRefreshEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IConfiguration _configuration; @@ -30,7 +30,7 @@ public RefreshEndpointHandler(IOptionsMonitor optionsMon public Task> InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogInformation("Refreshing Configuration"); + LogRefreshingConfiguration(); if (_configuration is not IConfigurationRoot root) { @@ -51,4 +51,7 @@ public Task> InvokeAsync(object? argument, CancellationToken cance return Task.FromResult>(keys.ToList()); } + + [LoggerMessage(Level = LogLevel.Information, Message = "Refreshing configuration.")] + private partial void LogRefreshingConfiguration(); } diff --git a/src/Management/src/Endpoint/Actuators/RouteMappings/AspNetEndpointProvider.cs b/src/Management/src/Endpoint/Actuators/RouteMappings/AspNetEndpointProvider.cs index 39c995b7a9..018ac42330 100644 --- a/src/Management/src/Endpoint/Actuators/RouteMappings/AspNetEndpointProvider.cs +++ b/src/Management/src/Endpoint/Actuators/RouteMappings/AspNetEndpointProvider.cs @@ -28,7 +28,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.RouteMappings; /// /// Gathers endpoints in an ASP.NET Core application by combining information from various sources. /// -internal sealed class AspNetEndpointProvider +internal sealed partial class AspNetEndpointProvider { private static readonly MethodInfo? ProducesContentTypesPropertyGetter = typeof(ProducesResponseTypeAttribute).GetProperty("ContentTypes", BindingFlags.Instance | BindingFlags.NonPublic)?.GetMethod; @@ -60,7 +60,7 @@ public IList GetEndpoints(bool includeActuators) { if (!_mvcOptionsMonitor.CurrentValue.EnableEndpointRouting) { - _logger.LogWarning("Conventional routing is not supported."); + LogConventionalRoutingNotSupported(); return []; } @@ -436,4 +436,7 @@ private static IEnumerable ExtractProducedContentTypes(EndpointMetadataC yield return contentType; } } + + [LoggerMessage(Level = LogLevel.Warning, Message = "Conventional routing is not supported.")] + private partial void LogConventionalRoutingNotSupported(); } diff --git a/src/Management/src/Endpoint/Actuators/ThreadDump/EventPipeThreadDumper.cs b/src/Management/src/Endpoint/Actuators/ThreadDump/EventPipeThreadDumper.cs index 5163eef70d..80e71fdc00 100644 --- a/src/Management/src/Endpoint/Actuators/ThreadDump/EventPipeThreadDumper.cs +++ b/src/Management/src/Endpoint/Actuators/ThreadDump/EventPipeThreadDumper.cs @@ -18,7 +18,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.ThreadDump; /// /// Thread dumper that uses the EventPipe to acquire the call stacks of all the running threads. /// -internal sealed class EventPipeThreadDumper : IThreadDumper +internal sealed partial class EventPipeThreadDumper : IThreadDumper { private const string ThreadIdTemplate = "Thread ("; @@ -63,7 +63,7 @@ public async Task> DumpThreadsAsync(CancellationToken cancella { try { - _logger.LogInformation("Attempting to create a thread dump."); + LogStart(); var client = new DiagnosticsClient(System.Environment.ProcessId); List providers = [new("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational)]; @@ -72,16 +72,16 @@ public async Task> DumpThreadsAsync(CancellationToken cancella using EventPipeSession session = client.StartEventPipeSession(providers); List threads = await GetThreadsFromEventPipeSessionAsync(session, logWriter, cancellationToken); - _logger.LogInformation("Successfully created a thread dump."); + LogSucceeded(); return threads; } finally { #pragma warning disable S1215 // "GC.Collect" should not be called - long totalMemory = GC.GetTotalMemory(true); + long memoryInBytes = GC.GetTotalMemory(true); #pragma warning restore S1215 // "GC.Collect" should not be called - _logger.LogDebug("Total memory: {Memory}.", totalMemory); + LogTotalMemory(memoryInBytes); } }, cancellationToken); } @@ -128,7 +128,7 @@ internal async Task CaptureLogOutputAsync(Func> GetThreadsFromEventPipeSessionAsync(EventPi List results = ReadStackSource(stackSource, symbolReader, logWriter).ToList(); - _logger.LogTrace("Finished thread walk, found {Count} results.", results.Count); + LogThreadWalkFinished(results.Count); return results; } finally @@ -183,9 +183,7 @@ private async Task CreateTraceFileAsync(EventPipeSession session, Cancel } catch (TimeoutException) when (!cancellationToken.IsCancellationRequested) { -#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. - _logger.LogInformation("Sufficiently large applications can cause this command to take non-trivial amounts of time."); -#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + LogPossiblySlow(); throw; } @@ -367,6 +365,24 @@ private static void SetThreadState(ThreadInfo threadInfo) : State.Runnable; } + [LoggerMessage(Level = LogLevel.Information, Message = "Attempting to create a thread dump.")] + private partial void LogStart(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Successfully created a thread dump.")] + private partial void LogSucceeded(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Total memory is {MemoryInBytes} bytes.")] + private partial void LogTotalMemory(long memoryInBytes); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Captured log from thread dump:{LineBreak}{DumpLog}")] + private partial void LogDumpLogCaptured(string lineBreak, string? dumpLog); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Finished thread walk, found {Count} results.")] + private partial void LogThreadWalkFinished(int count); + + [LoggerMessage(Level = LogLevel.Information, Message = "Sufficiently large applications can cause this command to take non-trivial amounts of time.")] + private partial void LogPossiblySlow(); + private sealed record StackFrameSymbol(string AssemblyName, string TypeName, string MemberName, string Parameters) { public static bool TryParse(string frameName, [NotNullWhen(true)] out StackFrameSymbol? symbol) diff --git a/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointHandler.cs index 597f545f9b..87bde9475f 100644 --- a/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointHandler.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.ThreadDump; -internal sealed class ThreadDumpEndpointHandler : IThreadDumpEndpointHandler +internal sealed partial class ThreadDumpEndpointHandler : IThreadDumpEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IThreadDumper _threadDumper; @@ -29,7 +29,10 @@ public ThreadDumpEndpointHandler(IOptionsMonitor opti public async Task> InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogTrace("Invoking ThreadDumper"); + LogInvokingThreadDumper(); return await _threadDumper.DumpThreadsAsync(cancellationToken); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Invoking thread dumper.")] + private partial void LogInvokingThreadDumper(); } diff --git a/src/Management/src/Endpoint/ManagementPort/ManagementPortMiddleware.cs b/src/Management/src/Endpoint/ManagementPort/ManagementPortMiddleware.cs index 761727907f..6ba88fb103 100644 --- a/src/Management/src/Endpoint/ManagementPort/ManagementPortMiddleware.cs +++ b/src/Management/src/Endpoint/ManagementPort/ManagementPortMiddleware.cs @@ -14,7 +14,7 @@ namespace Steeltoe.Management.Endpoint.ManagementPort; /// /// Blocks access to actuator endpoints on ports other than the management port. Blocks access to non-actuator endpoints on the management port. /// -internal sealed class ManagementPortMiddleware +internal sealed partial class ManagementPortMiddleware { private readonly IOptionsMonitor _managementOptionsMonitor; private readonly RequestDelegate? _next; @@ -36,7 +36,7 @@ public async Task InvokeAsync(HttpContext context) ArgumentNullException.ThrowIfNull(context); ManagementOptions managementOptions = _managementOptionsMonitor.CurrentValue; - _logger.LogDebug("InvokeAsync({RequestPath}), OptionsPath: {OptionsPath}", context.Request.Path.Value, managementOptions.Path); + LogEntering(context.Request.Path.Value, managementOptions.Path); bool allowRequest = IsRequestAllowed(context.Request, managementOptions); @@ -81,12 +81,7 @@ private bool HasMappedInstancePort(int managementPort, int? requestPort) if (portMapping != null) { - if (_logger.IsEnabled(LogLevel.Trace)) - { - _logger.LogTrace( - "Request received on port {RequestPort}. Allowed by CF_INSTANCE_PORTS mapping: [ Internal: {InternalPort}, ExternalTlsProxy: {ExternalTlsProxy}, InternalTlsProxy: {InternalTlsProxy} ]", - requestPort, portMapping.Internal, portMapping.ExternalTlsProxy, portMapping.InternalTlsProxy); - } + LogPortMappingAllowed(requestPort, portMapping.Internal, portMapping.ExternalTlsProxy, portMapping.InternalTlsProxy); return true; } @@ -104,12 +99,23 @@ private void SetResponseError(HttpContext context, int managementPort) defaultPort = context.Request.Scheme == "http" ? 80 : 443; } - _logger.LogWarning("Access to {Path} on port {Port} denied because 'Management:Endpoints:Port' is set to {ManagementPort}.", context.Request.Path, - defaultPort ?? context.Request.Host.Port, managementPort); + LogAccessDenied(context.Request.Path, defaultPort ?? context.Request.Host.Port, managementPort); context.Response.StatusCode = StatusCodes.Status404NotFound; } + [LoggerMessage(Level = LogLevel.Debug, Message = "Handling request at path {RequestPath} with options path {OptionsPath}.")] + private partial void LogEntering(string? requestPath, string? optionsPath); + + [LoggerMessage(Level = LogLevel.Trace, + Message = + "Request received on port {RequestPort}, allowed by CF_INSTANCE_PORTS mapping with internal port {InternalPort}, external TLS proxy {ExternalTlsProxy} and internal TLS proxy {InternalTlsProxy}.")] + private partial void LogPortMappingAllowed(int? requestPort, int? internalPort, int? externalTlsProxy, int? internalTlsProxy); + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Access to {Path} on port {Port} denied because 'Management:Endpoints:Port' is set to {ManagementPort}.")] + private partial void LogAccessDenied(PathString path, int? port, int managementPort); + private sealed record PortMapping { [JsonPropertyName("internal")] diff --git a/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs b/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs index 462fd375a5..6a1c54177c 100644 --- a/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Management.Endpoint.Middleware; -public abstract class EndpointMiddleware : IEndpointMiddleware +public abstract partial class EndpointMiddleware : IEndpointMiddleware { private readonly ILogger _logger; protected IOptionsMonitor ManagementOptionsMonitor { get; } @@ -57,25 +57,24 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate? next) { if (!allowedVerbs.Contains(context.Request.Method)) { - _logger.LogTrace("{Method} method is unavailable at path {Path}.", context.Request.Method, context.Request.Path.Value); + LogUnavailableHttpMethod(context.Request.Method, context.Request.Path.Value); context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; } else if (!IsValidContentType(context.Request)) { - _logger.LogDebug("Content-Type header '{RequestContentType}' is not supported for this request.", context.Request.ContentType); + LogUnsupportedContentType(context.Request.ContentType); context.Response.StatusCode = (int)HttpStatusCode.UnsupportedMediaType; await context.Response.WriteAsync($"Only the '{ContentType}' content type is supported.", context.RequestAborted); } else if (!IsCompatibleAcceptHeader(context.Request)) { - _logger.LogDebug("Accept header '{AcceptType}' is not supported for this request.", context.Request.Headers.Accept.ToString()); + LogUnsupportedAcceptHeader(context.Request.Headers.Accept.ToString()); context.Response.StatusCode = (int)HttpStatusCode.NotAcceptable; await context.Response.WriteAsync($"Only the '{ContentType}' content type is supported.", context.RequestAborted); } else { - _logger.LogDebug("Reading {Method} request at path {Path} using {MiddlewareType}.", context.Request.Method, context.Request.Path.Value, - GetType()); + LogReadingRequest(context.Request.Method, context.Request.Path.Value, GetType()); TRequest? request = await ParseRequestAsync(context, context.RequestAborted); TResponse response = await InvokeEndpointHandlerAsync(request, context.RequestAborted); @@ -87,7 +86,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate? next) } else { - _logger.LogTrace("CanInvoke returned false for {Method} request at path {Path}.", context.Request.Method, context.Request.Path.Value); + LogInvokeDenied(context.Request.Method, context.Request.Path.Value); } context.Response.StatusCode = (int)HttpStatusCode.NotFound; @@ -147,4 +146,19 @@ protected virtual async Task WriteResponseAsync(TResponse response, HttpContext JsonSerializerOptions options = ManagementOptionsMonitor.CurrentValue.SerializerOptions; await JsonSerializer.SerializeAsync(httpContext.Response.Body, response, options, cancellationToken); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "{Method} method is unavailable at path {Path}.")] + private partial void LogUnavailableHttpMethod(string method, string? path); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Content-Type header '{RequestContentType}' is not supported for this request.")] + private partial void LogUnsupportedContentType(string? requestContentType); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Accept header '{AcceptType}' is not supported for this request.")] + private partial void LogUnsupportedAcceptHeader(string acceptType); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Reading {Method} request at path {Path} using {MiddlewareType}.")] + private partial void LogReadingRequest(string method, string? path, Type middlewareType); + + [LoggerMessage(Level = LogLevel.Trace, Message = "CanInvoke returned false for {Method} request at path {Path}.")] + private partial void LogInvokeDenied(string method, string? path); } diff --git a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs index 40139c7c9b..6426993f12 100644 --- a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs +++ b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.SpringBootAdminClient; -internal sealed class SpringBootAdminPeriodicRefresh : IAsyncDisposable +internal sealed partial class SpringBootAdminPeriodicRefresh : IAsyncDisposable { private readonly SpringBootAdminRefreshRunner _runner; private readonly ILogger _logger; @@ -39,12 +39,12 @@ private async Task TimerLoopAsync(TimeSpan interval) { try { - _logger.LogDebug("Starting periodic refresh loop with interval {Interval}.", interval); + LogStartingPeriodicRefreshLoop(interval); bool isFirstTime = true; do { - _logger.LogDebug("Starting refresh cycle."); + LogStartingRefreshCycle(); try { @@ -52,7 +52,7 @@ private async Task TimerLoopAsync(TimeSpan interval) } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogWarning(exception, "Refresh cycle failed."); + LogRefreshCycleFailed(exception); } isFirstTime = false; @@ -61,10 +61,7 @@ private async Task TimerLoopAsync(TimeSpan interval) } catch (OperationCanceledException) { -#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. - // Justification: The exception contains no useful information. Logging it suggests something crashed, while this is expected behavior. - _logger.LogDebug("Stopped periodic refresh loop."); -#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + LogPeriodicRefreshLoopStopped(); } } @@ -75,7 +72,7 @@ private void ChangeInterval(TimeSpan interval) if (safeInterval != _periodicTimer.Period) { _periodicTimer.Period = safeInterval; - _logger.LogDebug("Refresh interval changed to {Interval}.", safeInterval); + LogRefreshIntervalChanged(safeInterval); } } @@ -87,10 +84,10 @@ private static TimeSpan InfiniteWhenZero(TimeSpan interval) public async Task StopAsync(CancellationToken cancellationToken) { - _logger.LogDebug("Signaling to stop periodic refresh loop."); + LogSignalingStop(); await DisposeAsync(); - _logger.LogDebug("Starting cleanup."); + LogStartingCleanup(); await _runner.CleanupAsync(cancellationToken); } @@ -107,4 +104,25 @@ public async ValueTask DisposeAsync() _periodicTimer.Dispose(); } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Starting periodic refresh loop with interval {Interval}.")] + private partial void LogStartingPeriodicRefreshLoop(TimeSpan interval); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Starting refresh cycle.")] + private partial void LogStartingRefreshCycle(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Refresh cycle failed.")] + private partial void LogRefreshCycleFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Stopped periodic refresh loop.")] + private partial void LogPeriodicRefreshLoopStopped(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Refresh interval changed to {Interval}.")] + private partial void LogRefreshIntervalChanged(TimeSpan interval); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Signaling to stop periodic refresh loop.")] + private partial void LogSignalingStop(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Starting cleanup.")] + private partial void LogStartingCleanup(); } diff --git a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminRefreshRunner.cs b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminRefreshRunner.cs index eaf7784216..fa5d7ba2d0 100644 --- a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminRefreshRunner.cs +++ b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminRefreshRunner.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Management.Endpoint.SpringBootAdminClient; -internal sealed class SpringBootAdminRefreshRunner +internal sealed partial class SpringBootAdminRefreshRunner { private readonly AppUrlCalculator _appUrlCalculator; private readonly SpringBootAdminApiClient _springBootAdminApiClient; @@ -56,13 +56,13 @@ public SpringBootAdminRefreshRunner(AppUrlCalculator appUrlCalculator, SpringBoo public async Task RunAsync(bool isFirstTime, CancellationToken cancellationToken) { - _logger.LogDebug("Validating options."); + LogValidatingOptions(); SpringBootAdminClientOptions clientOptions = _clientOptionsMonitor.CurrentValue; ValidateAndSetOptions(clientOptions); if (_lastGoodOptions?.Url != null && !string.Equals(_lastGoodOptions.Url, clientOptions.Url, StringComparison.OrdinalIgnoreCase)) { - _logger.LogDebug("Spring Boot Admin Server URL changed from {LastUrl} to {NewUrl}, unregistering first.", _lastGoodOptions.Url, clientOptions.Url); + LogUrlChanged(_lastGoodOptions.Url, clientOptions.Url); await SafeUnregisterAsync(_lastGoodOptions, cancellationToken); } @@ -130,11 +130,11 @@ private async Task RegisterAsync(SpringBootAdminClientOptions clientOptions, boo if (isFirstTime) { - _logger.LogInformation("Registering with Spring Boot Admin Server at {Url}.", clientOptions.Url); + LogRegisteringFirstTime(clientOptions.Url); } else { - _logger.LogDebug("Registering with Spring Boot Admin Server at {Url}.", clientOptions.Url); + LogRegisteringNotFirstTime(clientOptions.Url); } _lastRegistrationId = await _springBootAdminApiClient.RegisterAsync(app, clientOptions, cancellationToken); @@ -183,7 +183,7 @@ private async Task SafeUnregisterAsync(SpringBootAdminClientOptions clientOption { try { - _logger.LogDebug("Unregistering from Spring Boot Admin Server at {Url}.", clientOptions.Url); + LogUnregistering(clientOptions.Url); await _springBootAdminApiClient.UnregisterAsync(_lastRegistrationId, clientOptions, cancellationToken); _lastRegistrationId = null; } @@ -191,9 +191,27 @@ private async Task SafeUnregisterAsync(SpringBootAdminClientOptions clientOption { if (!exception.IsCancellation()) { - _logger.LogWarning(exception, "Failed to unregister from Spring Boot Admin server at {Url}.", clientOptions.Url); + LogUnregisterFailed(exception, clientOptions.Url); } } } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Validating options.")] + private partial void LogValidatingOptions(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Spring Boot Admin Server URL changed from {LastUrl} to {NewUrl}, unregistering first.")] + private partial void LogUrlChanged(string? lastUrl, string? newUrl); + + [LoggerMessage(Level = LogLevel.Information, Message = "Registering with Spring Boot Admin Server at {Url}.")] + private partial void LogRegisteringFirstTime(string? url); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Registering with Spring Boot Admin Server at {Url}.")] + private partial void LogRegisteringNotFirstTime(string? url); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Unregistering from Spring Boot Admin Server at {Url}.")] + private partial void LogUnregistering(string? url); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to unregister from Spring Boot Admin server at {Url}.")] + private partial void LogUnregisterFailed(Exception exception, string? url); } diff --git a/src/Management/src/Prometheus/PrometheusExtensions.cs b/src/Management/src/Prometheus/PrometheusExtensions.cs index 7d59c42236..f030566994 100644 --- a/src/Management/src/Prometheus/PrometheusExtensions.cs +++ b/src/Management/src/Prometheus/PrometheusExtensions.cs @@ -17,7 +17,7 @@ namespace Steeltoe.Management.Prometheus; -public static class PrometheusExtensions +public static partial class PrometheusExtensions { /// /// Adds the services used by the Steeltoe-configured OpenTelemetry Prometheus exporter and configures the ASP.NET Core middleware pipeline. @@ -30,7 +30,7 @@ public static class PrometheusExtensions /// public static IServiceCollection AddPrometheusActuator(this IServiceCollection services) { - return AddPrometheusActuator(services, true, null); + return services.AddPrometheusActuator(true, null); } /// @@ -48,7 +48,7 @@ public static IServiceCollection AddPrometheusActuator(this IServiceCollection s /// public static IServiceCollection AddPrometheusActuator(this IServiceCollection services, bool configureMiddleware) { - return AddPrometheusActuator(services, configureMiddleware, null); + return services.AddPrometheusActuator(configureMiddleware, null); } /// @@ -105,7 +105,7 @@ public static IServiceCollection AddPrometheusActuator(this IServiceCollection s /// public static IApplicationBuilder UsePrometheusActuator(this IApplicationBuilder builder) { - return UsePrometheusActuator(builder, null); + return builder.UsePrometheusActuator(null); } /// @@ -140,7 +140,7 @@ public static IApplicationBuilder UsePrometheusActuator(this IApplicationBuilder if (permissionsProvider is null) { - logger.LogWarning("The Cloud Foundry Actuator is required in order to run the Prometheus exporter under the Cloud Foundry context."); + LogCloudFoundryActuatorRequired(logger); } else { @@ -154,13 +154,12 @@ public static IApplicationBuilder UsePrometheusActuator(this IApplicationBuilder if (applyActuatorConventions && !isEndpointRoutingEnabled) { - logger.LogWarning("Customizing endpoints is only supported when using endpoint routing."); + LogEndpointRoutingRequired(logger); } if (managementOptions.Port == 0 && !applyActuatorConventions && configurePrometheusPipeline is null) { - logger.LogWarning( - "The Prometheus endpoint may not be configured securely. Consider using a dedicated management port, adding actuator conventions or configuring the Prometheus middleware pipeline."); + LogPrometheusEndpointNotSecure(logger); } builder.UseOpenTelemetryPrometheusScrapingEndpoint(null, null, endpointPath, ConfigureBranchedPipeline, null); @@ -195,4 +194,16 @@ void ConfigureBranchedPipeline(IApplicationBuilder branchedApplicationBuilder) } } } + + [LoggerMessage(Level = LogLevel.Warning, + Message = "The Cloud Foundry Actuator is required in order to run the Prometheus exporter under the Cloud Foundry context.")] + private static partial void LogCloudFoundryActuatorRequired(ILogger logger); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Customizing endpoints is only supported when using endpoint routing.")] + private static partial void LogEndpointRoutingRequired(ILogger logger); + + [LoggerMessage(Level = LogLevel.Warning, + Message = "The Prometheus endpoint may not be configured securely. " + + "Consider using a dedicated management port, adding actuator conventions or configuring the Prometheus middleware pipeline.")] + private static partial void LogPrometheusEndpointNotSecure(ILogger logger); } diff --git a/src/Management/src/Tasks/TaskHostExtensions.cs b/src/Management/src/Tasks/TaskHostExtensions.cs index 766f2e033e..c52dee9702 100644 --- a/src/Management/src/Tasks/TaskHostExtensions.cs +++ b/src/Management/src/Tasks/TaskHostExtensions.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Tasks; -public static class TaskHostExtensions +public static partial class TaskHostExtensions { /// /// Indicates whether will run an application task, instead of the regular application. @@ -132,7 +132,7 @@ private static async Task RunTaskAsync(string taskName, IServiceProvider service { var loggerFactory = serviceProvider.GetRequiredService(); ILogger logger = loggerFactory.CreateLogger($"{typeof(TaskHostExtensions).Namespace}.CloudFoundryTasks"); - logger.LogError("No task with name '{TaskName}' is registered in the service container.", taskName); + LogTaskNotFound(logger, taskName); } } @@ -147,4 +147,7 @@ private static async Task DisposeHostAsync(IDisposable host) host.Dispose(); } } + + [LoggerMessage(Level = LogLevel.Error, Message = "No task with name '{TaskName}' is registered in the service container.")] + private static partial void LogTaskNotFound(ILogger logger, string taskName); } diff --git a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTestScenarios.cs b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTestScenarios.cs index 133ee605a7..7b7419395a 100644 --- a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTestScenarios.cs +++ b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTestScenarios.cs @@ -13,78 +13,81 @@ internal sealed class CloudFoundrySecurityMiddlewareTestScenarios : TheoryData _logger; private ApplicationInstanceCertificate? _applicationInstanceCertificate; @@ -48,8 +48,7 @@ private void OnCertificateRefresh(CertificateOptions certificateOptions) } else { - _logger.LogError("Identity certificate did not match an expected pattern. Subject was: {CertificateSubject}", - certificateOptions.Certificate.Subject); + LogIdentityCertificateMismatch(certificateOptions.Certificate.Subject); } } @@ -75,8 +74,14 @@ private void HandleCertificateAuthorizationRequirement(AuthorizationHandlerCo } else { - _logger.LogDebug("User has the required claim, but the value doesn't match. Expected {ExpectedClaimValue} but got {ActualClaimValue}", claimValue, - context.User.FindFirstValue(claimType)); + LogClaimValueMismatch(claimValue, context.User.FindFirstValue(claimType)); } } + + [LoggerMessage(Level = LogLevel.Error, Message = "Identity certificate did not match an expected pattern. Subject was '{CertificateSubject}'.")] + private partial void LogIdentityCertificateMismatch(string certificateSubject); + + [LoggerMessage(Level = LogLevel.Debug, + Message = "User has the required claim, but the value doesn't match. Expected '{ExpectedClaimValue}' but got '{ActualClaimValue}'.")] + private partial void LogClaimValueMismatch(string expectedClaimValue, string? actualClaimValue); } diff --git a/src/Security/src/Authorization.Certificate/CertificateHttpClientBuilderExtensions.cs b/src/Security/src/Authorization.Certificate/CertificateHttpClientBuilderExtensions.cs index d9b497519b..ba2f19982f 100644 --- a/src/Security/src/Authorization.Certificate/CertificateHttpClientBuilderExtensions.cs +++ b/src/Security/src/Authorization.Certificate/CertificateHttpClientBuilderExtensions.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Security.Authorization.Certificate; -public static class CertificateHttpClientBuilderExtensions +public static partial class CertificateHttpClientBuilderExtensions { /// /// Binds certificate paths in configuration to representing the application instance and attaches the certificate to @@ -94,18 +94,24 @@ public static IHttpClientBuilder AddClientCertificate(this IHttpClientBuilder bu if (certificate != null) { - logger.LogTrace("Adding certificate with subject {CertificateSubject} to outbound requests in header {CertificateHeaderName}", - certificate.Subject, certificateHeaderName); + LogAddingCertificate(logger, certificate.Subject, certificateHeaderName); string b64 = Convert.ToBase64String(certificate.Export(X509ContentType.Cert)); client.DefaultRequestHeaders.Add(certificateHeaderName, b64); } else { - logger.LogError("Failed to find a certificate under the name {CertificateOptionsName}", certificateName); + LogCertificateNotFound(logger, certificateName); } }); return builder; } + + [LoggerMessage(Level = LogLevel.Trace, + Message = "Adding certificate with subject '{CertificateSubject}' to outbound requests in header {CertificateHeaderName}.")] + private static partial void LogAddingCertificate(ILogger logger, string certificateSubject, string certificateHeaderName); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to find a certificate under the name {CertificateOptionsName}.")] + private static partial void LogCertificateNotFound(ILogger logger, string certificateOptionsName); } diff --git a/src/Security/src/Authorization.Certificate/PostConfigureCertificateAuthenticationOptions.cs b/src/Security/src/Authorization.Certificate/PostConfigureCertificateAuthenticationOptions.cs index 9540636576..2085265d8c 100644 --- a/src/Security/src/Authorization.Certificate/PostConfigureCertificateAuthenticationOptions.cs +++ b/src/Security/src/Authorization.Certificate/PostConfigureCertificateAuthenticationOptions.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Security.Authorization.Certificate; -internal sealed class PostConfigureCertificateAuthenticationOptions : IPostConfigureOptions +internal sealed partial class PostConfigureCertificateAuthenticationOptions : IPostConfigureOptions { private readonly IOptionsMonitor _certificateOptionsMonitor; private readonly ILogger _logger; @@ -77,8 +77,7 @@ public void PostConfigure(string? name, CertificateAuthenticationOptions options } else { - _logger.LogError("Identity certificate did not match an expected pattern. Subject was: {CertificateSubject}", - context.ClientCertificate.Subject); + LogIdentityCertificateMismatch(context.ClientCertificate.Subject); } var identity = new ClaimsIdentity(claims, CertificateAuthenticationDefaults.AuthenticationScheme); @@ -89,4 +88,7 @@ public void PostConfigure(string? name, CertificateAuthenticationOptions options } }; } + + [LoggerMessage(Level = LogLevel.Error, Message = "Identity certificate did not match an expected pattern. Subject was '{CertificateSubject}'.")] + private partial void LogIdentityCertificateMismatch(string certificateSubject); } From 729df57b36ec17bb4d68533f612259af36ef4ce1 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:08:37 +0100 Subject: [PATCH 2/5] Use regex source generator, optimize flags --- .../ConfigureCertificateOptions.cs | 11 +++++--- .../ConfigurationKeyConverter.cs | 8 +++--- src/Common/src/Common/Net/InetUtils.cs | 7 +++-- .../CloudFoundryPostProcessor.cs | 18 ++++++++----- .../DecryptionConfigurationProvider.cs | 5 +++- .../ApplicationInstanceCertificate.cs | 26 ++++++++++--------- 6 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/Common/src/Certificates/ConfigureCertificateOptions.cs b/src/Common/src/Certificates/ConfigureCertificateOptions.cs index 3a83066e04..e65c5bd924 100644 --- a/src/Common/src/Certificates/ConfigureCertificateOptions.cs +++ b/src/Common/src/Certificates/ConfigureCertificateOptions.cs @@ -10,10 +10,9 @@ namespace Steeltoe.Common.Certificates; -internal sealed class ConfigureCertificateOptions : IConfigureNamedOptions +internal sealed partial class ConfigureCertificateOptions : IConfigureNamedOptions { - private static readonly Regex CertificateRegex = new("-+BEGIN CERTIFICATE-+.+?-+END CERTIFICATE-+", RegexOptions.Compiled | RegexOptions.Singleline, - TimeSpan.FromSeconds(1)); + private const int RegexMatchTimeoutInMilliseconds = 1_000; private readonly IConfiguration _configuration; @@ -24,6 +23,10 @@ public ConfigureCertificateOptions(IConfiguration configuration) _configuration = configuration; } + [GeneratedRegex("-+BEGIN CERTIFICATE-+.+?-+END CERTIFICATE-+", RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, + RegexMatchTimeoutInMilliseconds)] + private static partial Regex CertificateRegex(); + public void Configure(CertificateOptions options) { Configure(Options.DefaultName, options); @@ -47,7 +50,7 @@ public void Configure(string? name, CertificateOptions options) ? X509Certificate2.CreateFromPemFile(certificateFilePath, privateKeyFilePath) : new X509Certificate2(certificateFilePath); - X509Certificate2[] certificateChain = CertificateRegex.Matches(File.ReadAllText(certificateFilePath)) + X509Certificate2[] certificateChain = CertificateRegex().Matches(File.ReadAllText(certificateFilePath)) .Select(x => new X509Certificate2(Encoding.ASCII.GetBytes(x.Value))).ToArray(); #pragma warning restore SYSLIB0057 // Type or member is obsolete diff --git a/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs b/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs index 8b3b9cd4fd..97a88a65e9 100644 --- a/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs +++ b/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs @@ -8,15 +8,17 @@ namespace Steeltoe.Common.Configuration; -internal static class ConfigurationKeyConverter +internal static partial class ConfigurationKeyConverter { private const string DotDelimiterString = "."; private const char DotDelimiterChar = '.'; private const char UnderscoreDelimiterChar = '_'; private const char EscapeChar = '\\'; private const string EscapeString = "\\"; + private const int RegexMatchTimeoutInMilliseconds = 1_000; - private static readonly Regex ArrayRegex = new(@"\[(?\d+)\]", RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); + [GeneratedRegex(@"\[(?\d+)\]", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, RegexMatchTimeoutInMilliseconds)] + private static partial Regex ArrayRegex(); public static string AsDotNetConfigurationKey(string key) { @@ -82,6 +84,6 @@ static string UnEscapeString(string src) private static string ConvertArrayKey(string key) { - return ArrayRegex.Replace(key, ":${digits}"); + return ArrayRegex().Replace(key, ":${digits}"); } } diff --git a/src/Common/src/Common/Net/InetUtils.cs b/src/Common/src/Common/Net/InetUtils.cs index 939aa19dd2..47bf5c0b79 100644 --- a/src/Common/src/Common/Net/InetUtils.cs +++ b/src/Common/src/Common/Net/InetUtils.cs @@ -15,6 +15,9 @@ namespace Steeltoe.Common.Net; // Non-sealed because this type is mocked by tests. internal partial class InetUtils { + private const RegexOptions InetRegexOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture; + private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(1); + private readonly IDomainNameResolver _domainNameResolver; private readonly IOptionsMonitor _optionsMonitor; private readonly ILogger _logger; @@ -139,7 +142,7 @@ internal bool IsPreferredAddress(IPAddress address, InetOptions inetOptions) foreach (string regex in preferredNetworks) { string hostAddress = address.ToString(); - var matcher = new Regex(regex, RegexOptions.None, TimeSpan.FromSeconds(1)); + var matcher = new Regex(regex, InetRegexOptions, RegexMatchTimeout); if (matcher.IsMatch(hostAddress) || hostAddress.StartsWith(regex, StringComparison.Ordinal)) { @@ -160,7 +163,7 @@ internal bool IgnoreInterface(string interfaceName, InetOptions inetOptions) foreach (string regex in inetOptions.GetIgnoredInterfaces()) { - var matcher = new Regex(regex, RegexOptions.None, TimeSpan.FromSeconds(1)); + var matcher = new Regex(regex, InetRegexOptions, RegexMatchTimeout); if (matcher.IsMatch(interfaceName)) { diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/CloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/CloudFoundryPostProcessor.cs index dbf1c2a71e..a54b06d45e 100644 --- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/CloudFoundryPostProcessor.cs +++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/CloudFoundryPostProcessor.cs @@ -7,13 +7,17 @@ namespace Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors; -internal abstract class CloudFoundryPostProcessor : IConfigurationPostProcessor +internal abstract partial class CloudFoundryPostProcessor : IConfigurationPostProcessor { - private static readonly Regex TagsConfigurationKeyRegex = - new("^vcap:services:[^:]+:[0-9]+:tags:[0-9]+", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); + private const int RegexMatchTimeoutInMilliseconds = 1_000; - private static readonly Regex LabelConfigurationKeyRegex = - new("^vcap:services:[^:]+:[0-9]+:label+", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); + [GeneratedRegex("^vcap:services:[^:]+:[0-9]+:tags:[0-9]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, + RegexMatchTimeoutInMilliseconds)] + private static partial Regex TagsConfigurationKeyRegex(); + + [GeneratedRegex("^vcap:services:[^:]+:[0-9]+:label+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, + RegexMatchTimeoutInMilliseconds)] + private static partial Regex LabelConfigurationKeyRegex(); public abstract void PostProcessConfiguration(PostProcessorConfigurationProvider provider, IDictionary configurationData); @@ -23,7 +27,7 @@ protected ICollection FilterKeys(IDictionary configurat foreach ((string key, string? value) in configurationData) { - if ((sources & KeyFilterSources.Tag) != 0 && TagsConfigurationKeyRegex.IsMatch(key) && + if ((sources & KeyFilterSources.Tag) != 0 && TagsConfigurationKeyRegex().IsMatch(key) && string.Equals(value, valueToFind, StringComparison.OrdinalIgnoreCase)) { string? parentKey = ConfigurationPath.GetParentPath(key); @@ -39,7 +43,7 @@ protected ICollection FilterKeys(IDictionary configurat } } - if ((sources & KeyFilterSources.Label) != 0 && LabelConfigurationKeyRegex.IsMatch(key) && + if ((sources & KeyFilterSources.Label) != 0 && LabelConfigurationKeyRegex().IsMatch(key) && string.Equals(value, valueToFind, StringComparison.OrdinalIgnoreCase)) { string? serviceBindingKey = ConfigurationPath.GetParentPath(key); diff --git a/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs b/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs index 5caa46c875..0b8be46740 100644 --- a/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs +++ b/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs @@ -13,10 +13,13 @@ internal sealed partial class DecryptionConfigurationProvider( IList providers, ITextDecryptor? textDecryptor, ILoggerFactory loggerFactory) : CompositeConfigurationProvider(providers, loggerFactory) { + private const int RegexMatchTimeoutInMilliseconds = 1_000; + private readonly ILogger _logger = loggerFactory.CreateLogger(); private ITextDecryptor? _textDecryptor = textDecryptor; - [GeneratedRegex("^{cipher}({key:(?.*)})?(?.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, 1000)] + [GeneratedRegex("^{cipher}({key:(?.*)})?(?.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, + RegexMatchTimeoutInMilliseconds)] private static partial Regex CipherRegex(); public override bool TryGet(string key, out string? value) diff --git a/src/Security/src/Authorization.Certificate/ApplicationInstanceCertificate.cs b/src/Security/src/Authorization.Certificate/ApplicationInstanceCertificate.cs index 2431888d87..abf71aeb1f 100644 --- a/src/Security/src/Authorization.Certificate/ApplicationInstanceCertificate.cs +++ b/src/Security/src/Authorization.Certificate/ApplicationInstanceCertificate.cs @@ -7,17 +7,9 @@ namespace Steeltoe.Security.Authorization.Certificate; -internal sealed class ApplicationInstanceCertificate +internal sealed partial class ApplicationInstanceCertificate { - // This pattern is found on certificates issued by Diego - private static readonly Regex CloudFoundryInstanceCertificateSubjectRegex = - new(@"^CN=(?[0-9a-f-]+),\sOU=organization:(?[0-9a-f-]+)\s\+\sOU=space:(?[0-9a-f-]+)\s\+\sOU=app:(?[0-9a-f-]+)$", - RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); - - // This pattern is found on certificates created by Steeltoe - private static readonly Regex SteeltoeInstanceCertificateSubjectRegex = - new(@"^CN=(?[0-9a-f-]+),\sOU=app:(?[0-9a-f-]+)\s\+\sOU=space:(?[0-9a-f-]+)\s\+\sOU=organization:(?[0-9a-f-]+)$", - RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); + private const int RegexMatchTimeoutInMilliseconds = 1_000; public string OrgId { get; } public string SpaceId { get; } @@ -32,16 +24,26 @@ private ApplicationInstanceCertificate(string orgId, string spaceId, string appl InstanceId = instanceId; } + // This pattern is found on certificates issued by Diego. + [GeneratedRegex(@"^CN=(?[0-9a-f-]+),\sOU=organization:(?[0-9a-f-]+)\s\+\sOU=space:(?[0-9a-f-]+)\s\+\sOU=app:(?[0-9a-f-]+)$", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, RegexMatchTimeoutInMilliseconds)] + private static partial Regex CloudFoundryInstanceCertificateSubjectRegex(); + + // This pattern is found on certificates created by Steeltoe. + [GeneratedRegex(@"^CN=(?[0-9a-f-]+),\sOU=app:(?[0-9a-f-]+)\s\+\sOU=space:(?[0-9a-f-]+)\s\+\sOU=organization:(?[0-9a-f-]+)$", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, RegexMatchTimeoutInMilliseconds)] + private static partial Regex SteeltoeInstanceCertificateSubjectRegex(); + public static bool TryParse(string certificateSubject, [NotNullWhen(true)] out ApplicationInstanceCertificate? instanceCertificate) { instanceCertificate = null; certificateSubject = certificateSubject.Replace("\"", string.Empty, StringComparison.OrdinalIgnoreCase); - Match instanceMatch = CloudFoundryInstanceCertificateSubjectRegex.Match(certificateSubject); + Match instanceMatch = CloudFoundryInstanceCertificateSubjectRegex().Match(certificateSubject); if (!instanceMatch.Success) { - instanceMatch = SteeltoeInstanceCertificateSubjectRegex.Match(certificateSubject); + instanceMatch = SteeltoeInstanceCertificateSubjectRegex().Match(certificateSubject); } if (instanceMatch.Success) From 0516d54d38e24eab82a308413349b09344e379c1 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:00:34 +0100 Subject: [PATCH 3/5] Update to latest Sonar analyzer to match analysis in Sonar Cloud --- src/Discovery/src/Eureka/AppInfo/ApplicationInfo.cs | 7 ++++++- versions.props | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Discovery/src/Eureka/AppInfo/ApplicationInfo.cs b/src/Discovery/src/Eureka/AppInfo/ApplicationInfo.cs index 8417407d59..df11be4347 100644 --- a/src/Discovery/src/Eureka/AppInfo/ApplicationInfo.cs +++ b/src/Discovery/src/Eureka/AppInfo/ApplicationInfo.cs @@ -17,7 +17,7 @@ public sealed class ApplicationInfo private readonly ConcurrentDictionary _instanceMap = new(); public string Name { get; } - public IReadOnlyList Instances => new List(_instanceMap.Values); + public IReadOnlyList Instances => GetInstancesSnapshot(); internal ApplicationInfo(string name) : this(name, Array.Empty()) @@ -38,6 +38,11 @@ internal ApplicationInfo(string name, ICollection instances) } } + private List GetInstancesSnapshot() + { + return [.. _instanceMap.Values]; + } + internal InstanceInfo? GetInstance(string instanceId) { ArgumentException.ThrowIfNullOrWhiteSpace(instanceId); diff --git a/versions.props b/versions.props index 7281d21072..123a774861 100644 --- a/versions.props +++ b/versions.props @@ -21,7 +21,7 @@ 7.2.* 4.0.* 8.4.* - 10.18.0.131500 + 10.20.0.135146 1.2.0-beta.556 2.0.* 8.15.* From 010d934b6f68120ec28c10dc300e542ea823abf1 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:02:18 +0100 Subject: [PATCH 4/5] Update to latest R# version --- .config/dotnet-tools.json | 2 +- src/Steeltoe.All.sln.DotSettings | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 0763ef1c5b..826a95c4eb 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2025.3.0.3", + "version": "2025.3.3", "commands": [ "jb" ], diff --git a/src/Steeltoe.All.sln.DotSettings b/src/Steeltoe.All.sln.DotSettings index 112d0c37d2..767688ffbd 100644 --- a/src/Steeltoe.All.sln.DotSettings +++ b/src/Steeltoe.All.sln.DotSettings @@ -58,6 +58,7 @@ WARNING SUGGESTION DO_NOT_SHOW + DO_NOT_SHOW WARNING DO_NOT_SHOW HINT From 16fea688ce8dabed197b6843d748e15f16fa5e0d Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:44:46 +0100 Subject: [PATCH 5/5] Revert switch to extension method syntax --- src/Management/src/Prometheus/PrometheusExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Management/src/Prometheus/PrometheusExtensions.cs b/src/Management/src/Prometheus/PrometheusExtensions.cs index f030566994..de9520efa4 100644 --- a/src/Management/src/Prometheus/PrometheusExtensions.cs +++ b/src/Management/src/Prometheus/PrometheusExtensions.cs @@ -30,7 +30,7 @@ public static partial class PrometheusExtensions /// public static IServiceCollection AddPrometheusActuator(this IServiceCollection services) { - return services.AddPrometheusActuator(true, null); + return AddPrometheusActuator(services, true, null); } /// @@ -48,7 +48,7 @@ public static IServiceCollection AddPrometheusActuator(this IServiceCollection s /// public static IServiceCollection AddPrometheusActuator(this IServiceCollection services, bool configureMiddleware) { - return services.AddPrometheusActuator(configureMiddleware, null); + return AddPrometheusActuator(services, configureMiddleware, null); } /// @@ -105,7 +105,7 @@ public static IServiceCollection AddPrometheusActuator(this IServiceCollection s /// public static IApplicationBuilder UsePrometheusActuator(this IApplicationBuilder builder) { - return builder.UsePrometheusActuator(null); + return UsePrometheusActuator(builder, null); } ///