From 76bdd61b8f5cb6f89e815e62e7b9524af57cd9e9 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 15 Jun 2026 16:41:13 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Attribute=20exported=20OTLP=20l?= =?UTF-8?q?ogs=20to=20the=20ServiceControl=20instance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OTLP log exporter was registered with a bare AddOtlpExporter() and no OpenTelemetry resource, so exported log records had no service identity and showed up as "unknown_service" in the backend. LoggerUtil.Initialize(serviceName, serviceVersion) is now called once at process startup (in each instance's Program.cs, before any logger is created) and builds a single OpenTelemetry resource — service.name = instance name, service.version = instance version, plus an auto-generated service.instance.id. Both the host logging pipeline and the static bootstrap loggers (CreateStaticLogger) share that resource, so every exported record — including early startup diagnostics — is attributed to the instance. service.name matches the value used by the Audit metrics resource, so logs and metrics correlate. The resource still uses ResourceBuilder.CreateDefault(), so operators can enrich it with deployment attributes via OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES. --- src/ServiceControl.Audit/Program.cs | 8 ++++++ .../LoggerUtil.cs | 25 ++++++++++++++++++- src/ServiceControl.Monitoring/Program.cs | 6 +++++ src/ServiceControl/Program.cs | 8 ++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/ServiceControl.Audit/Program.cs b/src/ServiceControl.Audit/Program.cs index 8cf01d435c..73a9eff6cc 100644 --- a/src/ServiceControl.Audit/Program.cs +++ b/src/ServiceControl.Audit/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Reflection; using Microsoft.Extensions.Logging; using ServiceControl.Audit.Infrastructure.Hosting; @@ -13,6 +14,13 @@ { ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); + // Establish the telemetry identity once, before any logger is created, so every logger — including the + // static bootstrap loggers — attributes exported OTLP logs to this instance. Mirrors the InstanceName + // resolution in Settings (InternalQueueName is the legacy fallback for the instance name). + var instanceName = SettingsReader.Read(Settings.SettingsRootNamespace, "InstanceName", + SettingsReader.Read(Settings.SettingsRootNamespace, "InternalQueueName", Settings.DEFAULT_INSTANCE_NAME)); + LoggerUtil.Initialize(instanceName, FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion); + var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); diff --git a/src/ServiceControl.Infrastructure/LoggerUtil.cs b/src/ServiceControl.Infrastructure/LoggerUtil.cs index b00617fbe0..86a4931b16 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] @@ -24,6 +25,24 @@ public static class LoggerUtil public static string SeqAddress { private get; set; } + // Telemetry resource attached to exported OTLP logs (service.name/service.version/service.instance.id). + // Set once at process startup via Initialize() — before any logger is created — so both the host pipeline + // and the static bootstrap loggers (CreateStaticLogger) share a single instance identity. Defaults to + // CreateDefault() (which still honors OTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES) for the rare logger + // created before Initialize runs. + static ResourceBuilder serviceResourceBuilder = ResourceBuilder.CreateDefault(); + + public static void Initialize(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. + serviceResourceBuilder = ResourceBuilder.CreateDefault() + .AddService(serviceName, serviceVersion: serviceVersion, autoGenerateServiceInstanceId: true); + } + public static bool IsLoggingTo(Loggers logger) { return (logger & ActiveLoggers) == logger; @@ -54,7 +73,11 @@ public static void ConfigureLogging(this ILoggingBuilder loggingBuilder, LogLeve } if (IsLoggingTo(Loggers.Otlp)) { - loggingBuilder.AddOpenTelemetry(configure => configure.AddOtlpExporter()); + loggingBuilder.AddOpenTelemetry(configure => + { + configure.SetResourceBuilder(serviceResourceBuilder); + configure.AddOtlpExporter(); + }); } } diff --git a/src/ServiceControl.Monitoring/Program.cs b/src/ServiceControl.Monitoring/Program.cs index 8635862a91..19bd378d99 100644 --- a/src/ServiceControl.Monitoring/Program.cs +++ b/src/ServiceControl.Monitoring/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Reflection; using Microsoft.Extensions.Logging; using ServiceControl.Configuration; @@ -11,6 +12,11 @@ { ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); + // Establish the telemetry identity once, before any logger is created, so every logger — including the + // static bootstrap loggers — attributes exported OTLP logs to this instance. + var instanceName = SettingsReader.Read(Settings.SettingsRootNamespace, "InstanceName", Settings.DEFAULT_INSTANCE_NAME); + LoggerUtil.Initialize(instanceName, FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion); + var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); diff --git a/src/ServiceControl/Program.cs b/src/ServiceControl/Program.cs index 7234479cd3..824eda9764 100644 --- a/src/ServiceControl/Program.cs +++ b/src/ServiceControl/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Reflection; using Microsoft.Extensions.Logging; using Particular.ServiceControl.Hosting; @@ -13,6 +14,13 @@ { ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); + // Establish the telemetry identity once, before any logger is created, so every logger — including the + // static bootstrap loggers — attributes exported OTLP logs to this instance. Mirrors the InstanceName + // resolution in Settings (InternalQueueName is the legacy fallback for the instance name). + var instanceName = SettingsReader.Read(Settings.SettingsRootNamespace, "InstanceName", + SettingsReader.Read(Settings.SettingsRootNamespace, "InternalQueueName", Settings.DEFAULT_INSTANCE_NAME)); + LoggerUtil.Initialize(instanceName, FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion); + var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); From f2e376e863dd5f4f147567e9ceabbf17d9aa1a6f Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 19 Jun 2026 16:19:45 +0200 Subject: [PATCH 2/4] Refactor LoggerUtil to auto-resolve service identity at startup --- src/ServiceControl.Audit/Program.cs | 8 +------- .../LoggerUtil.cs | 18 +++++++++++++----- src/ServiceControl.Monitoring/Program.cs | 6 +----- src/ServiceControl/Program.cs | 8 +------- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/ServiceControl.Audit/Program.cs b/src/ServiceControl.Audit/Program.cs index 73a9eff6cc..228b535dac 100644 --- a/src/ServiceControl.Audit/Program.cs +++ b/src/ServiceControl.Audit/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Reflection; using Microsoft.Extensions.Logging; using ServiceControl.Audit.Infrastructure.Hosting; @@ -14,12 +13,7 @@ { ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - // Establish the telemetry identity once, before any logger is created, so every logger — including the - // static bootstrap loggers — attributes exported OTLP logs to this instance. Mirrors the InstanceName - // resolution in Settings (InternalQueueName is the legacy fallback for the instance name). - var instanceName = SettingsReader.Read(Settings.SettingsRootNamespace, "InstanceName", - SettingsReader.Read(Settings.SettingsRootNamespace, "InternalQueueName", Settings.DEFAULT_INSTANCE_NAME)); - LoggerUtil.Initialize(instanceName, FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion); + LoggerUtil.Initialize(); var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); diff --git a/src/ServiceControl.Infrastructure/LoggerUtil.cs b/src/ServiceControl.Infrastructure/LoggerUtil.cs index 86a4931b16..9352501478 100644 --- a/src/ServiceControl.Infrastructure/LoggerUtil.cs +++ b/src/ServiceControl.Infrastructure/LoggerUtil.cs @@ -2,6 +2,8 @@ { using System; using System.Collections.Concurrent; + using System.Diagnostics; + using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; @@ -32,15 +34,21 @@ public static class LoggerUtil // created before Initialize runs. static ResourceBuilder serviceResourceBuilder = ResourceBuilder.CreateDefault(); - public static void Initialize(string serviceName, string serviceVersion) + public static void Initialize() { - ArgumentException.ThrowIfNullOrEmpty(serviceName); - ArgumentException.ThrowIfNullOrEmpty(serviceVersion); + var asm = Assembly.GetEntryAssembly() ?? throw new InvalidOperationException("Entry assembly not found"); + var serviceName = asm.GetName().Name ?? throw new InvalidOperationException("Entry assembly name not found"); + var serviceVersion = FileVersionInfo.GetVersionInfo(asm.Location).ProductVersion; // CreateDefault() also reads OTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES, so operators can still enrich // the resource with deployment-specific attributes via those environment variables. - serviceResourceBuilder = ResourceBuilder.CreateDefault() - .AddService(serviceName, serviceVersion: serviceVersion, autoGenerateServiceInstanceId: true); + serviceResourceBuilder = ResourceBuilder + .CreateDefault() + .AddService( + serviceName, + serviceVersion: serviceVersion, + autoGenerateServiceInstanceId: true + ); } public static bool IsLoggingTo(Loggers logger) diff --git a/src/ServiceControl.Monitoring/Program.cs b/src/ServiceControl.Monitoring/Program.cs index 19bd378d99..891c80c1db 100644 --- a/src/ServiceControl.Monitoring/Program.cs +++ b/src/ServiceControl.Monitoring/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Reflection; using Microsoft.Extensions.Logging; using ServiceControl.Configuration; @@ -12,10 +11,7 @@ { ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - // Establish the telemetry identity once, before any logger is created, so every logger — including the - // static bootstrap loggers — attributes exported OTLP logs to this instance. - var instanceName = SettingsReader.Read(Settings.SettingsRootNamespace, "InstanceName", Settings.DEFAULT_INSTANCE_NAME); - LoggerUtil.Initialize(instanceName, FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion); + LoggerUtil.Initialize(); var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); diff --git a/src/ServiceControl/Program.cs b/src/ServiceControl/Program.cs index 824eda9764..d310668003 100644 --- a/src/ServiceControl/Program.cs +++ b/src/ServiceControl/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Reflection; using Microsoft.Extensions.Logging; using Particular.ServiceControl.Hosting; @@ -14,12 +13,7 @@ { ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - // Establish the telemetry identity once, before any logger is created, so every logger — including the - // static bootstrap loggers — attributes exported OTLP logs to this instance. Mirrors the InstanceName - // resolution in Settings (InternalQueueName is the legacy fallback for the instance name). - var instanceName = SettingsReader.Read(Settings.SettingsRootNamespace, "InstanceName", - SettingsReader.Read(Settings.SettingsRootNamespace, "InternalQueueName", Settings.DEFAULT_INSTANCE_NAME)); - LoggerUtil.Initialize(instanceName, FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion); + LoggerUtil.Initialize(); var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); From 967305628e0f78fedfcda437ce6fd21969376535 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 19 Jun 2026 16:24:20 +0200 Subject: [PATCH 3/4] As resource builder is now created based only on info from entry assembly that can be handled fully private --- src/ServiceControl.Audit/Program.cs | 2 -- src/ServiceControl.Infrastructure/LoggerUtil.cs | 11 ++++------- src/ServiceControl.Monitoring/Program.cs | 2 -- src/ServiceControl/Program.cs | 2 -- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/ServiceControl.Audit/Program.cs b/src/ServiceControl.Audit/Program.cs index 228b535dac..8cf01d435c 100644 --- a/src/ServiceControl.Audit/Program.cs +++ b/src/ServiceControl.Audit/Program.cs @@ -13,8 +13,6 @@ { ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - LoggerUtil.Initialize(); - var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); diff --git a/src/ServiceControl.Infrastructure/LoggerUtil.cs b/src/ServiceControl.Infrastructure/LoggerUtil.cs index 9352501478..eb49480cbf 100644 --- a/src/ServiceControl.Infrastructure/LoggerUtil.cs +++ b/src/ServiceControl.Infrastructure/LoggerUtil.cs @@ -32,9 +32,9 @@ public static class LoggerUtil // and the static bootstrap loggers (CreateStaticLogger) share a single instance identity. Defaults to // CreateDefault() (which still honors OTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES) for the rare logger // created before Initialize runs. - static ResourceBuilder serviceResourceBuilder = ResourceBuilder.CreateDefault(); + static ResourceBuilder serviceResourceBuilder = CreateResourcesBuilder(); - public static void Initialize() + static ResourceBuilder CreateResourcesBuilder() { var asm = Assembly.GetEntryAssembly() ?? throw new InvalidOperationException("Entry assembly not found"); var serviceName = asm.GetName().Name ?? throw new InvalidOperationException("Entry assembly name not found"); @@ -42,7 +42,7 @@ public static void Initialize() // CreateDefault() also reads OTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES, so operators can still enrich // the resource with deployment-specific attributes via those environment variables. - serviceResourceBuilder = ResourceBuilder + return ResourceBuilder .CreateDefault() .AddService( serviceName, @@ -51,10 +51,7 @@ public static void Initialize() ); } - public static bool IsLoggingTo(Loggers logger) - { - return (logger & ActiveLoggers) == logger; - } + public static bool IsLoggingTo(Loggers logger) => (logger & ActiveLoggers) == logger; public static void ConfigureLogging(this ILoggingBuilder loggingBuilder, LogLevel level) { diff --git a/src/ServiceControl.Monitoring/Program.cs b/src/ServiceControl.Monitoring/Program.cs index 891c80c1db..8635862a91 100644 --- a/src/ServiceControl.Monitoring/Program.cs +++ b/src/ServiceControl.Monitoring/Program.cs @@ -11,8 +11,6 @@ { ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - LoggerUtil.Initialize(); - var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); diff --git a/src/ServiceControl/Program.cs b/src/ServiceControl/Program.cs index d310668003..7234479cd3 100644 --- a/src/ServiceControl/Program.cs +++ b/src/ServiceControl/Program.cs @@ -13,8 +13,6 @@ { ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - LoggerUtil.Initialize(); - var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); From 102c1c50e6c9148d1b3cce4544f1028f4102af7c Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 19 Jun 2026 16:46:44 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B=20Fix=20IDE0055=20formatting:?= =?UTF-8?q?=20remove=20double=20space=20in=20CreateResourcesBuilder=20sign?= =?UTF-8?q?ature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ServiceControl.Infrastructure/LoggerUtil.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceControl.Infrastructure/LoggerUtil.cs b/src/ServiceControl.Infrastructure/LoggerUtil.cs index eb49480cbf..a562e6f934 100644 --- a/src/ServiceControl.Infrastructure/LoggerUtil.cs +++ b/src/ServiceControl.Infrastructure/LoggerUtil.cs @@ -34,7 +34,7 @@ public static class LoggerUtil // created before Initialize runs. static ResourceBuilder serviceResourceBuilder = CreateResourcesBuilder(); - static ResourceBuilder CreateResourcesBuilder() + static ResourceBuilder CreateResourcesBuilder() { var asm = Assembly.GetEntryAssembly() ?? throw new InvalidOperationException("Entry assembly not found"); var serviceName = asm.GetName().Name ?? throw new InvalidOperationException("Entry assembly name not found");