diff --git a/Directory.Build.props b/Directory.Build.props
index 556ac2a..b06a2b9 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,7 +4,7 @@
net8.0;net9.0;net10.0
- 5.4.3
+ 5.4.4
5.4.0
5.4.0
Jon Sagara
diff --git a/src/Sagara.Core.Logging.Serilog/AddSerilogToExistingILoggerFactoryResult.cs b/src/Sagara.Core.Logging.Serilog/AddSerilogToExistingILoggerFactoryResult.cs
new file mode 100644
index 0000000..03dfa2c
--- /dev/null
+++ b/src/Sagara.Core.Logging.Serilog/AddSerilogToExistingILoggerFactoryResult.cs
@@ -0,0 +1,17 @@
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Serilog.Extensions.Logging;
+
+namespace Sagara.Core.Logging.Serilog;
+
+///
+/// The result of configuring an Azure App Service or dotnet-isolated Azure Function to support logging with both
+/// Serilog and Microsoft.Extensions.Logging (MEL).
+/// Initially, the collections is empty. After building the host, the caller MUST call
+///
+/// to populate it.
+///
+/// The concrete instances retrieved from the
+/// instance.
+public record AddSerilogToExistingILoggerFactoryResult(
+ LoggerProviderCollection NonSerilogMELProviders);
diff --git a/src/Sagara.Core.Logging.Serilog/Azure/Functions/ConfigureAzureFunctionsLoggingResult.cs b/src/Sagara.Core.Logging.Serilog/Azure/Functions/ConfigureAzureFunctionsLoggingResult.cs
deleted file mode 100644
index d3715bc..0000000
--- a/src/Sagara.Core.Logging.Serilog/Azure/Functions/ConfigureAzureFunctionsLoggingResult.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Serilog.Extensions.Logging;
-
-namespace Sagara.Core.Logging.Serilog.Azure.Functions;
-
-///
-///
-///
-///
-public record ConfigureAzureFunctionsLoggingResult(
- LoggerProviderCollection NonSerilogMELProviders);
diff --git a/src/Sagara.Core.Logging.Serilog/Azure/Functions/SerilogAzureFunctionsExtensions.cs b/src/Sagara.Core.Logging.Serilog/Azure/Functions/SerilogAzureFunctionsExtensions.cs
deleted file mode 100644
index 81c07a5..0000000
--- a/src/Sagara.Core.Logging.Serilog/Azure/Functions/SerilogAzureFunctionsExtensions.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Serilog;
-using Serilog.Extensions.Logging;
-
-namespace Sagara.Core.Logging.Serilog.Azure.Functions;
-
-public static class SerilogAzureFunctionsExtensions
-{
- public static ConfigureAzureFunctionsLoggingResult ConfigureAzureFunctionsLogging(this IHostApplicationBuilder builder, Func createSerilogLoggerConfiguration)
- {
- Check.ThrowIfNull(builder);
- Check.ThrowIfNull(createSerilogLoggerConfiguration);
-
- // The "DirectSerilog" property tags events that originate from Serilog directly (DI / static); WriteTo.Providers()
- // forwards only these to other MEL providers, so MEL log events that get sent to Serilog aren't again forwarded
- // back to MEL. This eliniates duplicates in Application Insights.
- // NOTE: the random suffix has no meaning; it' s just to ensure the property name is unique and won't collide
- // with any user-defined properties.
- var directSerilogPropertyName = $"DirectSerilog_{RandomString.GenerateAlphanumeric(length: 16)}";
-
- // This collection lets Serilog write log events from Serilog.ILogger to through other dynamically-added MEL
- // ILoggerProviders (but not SerilogLoggerProvider, as that creates circular references and crashes the app).
- // Justification: This lives for the life of the process as a registered singleton, and the process will dispose of
- // it upon shutdown.
-#pragma warning disable CA2000 // Unnecessary assignment of a value
- var nonSerilogMELProviders = new LoggerProviderCollection();
-#pragma warning restore CA2000 // Unnecessary assignment of a value
-
- // Always write Serilog.ILogger log events to all other non-Serilog ILoggerProviders (most importantly, OTel).
- // For logs that originate from MEL (and thus don't have the "DirectSerilog" property), we assume they've
- // already reached OTel, and we don't forward them again.
- var underlyingLogger = createSerilogLoggerConfiguration()
- .WriteTo.Logger(lc => lc
- .Filter.ByIncludingOnly(le => le.Properties.ContainsKey(directSerilogPropertyName))
- .WriteTo.Providers(nonSerilogMELProviders)
- )
- .CreateLogger();
-
- // Create a Serilog logger that has the "DirectSerilog" property, so that the bridge to OTel can identify
- // events that originate from Serilog directly (DI / static).
- // This is also the Serilog.ILogger instance that register with DI for injecting, and also for using as
- // static logging properties via Log.Logger.ForContext(). The "DirectSerilog" property will be included on
- // all logs from that logger, so they will be forwarded to OTel and thus to Azure Monitor / App Insights.
- // Logs that don't have the "DirectSerilog" property are assumed to have come through MEL (and thus already
- // reached OTel via OpenTelemetryLoggerProvider) and are not forwarded again to avoid double-writing in Azure
- // Monitor / App Insights.
- // "true" has no meaning. We only care whether or not the property is present on the LogEvent.
- Log.Logger = underlyingLogger.ForContext(propertyName: directSerilogPropertyName, value: true);
-
- // Now that we have the underlying Serilog logger, we can add the SerilogLoggerProvider to MEL, which allows a
- // MEL ILogger to write to Serilog's non-OTel sinks (i.e., Files).
- // We can't call builder.Services.AddSerilog() or builder.Host.UseSerilog() because that would overwrite the
- // Functions host's ILoggerFactory and thus break communication between the host and the worker.
- builder.Logging.AddSerilog(underlyingLogger, dispose: true);
-
- // Register the collection of other MEL providers and the Serilog logger itself for DI. Serilog needs the providers
- // collection to know what to forward to.
- builder.Services.AddSingleton(nonSerilogMELProviders);
-
- // Log.Logger is what will be used for static logging via Log.ForContext().
- builder.Services.AddSingleton(Log.Logger);
-
- // Return the collection of non-Serilog MEL providers so that the caller can populate it after building the host.
- return new(NonSerilogMELProviders: nonSerilogMELProviders);
- }
-
- ///
- /// After the host is built, retrieve all non- instances
- /// and register them with Serilog. This enables Serilog to forward all Serilog.ILogger log entries to the registered
- /// MEL providers.
- ///
- /// The dotnet-isolated Azure Function's instance.
- /// The result of configuring Azure Functions logging for Serilog and MEL.
- public static void RegisterNonSerilogMELProvidersWithSerilog(this IHost host, ConfigureAzureFunctionsLoggingResult configureAzureFuncLoggingResult)
- {
- Check.ThrowIfNull(host);
- Check.ThrowIfNull(configureAzureFuncLoggingResult);
-
- // After building the host, we can pull out any MEL ILoggerProviders that were registered and add them to the
- // LoggerProviderCollection (except SerilogLoggerProvider, which causes circular references and crashes the process).
- // This is what allows Serilog to write to any MEL providers, including to Azure Monitor for viewing in Application Insights.
- var loggerProviders = host.Services
- .GetServices()
- .Where(lp => lp.GetType() != typeof(SerilogLoggerProvider))
- .ToArray();
-
- foreach (var loggerProvider in loggerProviders)
- {
- configureAzureFuncLoggingResult.NonSerilogMELProviders.AddProvider(loggerProvider);
- }
- }
-}
diff --git a/src/Sagara.Core.Logging.Serilog/SerilogExtensions.cs b/src/Sagara.Core.Logging.Serilog/SerilogExtensions.cs
index dc5ebaf..9922928 100644
--- a/src/Sagara.Core.Logging.Serilog/SerilogExtensions.cs
+++ b/src/Sagara.Core.Logging.Serilog/SerilogExtensions.cs
@@ -1,10 +1,139 @@
-using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Sagara.Core.Logging.Serilog;
using Serilog;
+using Serilog.Events;
+using Serilog.Extensions.Logging;
namespace Sagara.Core.Logging.Serilog;
public static class SerilogExtensions
{
+ ///
+ /// Configure Serilog and Microsoft.Extensions.Logging (MEL) for Azure App Service and dotnet-isolated Azure Function so that:
+ ///
+ /// -
+ /// Serilog.ILogger log events are forwarded to all MEL providers EXCEPT SerilogLoggerProvider.
+ ///
+ /// -
+ /// MEL log events are forwarded to all of Serilog's sinks.
+ ///
+ /// -
+ /// Both Serilog.ILogger and MEL write log events to Azure Monitor (via OTel) without duplicates, and are viewable in Application Insights.
+ ///
+ ///
+ /// See also: .
+ ///
+ ///
+ /// NOTE: this is intended for Azure App Service and dotnet-isolated Azure Functions.
+ ///
+ /// The application builder. The type is not enforced so that we don't have to take a dependency
+ /// on the Azure Functions SDK, but it should be either a WebApplicationBuilder or a FunctionsApplicationBuilder.
+ /// The caller-provided a function that creates a Serilog LoggerConfiguration.
+ /// An optional predicate to add additional conditions for forwarding Serilog LogEvents
+ /// to MEL providers via WriteTo.Providers(). By default, only events with the "DirectSerilog" property are forwarded. If provided, this predicate's
+ /// return value will be ANDed together with the check for existence of the "DirectSerilog" property.
+ /// A result containing the empty collection of other MEL providers. The caller must populate it after building the host.
+
+ public static AddSerilogToExistingILoggerFactoryResult AddSerilogToExistingILoggerFactory(this IHostApplicationBuilder builder,
+ Func createSerilogLoggerConfiguration, Func? additionalWriteToProvidersInclusionPredicate = null)
+ {
+ Check.ThrowIfNull(builder);
+ Check.ThrowIfNull(createSerilogLoggerConfiguration);
+
+ // The "DirectSerilog" property tags events that originate from Serilog directly (DI / static); WriteTo.Providers()
+ // forwards only these to other MEL providers, so MEL log events that get sent to Serilog aren't again forwarded
+ // back to MEL. This eliniates duplicates in Application Insights.
+ // NOTE: the random suffix has no meaning; it' s just to ensure the property name is unique and won't collide
+ // with any user-defined properties.
+ var directSerilogPropertyName = $"DirectSerilog_{RandomString.GenerateAlphanumeric(length: 16)}";
+
+ // This collection lets Serilog write log events from Serilog.ILogger to through other dynamically-added MEL
+ // ILoggerProviders (but not SerilogLoggerProvider, as that creates circular references and crashes the app).
+ // Justification: This lives for the life of the process as a registered singleton, and the process will dispose of
+ // it upon shutdown.
+#pragma warning disable CA2000 // Unnecessary assignment of a value
+ var nonSerilogMELProviders = new LoggerProviderCollection();
+#pragma warning restore CA2000 // Unnecessary assignment of a value
+
+ // Always write Serilog.ILogger log evnets to all other non-Serilog ILoggerProviders (most importantly, OTel).
+ // For logs that originate from MEL (and thus don't have the "DirectSerilog" property), we assume they've
+ // already reached OTel, and we don't forward them again.
+ var underlyingLogger = createSerilogLoggerConfiguration()
+ .WriteTo.Logger(lc => lc
+ .Filter.ByIncludingOnly(le =>
+ {
+ // ONLY send log events that originated from Serilog.ILogger.
+ var isFromSerilog = le.Properties.ContainsKey(directSerilogPropertyName);
+
+ // If the caller sent an additional predicate, apply that as well. The prime example of using this is to exclude
+ // "bad API middleware" Serilog logs from being forwarded to MEL, which probably get us blocked from using
+ // Application Insights.
+ // If the user didn't send an additional predicate, default to true so we don't automatically discard the event.
+ var additionalPredicateResult = additionalWriteToProvidersInclusionPredicate?.Invoke(le) ?? true;
+
+ return isFromSerilog && additionalPredicateResult;
+ })
+ .WriteTo.Providers(nonSerilogMELProviders)
+ )
+ .CreateLogger();
+
+ // Create a Serilog logger that has the "DirectSerilog" property, so that the bridge to OTel can identify
+ // events that originate from Serilog directly (DI / static).
+ // This is also the Serilog.ILogger instance that register with DI for injecting, and also for using as
+ // static logging properties via Log.Logger.ForContext(). The "DirectSerilog" property will be included on
+ // all logs from that logger, so they will be forwarded to OTel and thus to Azure Monitor / App Insights.
+ // Logs that don't have the "DirectSerilog" property are assumed to have come through MEL (and thus already
+ // reached OTel via OpenTelemetryLoggerProvider) and are not forwarded again to avoid double-writing in Azure
+ // Monitor / App Insights.
+ // "true" has no meaning. We only care whether or not the property is present on the LogEvent.
+ Log.Logger = underlyingLogger.ForContext(propertyName: directSerilogPropertyName, value: true);
+
+ // Now that we have the underlying Serilog logger, we can add the SerilogLoggerProvider to MEL, which allows a
+ // MEL ILogger to write to Serilog's non-OTel sinks (i.e., Files).
+ // We can't call builder.Services.AddSerilog() or builder.Host.UseSerilog() because that would overwrite the
+ // Functions host's ILoggerFactory and thus break communication between the host and the worker.
+ builder.Logging.AddSerilog(underlyingLogger, dispose: true);
+
+ // Register the collection of other MEL providers and the Serilog logger itself for DI. Serilog needs the providers
+ // collection to know what to forward to.
+ builder.Services.AddSingleton(nonSerilogMELProviders);
+
+ // Log.Logger is what will be used for static logging via Log.ForContext().
+ builder.Services.AddSingleton(Log.Logger);
+
+ // Return the collection of non-Serilog MEL providers so that the caller can populate it after building the host.
+ return new(NonSerilogMELProviders: nonSerilogMELProviders);
+ }
+
+ ///
+ /// After the host is built, retrieve all non- instances
+ /// and add them to Serilog's "write to" providers retuned by .
+ /// Serilog will forward all directly written Serilog.ILogger log entries to the registered MEL providers.
+ ///
+ /// The Azure App Service or dotnet-isolated Azure Function's instance.
+ /// A collection of concrete instances that were
+ /// registered with the host, excluding .
+ public static void AddNonSerilogMELProvidersToSerilog(this IHost host, AddSerilogToExistingILoggerFactoryResult addSerilogResult)
+ {
+ Check.ThrowIfNull(host);
+ Check.ThrowIfNull(addSerilogResult);
+
+ // After building the host, we can pull out any MEL ILoggerProviders that were registered and add them to the
+ // LoggerProviderCollection (except SerilogLoggerProvider, which causes circular references and crashes the process).
+ // This is what allows Serilog to write to any MEL providers, including to Azure Monitor for viewing in Application Insights.
+ var loggerProviders = host.Services
+ .GetServices()
+ .Where(lp => lp.GetType() != typeof(SerilogLoggerProvider))
+ .ToArray();
+
+ foreach (var loggerProvider in loggerProviders)
+ {
+ addSerilogResult.NonSerilogMELProviders.AddProvider(loggerProvider);
+ }
+ }
+
///
/// Sets Serilog as the logging provider.
///
@@ -22,6 +151,7 @@ public static class SerilogExtensions
/// registered through the Microsoft.Extensions.Logging API. Normally, equivalent Serilog sinks are used in place of providers.
/// Specify true to write events to all providers.
/// The host application builder.
+ [Obsolete($"Obsolete. Use {nameof(AddSerilogToExistingILoggerFactory)} instead. It's additive instead of overwriting the existing ILoggerFactory.")]
public static IHostApplicationBuilder UseSerilog(this IHostApplicationBuilder builder, Action configureLogger, bool preserveStaticLogger = false, bool writeToProviders = false)
{
ArgumentNullException.ThrowIfNull(builder);