diff --git a/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs b/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs index 151d7b9315..6c5828dbd1 100644 --- a/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs @@ -39,7 +39,7 @@ public static void AddServiceControlAudit(this IHostApplicationBuilder builder, RecordStartup(settings, configuration, persistenceConfiguration); builder.Logging.ClearProviders(); - builder.Logging.ConfigureLogging(settings.LoggingSettings.LogLevel); + builder.Logging.ConfigureLogging(settings.LoggingSettings.LogLevel, settings.InstanceName, InstanceVersion); var services = builder.Services; var transportSettings = settings.ToTransportSettings(); diff --git a/src/ServiceControl.Infrastructure/LoggerUtil.cs b/src/ServiceControl.Infrastructure/LoggerUtil.cs index b00617fbe0..0457e32edc 100644 --- a/src/ServiceControl.Infrastructure/LoggerUtil.cs +++ b/src/ServiceControl.Infrastructure/LoggerUtil.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; using OpenTelemetry.Logs; + using OpenTelemetry.Resources; using ServiceControl.Infrastructure.TestLogger; [Flags] @@ -29,7 +30,24 @@ public static bool IsLoggingTo(Loggers logger) return (logger & ActiveLoggers) == logger; } - public static void ConfigureLogging(this ILoggingBuilder loggingBuilder, LogLevel level) + // Configures the host logging pipeline. serviceName/serviceVersion identify this instance and are + // attached to exported OTLP telemetry as resource attributes (service.name/service.version) so logs + // are attributable instead of arriving as "unknown_service". They are mandatory: the host always knows + // its instance name and version, and the alternative (defaulting them) is what produced unattributable logs. + public static void ConfigureLogging(this ILoggingBuilder loggingBuilder, LogLevel level, string serviceName, string serviceVersion) + { + ArgumentException.ThrowIfNullOrEmpty(serviceName); + ArgumentException.ThrowIfNullOrEmpty(serviceVersion); + + // CreateDefault() also reads OTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES, so operators can still enrich + // the resource with deployment-specific attributes via those environment variables. + var resourceBuilder = ResourceBuilder.CreateDefault() + .AddService(serviceName, serviceVersion: serviceVersion, autoGenerateServiceInstanceId: true); + + loggingBuilder.AddProviders(level, resourceBuilder); + } + + static void AddProviders(this ILoggingBuilder loggingBuilder, LogLevel level, ResourceBuilder resourceBuilder) { loggingBuilder.SetMinimumLevel(level); @@ -54,7 +72,11 @@ public static void ConfigureLogging(this ILoggingBuilder loggingBuilder, LogLeve } if (IsLoggingTo(Loggers.Otlp)) { - loggingBuilder.AddOpenTelemetry(configure => configure.AddOtlpExporter()); + loggingBuilder.AddOpenTelemetry(configure => + { + configure.SetResourceBuilder(resourceBuilder); + configure.AddOtlpExporter(); + }); } } @@ -64,7 +86,10 @@ static ILoggerFactory GetOrCreateLoggerFactory(LogLevel level) { if (!_factories.TryGetValue(level, out var factory)) { - factory = LoggerFactory.Create(configure => configure.ConfigureLogging(level)); + // Static/bootstrap loggers are created from scattered call sites that have no instance identity + // (and many run before it is known). They use the default resource, which still honors + // OTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES if the operator sets them. + factory = LoggerFactory.Create(configure => configure.AddProviders(level, ResourceBuilder.CreateDefault())); _factories[level] = factory; } diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index a5d7623d79..c674d7a073 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -98,7 +98,7 @@ async Task InitializeServiceControl(ScenarioContext context) EnvironmentName = Environments.Development }); hostBuilder.Logging.ClearProviders(); - hostBuilder.Logging.ConfigureLogging(LogLevel.Information); + hostBuilder.Logging.ConfigureLogging(LogLevel.Information, settings.InstanceName, "0.0.0"); hostBuilder.Logging.AddContextAppender(context); hostBuilder.Services.AddScenarioContext(context); diff --git a/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs b/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs index 3316da03a5..634c9d5932 100644 --- a/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Monitoring; using System; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -26,11 +27,13 @@ namespace ServiceControl.Monitoring; public static class HostApplicationBuilderExtensions { + static readonly string InstanceVersion = FileVersionInfo.GetVersionInfo(typeof(HostApplicationBuilderExtensions).Assembly.Location).ProductVersion; + public static void AddServiceControlMonitoring(this IHostApplicationBuilder hostBuilder, Func onCriticalError, Settings settings, EndpointConfiguration endpointConfiguration) { - hostBuilder.Logging.ConfigureLogging(settings.LoggingSettings.LogLevel); + hostBuilder.Logging.ConfigureLogging(settings.LoggingSettings.LogLevel, settings.InstanceName, InstanceVersion); var services = hostBuilder.Services; diff --git a/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs b/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs index fdd41695c7..97f5903c09 100644 --- a/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs +++ b/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs @@ -34,7 +34,7 @@ public async Task SetUp() var hostBuilder = Host.CreateApplicationBuilder(); LoggerUtil.ActiveLoggers = Loggers.Test; - hostBuilder.Logging.ConfigureLogging(LogLevel.Information); + hostBuilder.Logging.ConfigureLogging(LogLevel.Information, "ServiceControl.Persistence.Tests", "0.0.0"); await PersistenceTestsContext.Setup(hostBuilder); diff --git a/src/ServiceControl/HostApplicationBuilderExtensions.cs b/src/ServiceControl/HostApplicationBuilderExtensions.cs index d8a6ab6b81..b3cfaaf2e7 100644 --- a/src/ServiceControl/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl/HostApplicationBuilderExtensions.cs @@ -30,6 +30,8 @@ namespace Particular.ServiceControl static class HostApplicationBuilderExtensions { + static readonly string InstanceVersion = FileVersionInfo.GetVersionInfo(typeof(HostApplicationBuilderExtensions).Assembly.Location).ProductVersion; + public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, Settings settings, EndpointConfiguration configuration, params ReadOnlySpan components) { ArgumentNullException.ThrowIfNull(configuration); @@ -42,7 +44,7 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S } hostBuilder.Logging.ClearProviders(); - hostBuilder.Logging.ConfigureLogging(settings.LoggingSettings.LogLevel); + hostBuilder.Logging.ConfigureLogging(settings.LoggingSettings.LogLevel, settings.InstanceName, InstanceVersion); var componentSetupContext = new ComponentInstallationContext(); var serviceControlComponents = components is { Length: 0 } ? ServiceControlMainInstance.Components : components; @@ -112,11 +114,9 @@ public static void AddServiceControlInstallers(this IHostApplicationBuilder host static void RecordStartup(Settings settings, EndpointConfiguration endpointConfiguration) { - var version = FileVersionInfo.GetVersionInfo(typeof(HostApplicationBuilderExtensions).Assembly.Location).ProductVersion; - var startupMessage = $@" ------------------------------------------------------------- -ServiceControl Version: {version} +ServiceControl Version: {InstanceVersion} Audit Retention Period (optional): {settings.AuditRetentionPeriod} Error Retention Period: {settings.ErrorRetentionPeriod} Ingest Error Messages: {settings.IngestErrorMessages}