diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/EnricherExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/EnricherExtensions.cs
index 936adf55559..a80820c3dc3 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/EnricherExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/EnricherExtensions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Shared.Diagnostics;
@@ -42,6 +43,38 @@ public static IServiceCollection AddLogEnricher(this IServiceCollection services
return services.AddSingleton(enricher);
}
+ ///
+ /// Registers a static log enricher type.
+ ///
+ /// The dependency injection container to add the enricher type to.
+ /// Enricher type.
+ /// The value of .
+ /// is .
+ [Experimental]
+ public static IServiceCollection AddStaticLogEnricher(this IServiceCollection services)
+ where T : class, IStaticLogEnricher
+ {
+ _ = Throw.IfNull(services);
+
+ return services.AddSingleton();
+ }
+
+ ///
+ /// Registers a static log enricher instance.
+ ///
+ /// The dependency injection container to add the enricher instance to.
+ /// The enricher instance to add.
+ /// The value of .
+ /// or are .
+ [Experimental]
+ public static IServiceCollection AddStaticLogEnricher(this IServiceCollection services, IStaticLogEnricher enricher)
+ {
+ _ = Throw.IfNull(services);
+ _ = Throw.IfNull(enricher);
+
+ return services.AddSingleton(enricher);
+ }
+
///
/// Registers a metric enricher type.
///
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IStaticLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IStaticLogEnricher.cs
new file mode 100644
index 00000000000..8d03972a16c
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IStaticLogEnricher.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Extensions.Telemetry.Enrichment;
+
+///
+/// A component that augments log records with additional properties which are unchanging over the life of the object.
+///
+[Experimental]
+public interface IStaticLogEnricher
+{
+ ///
+ /// Called to generate properties for a log record.
+ ///
+ /// Where the enricher puts the properties it is producing.
+ void Enrich(IEnrichmentPropertyBag bag);
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/ILogPropertyCollector.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/ILogPropertyCollector.cs
index bb2d6823f8a..befaee3e7f2 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/ILogPropertyCollector.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/ILogPropertyCollector.cs
@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Compliance.Classification;
namespace Microsoft.Extensions.Telemetry.Logging;
@@ -23,4 +25,17 @@ public interface ILogPropertyCollector
/// or when a property of the same name has already been added.
///
void Add(string propertyName, object? propertyValue);
+
+ ///
+ /// Adds a property to the current log record.
+ ///
+ /// The name of the property to add.
+ /// The value of the property to add.
+ /// The data classification of the property value.
+ /// is .
+ /// is empty or contains exclusively whitespace,
+ /// or when a property of the same name has already been added.
+ ///
+ [Experimental]
+ void Add(string propertyName, object? propertyValue, DataClassification classification);
}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodHelper.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodHelper.cs
index bfa1c1ce49d..ff64387a742 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodHelper.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodHelper.cs
@@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Compliance.Classification;
#if NET6_0_OR_GREATER
using Microsoft.Extensions.Logging;
#endif
@@ -37,6 +38,10 @@ public void Add(string propertyName, object? propertyValue)
Add(new KeyValuePair(fullName, propertyValue));
}
+ ///
+ [Experimental]
+ public void Add(string propertyName, object? propertyValue, DataClassification classification) => Add(propertyName, propertyValue);
+
///
/// Resets state of this container as described in .
///
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerExtensions.cs
new file mode 100644
index 00000000000..981275cbd43
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerExtensions.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Logging;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+///
+/// Extensions for .
+///
+[Experimental]
+public static class LoggerExtensions
+{
+ ///
+ /// Emits a structured log entry.
+ ///
+ /// The logger to emit the log entry to.
+ /// Entry will be written on this level.
+ /// Id of the event.
+ /// The set of name/value pairs to log with this entry.
+ public static void StructuredLog(this ILogger logger, LogLevel logLevel, EventId eventId, LoggerMessageProperties properties)
+ {
+ Throw.IfNull(logger).Log(logLevel, eventId, properties, null, (_, _) => string.Empty);
+ }
+
+ ///
+ /// Emits a structured log entry.
+ ///
+ /// The logger to emit the log entry to.
+ /// Entry will be written on this level.
+ /// Id of the event.
+ /// The set of name/value pairs to log with this entry.
+ /// The exception related to this entry.
+ public static void StructuredLog(this ILogger logger, LogLevel logLevel, EventId eventId, LoggerMessageProperties properties, Exception? exception)
+ {
+ Throw.IfNull(logger).Log(logLevel, eventId, properties, exception, (_, _) => string.Empty);
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageHelper.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageHelper.cs
new file mode 100644
index 00000000000..288cf4c87e7
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageHelper.cs
@@ -0,0 +1,133 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Shared.Pools;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+///
+/// Utility type to support generated logging methods.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+[Experimental]
+public static class LoggerMessageHelper
+{
+ [ThreadStatic]
+ private static LoggerMessageProperties? _properties;
+
+ ///
+ /// Gets a thread-local instance of this type.
+ ///
+ public static LoggerMessageProperties ThreadStaticLoggerMessageProperties
+ {
+ get
+ {
+ _properties ??= new();
+ _ = _properties.TryReset();
+ return _properties;
+ }
+ }
+
+ ///
+ /// Enumerates an enumerable into a string.
+ ///
+ /// The enumerable object.
+ ///
+ /// A string representation of the enumerable.
+ ///
+ public static string Stringify(IEnumerable? enumerable)
+ {
+ if (enumerable == null)
+ {
+ return "null";
+ }
+
+ var sb = PoolFactory.SharedStringBuilderPool.Get();
+ _ = sb.Append('[');
+
+ bool first = true;
+ foreach (object? e in enumerable)
+ {
+ if (!first)
+ {
+ _ = sb.Append(',');
+ }
+
+ if (e == null)
+ {
+ _ = sb.Append("null");
+ }
+ else
+ {
+ _ = sb.Append(FormattableString.Invariant($"\"{e}\""));
+ }
+
+ first = false;
+ }
+
+ _ = sb.Append(']');
+ var result = sb.ToString();
+ PoolFactory.SharedStringBuilderPool.Return(sb);
+ return result;
+ }
+
+ ///
+ /// Enumerates an enumerable of key/value pairs into a string.
+ ///
+ /// Type of keys.
+ /// Type of values.
+ /// The enumerable object.
+ ///
+ /// A string representation of the enumerable.
+ ///
+ public static string Stringify(IEnumerable>? enumerable)
+ {
+ if (enumerable == null)
+ {
+ return "null";
+ }
+
+ var sb = PoolFactory.SharedStringBuilderPool.Get();
+ _ = sb.Append('{');
+
+ bool first = true;
+ foreach (var kvp in enumerable)
+ {
+ if (!first)
+ {
+ _ = sb.Append(',');
+ }
+
+ if (typeof(TKey).IsValueType || kvp.Key is not null)
+ {
+ _ = sb.Append(FormattableString.Invariant($"\"{kvp.Key}\"="));
+ }
+ else
+ {
+ _ = sb.Append("null=");
+ }
+
+ if (typeof(TValue).IsValueType || kvp.Value is not null)
+ {
+ _ = sb.Append(FormattableString.Invariant($"\"{kvp.Value}\""));
+ }
+ else
+ {
+ _ = sb.Append("null");
+ }
+
+ first = false;
+ }
+
+ _ = sb.Append('}');
+ var result = sb.ToString();
+ PoolFactory.SharedStringBuilderPool.Return(sb);
+ return result;
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.ClassifiedProperty.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.ClassifiedProperty.cs
new file mode 100644
index 00000000000..54a67ea2740
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.ClassifiedProperty.cs
@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Compliance.Classification;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+public partial class LoggerMessageProperties
+{
+ ///
+ /// Represents a captured property that needs redaction.
+ ///
+ [SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Not for customer use and hidden from docs")]
+ [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Not needed")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public readonly struct ClassifiedProperty
+ {
+ ///
+ /// Gets the name of the property.
+ ///
+ public readonly string Name { get; }
+
+ ///
+ /// Gets the property's value.
+ ///
+ public readonly object? Value { get; }
+
+ ///
+ /// Gets the property's data classification.
+ ///
+ public readonly DataClassification Classification { get; }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ public ClassifiedProperty(string name, object? value, DataClassification classification)
+ {
+ Name = name;
+ Value = value;
+ Classification = classification;
+ }
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.PropertyBag.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.PropertyBag.cs
new file mode 100644
index 00000000000..c5dc8d5d854
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.PropertyBag.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Telemetry.Enrichment;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+public partial class LoggerMessageProperties
+{
+ private sealed class PropertyBag : IEnrichmentPropertyBag
+ {
+ private readonly List> _properties;
+
+ public PropertyBag(List> properties)
+ {
+ _properties = properties;
+ }
+
+ void IEnrichmentPropertyBag.Add(string key, object value)
+ {
+ _properties.Add(new KeyValuePair(key, value));
+ }
+
+ ///
+ void IEnrichmentPropertyBag.Add(string key, string value)
+ {
+ _properties.Add(new KeyValuePair(key, value));
+ }
+
+ ///
+ void IEnrichmentPropertyBag.Add(ReadOnlySpan> properties)
+ {
+ foreach (var p in properties)
+ {
+ // we're going from KVP to KVP which is strictly correct, so ignore the complaint
+#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
+ _properties.Add(p);
+#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
+ }
+ }
+
+ ///
+ void IEnrichmentPropertyBag.Add(ReadOnlySpan> properties)
+ {
+ foreach (var p in properties)
+ {
+ _properties.Add(new KeyValuePair(p.Key, p.Value));
+ }
+ }
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.PropertyCollector.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.PropertyCollector.cs
new file mode 100644
index 00000000000..8c77ca42026
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.PropertyCollector.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using Microsoft.Extensions.Compliance.Classification;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+public partial class LoggerMessageProperties
+{
+ private sealed class PropertyCollector : ILogPropertyCollector
+ {
+ private const string Separator = "_";
+
+ private readonly List> _properties;
+ private readonly List _classifiedProperties;
+
+ public PropertyCollector(List> properties, List classifiedProperties)
+ {
+ _properties = properties;
+ _classifiedProperties = classifiedProperties;
+ }
+
+ public void Add(string propertyName, object? propertyValue)
+ {
+ _ = Throw.IfNull(propertyName);
+
+ string fullName = ParameterName.Length > 0 ? ParameterName + Separator + propertyName : propertyName;
+ _properties.Add(new KeyValuePair(fullName, propertyValue));
+ }
+
+ public void Add(string propertyName, object? propertyValue, DataClassification classification)
+ {
+ _ = Throw.IfNull(propertyName);
+
+ string fullName = ParameterName.Length > 0 ? ParameterName + Separator + propertyName : propertyName;
+ _classifiedProperties.Add(new(fullName, propertyValue, classification));
+ }
+
+ public string ParameterName { get; set; } = string.Empty;
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.cs
new file mode 100644
index 00000000000..923828a4b47
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageProperties.cs
@@ -0,0 +1,133 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Compliance.Classification;
+#if NET6_0_OR_GREATER
+using Microsoft.Extensions.Logging;
+#endif
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Telemetry.Enrichment;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+///
+/// Utility type to support generated logging methods.
+///
+///
+/// This type is not intended to be directly invoked by application code,
+/// it is intended to be invoked by generated logging method code.
+///
+[Experimental]
+public sealed partial class LoggerMessageProperties : IResettable
+{
+ private readonly PropertyBag _enrichmentPropertyBag;
+ private readonly PropertyCollector _propertyCollector;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LoggerMessageProperties()
+ {
+ _enrichmentPropertyBag = new(Properties);
+ _propertyCollector = new(new(Properties), new(ClassifiedProperties));
+ }
+
+ ///
+ /// Adds a property to the current log record.
+ ///
+ /// The name of the property to add.
+ /// The value of the property to add.
+ /// is .
+ /// is empty or contains exclusively whitespace,
+ /// or when a property of the same name has already been added.
+ ///
+ public void Add(string propertyName, object? propertyValue)
+ {
+ _ = Throw.IfNull(propertyName);
+ Properties.Add(new KeyValuePair(propertyName, propertyValue));
+ }
+
+ ///
+ /// Adds a property to the current log record.
+ ///
+ /// The name of the property to add.
+ /// The value of the property to add.
+ /// The data classification of the property value.
+ /// is .
+ /// is empty or contains exclusively whitespace,
+ /// or when a property of the same name has already been added.
+ ///
+ public void Add(string propertyName, object? propertyValue, DataClassification classification)
+ {
+ _ = Throw.IfNull(propertyName);
+ ClassifiedProperties.Add(new(propertyName, propertyValue, classification));
+ }
+
+ ///
+ /// Resets state of this container as described in .
+ ///
+ ///
+ /// if the object successfully reset and can be reused.
+ ///
+ public bool TryReset()
+ {
+ Properties.Clear();
+ ClassifiedProperties.Clear();
+ _propertyCollector.ParameterName = string.Empty;
+ return true;
+ }
+
+#if NET6_0_OR_GREATER
+ ///
+ /// Gets log define options configured to skip the log level enablement check.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static LogDefineOptions SkipEnabledCheckOptions { get; } = new() { SkipEnabledCheck = true };
+#endif
+
+ ///
+ /// Gets the list of properties added to this instance.
+ ///
+ [SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Not intended for application use")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public List> Properties { get; } = new();
+
+ ///
+ /// Gets a list of properties which must receive redaction before being used.
+ ///
+ [SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Not intended for application use")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public List ClassifiedProperties { get; } = new();
+
+ ///
+ /// Gets the property collector instance.
+ ///
+ /// The name of the parameter to prefix in front of all property names inserted into the collector.
+ /// The collector instance.
+ ///
+ /// This method is used by the logger message code generator to get an instance of a collector to
+ /// use when invoking a custom property collector method.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ILogPropertyCollector GetPropertyCollector(string parameterName)
+ {
+ _propertyCollector.ParameterName = parameterName;
+ return _propertyCollector;
+ }
+
+ ///
+ /// Gets an enrichment property bag.
+ ///
+ ///
+ /// This method is used by logger implementations that receive a
+ /// instance and want to use the instance as an enrichment property bag in order to harvest
+ /// properties from enrichers.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public IEnrichmentPropertyBag EnrichmentPropertyBag => _enrichmentPropertyBag;
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj
index 4edcb8dc6b3..99e60983dfd 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj
+++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj
@@ -22,6 +22,10 @@
+
+
+
+
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherExtensions.cs
index 4bdbe2879f8..6b0ea7b4ad6 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherExtensions.cs
@@ -43,6 +43,7 @@ public static IServiceCollection AddProcessLogEnricher(this IServiceCollection s
return services
.AddLogEnricher()
+ .AddStaticLogEnricher()
.AddLogEnricherOptions(configure);
}
@@ -60,6 +61,7 @@ public static IServiceCollection AddProcessLogEnricher(this IServiceCollection s
return services
.AddLogEnricher()
+ .AddStaticLogEnricher()
.AddLogEnricherOptions(_ => { }, section);
}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricher.cs
index 5828e5d866f..b67bb5f1190 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricher.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricher.cs
@@ -18,33 +18,15 @@ internal sealed class ProcessLogEnricher : ILogEnricher
private static string? _threadId;
private readonly bool _threadIdEnabled;
- private readonly string? _processId;
-
public ProcessLogEnricher(IOptions options)
{
var enricherOptions = Throw.IfMemberNull(options, options.Value);
_threadIdEnabled = enricherOptions.ThreadId;
-
- if (enricherOptions.ProcessId)
- {
-#if NET5_0_OR_GREATER
- var pid = Environment.ProcessId;
-#else
- var pid = System.Diagnostics.Process.GetCurrentProcess().Id;
-#endif
-
- _processId = pid.ToInvariantString();
- }
}
public void Enrich(IEnrichmentPropertyBag enrichmentBag)
{
- if (_processId != null)
- {
- enrichmentBag.Add(ProcessEnricherDimensions.ProcessId, _processId);
- }
-
if (_threadIdEnabled)
{
#pragma warning disable S2696 // Instance members should not write to "static" fields
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/StaticProcessLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/StaticProcessLogEnricher.cs
new file mode 100644
index 00000000000..a032134455a
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/StaticProcessLogEnricher.cs
@@ -0,0 +1,42 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Telemetry.Enrichment;
+using Microsoft.Shared.Diagnostics;
+using Microsoft.Shared.Text;
+
+namespace Microsoft.Extensions.Telemetry.Enrichment;
+
+///
+/// Enriches logs with process information.
+///
+internal sealed class StaticProcessLogEnricher : IStaticLogEnricher
+{
+ private readonly string? _processId;
+
+ public StaticProcessLogEnricher(IOptions options)
+ {
+ var enricherOptions = Throw.IfMemberNull(options, options.Value);
+
+ if (enricherOptions.ProcessId)
+ {
+#if NET5_0_OR_GREATER
+ var pid = Environment.ProcessId;
+#else
+ var pid = System.Diagnostics.Process.GetCurrentProcess().Id;
+#endif
+
+ _processId = pid.ToInvariantString();
+ }
+ }
+
+ public void Enrich(IEnrichmentPropertyBag enrichmentBag)
+ {
+ if (_processId != null)
+ {
+ enrichmentBag.Add(ProcessEnricherDimensions.ProcessId, _processId);
+ }
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherExtensions.cs
index 9de17701591..aafa10e6753 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherExtensions.cs
@@ -43,7 +43,7 @@ public static IServiceCollection AddServiceLogEnricher(this IServiceCollection s
_ = Throw.IfNull(configure);
return services
- .AddLogEnricher()
+ .AddStaticLogEnricher()
.AddLogEnricherOptions(configure);
}
@@ -60,7 +60,7 @@ public static IServiceCollection AddServiceLogEnricher(this IServiceCollection s
_ = Throw.IfNull(section);
return services
- .AddLogEnricher()
+ .AddStaticLogEnricher()
.AddLogEnricherOptions(_ => { }, section);
}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricher.cs
index 30e3a362e88..15a53c8379b 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricher.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricher.cs
@@ -9,7 +9,7 @@
namespace Microsoft.Extensions.Telemetry.Enrichment;
-internal sealed class ServiceLogEnricher : ILogEnricher
+internal sealed class ServiceLogEnricher : IStaticLogEnricher
{
private readonly KeyValuePair[] _props;
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/DefaultLoggerLevelConfigureOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/DefaultLoggerLevelConfigureOptions.cs
new file mode 100644
index 00000000000..973c7a540de
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/DefaultLoggerLevelConfigureOptions.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+internal sealed class DefaultLoggerLevelConfigureOptions : ConfigureOptions
+{
+ public DefaultLoggerLevelConfigureOptions(LogLevel level)
+ : base(options => options.MinLevel = level)
+ {
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs
new file mode 100644
index 00000000000..17c14092a75
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs
@@ -0,0 +1,254 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Microsoft.Extensions.Compliance.Redaction;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Telemetry.Enrichment;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+// TODO: support for IOptionMonitor
+
+///
+/// Produces instances of classes based on the given providers, with support for redaction and enrichment.
+///
+[Experimental]
+public sealed class ExtendedLoggerFactory : ILoggerFactory
+{
+ private readonly LoggerFactory _loggerFactory;
+ private readonly ILogEnricher[] _enrichers;
+ private readonly IStaticLogEnricher[] _staticEnrichers;
+ private readonly ExtendedLoggerFilterOptions _loggingOptions;
+ private readonly IRedactorProvider? _redactorProvider;
+ private readonly ConcurrentDictionary _cache = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ExtendedLoggerFactory()
+ {
+ _loggerFactory = new LoggerFactory();
+ _enrichers = Array.Empty();
+ _staticEnrichers = Array.Empty();
+ _loggingOptions = new();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The providers to use in producing instances.
+ /// The filter options to use.
+ public ExtendedLoggerFactory(
+ IEnumerable providers,
+ IOptionsMonitor filterOptions)
+ {
+ _ = Throw.IfNull(providers);
+ _ = Throw.IfNull(filterOptions);
+
+ _loggerFactory = new LoggerFactory(providers, filterOptions);
+ _enrichers = Array.Empty();
+ _staticEnrichers = Array.Empty();
+ _loggingOptions = filterOptions.CurrentValue;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The providers to use in producing instances.
+ /// The filter options to use.
+ public ExtendedLoggerFactory(
+ IEnumerable providers,
+ ExtendedLoggerFilterOptions filterOptions)
+ {
+ _ = Throw.IfNull(providers);
+ _ = Throw.IfNull(filterOptions);
+
+ _loggerFactory = new LoggerFactory(providers, filterOptions);
+ _enrichers = Array.Empty();
+ _staticEnrichers = Array.Empty();
+ _loggingOptions = filterOptions;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The providers to use in producing instances.
+ /// The filter options to use.
+ /// The .
+ public ExtendedLoggerFactory(
+ IEnumerable providers,
+ IOptionsMonitor filterOptions,
+ IOptions options)
+ {
+ _ = Throw.IfNull(providers);
+ _ = Throw.IfNull(filterOptions);
+ _ = Throw.IfNull(options);
+
+ _loggerFactory = new LoggerFactory(providers, filterOptions, options);
+ _enrichers = Array.Empty();
+ _staticEnrichers = Array.Empty();
+ _loggingOptions = filterOptions.CurrentValue;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The providers to use in producing instances.
+ /// The filter options to use.
+ /// The .
+ /// The .
+ public ExtendedLoggerFactory(
+ IEnumerable providers,
+ IOptionsMonitor filterOptions,
+ IOptions options,
+ IExternalScopeProvider scopeProvider)
+ {
+ _ = Throw.IfNull(providers);
+ _ = Throw.IfNull(filterOptions);
+ _ = Throw.IfNull(options);
+ _ = Throw.IfNull(scopeProvider);
+
+ _loggerFactory = new LoggerFactory(providers, filterOptions, options, scopeProvider);
+ _enrichers = Array.Empty();
+ _staticEnrichers = Array.Empty();
+ _loggingOptions = filterOptions.CurrentValue;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The providers to use in producing instances.
+ /// The filter options to use.
+ /// The .
+ /// The .
+ /// The dynamic enrichers to augment individual log entries with properties.
+ /// The static enrichers to augment individual log entries with properties.
+ /// The redactor provider that enables per-property redaction.
+ public ExtendedLoggerFactory(
+ IEnumerable providers,
+ IOptionsMonitor filterOptions,
+ IOptions? options,
+ IExternalScopeProvider? scopeProvider,
+ IEnumerable enrichers,
+ IEnumerable staticEnrichers,
+ IRedactorProvider redactorProvider)
+ {
+ _ = Throw.IfNull(providers);
+ _ = Throw.IfNull(filterOptions);
+ _ = Throw.IfNull(options);
+ _ = Throw.IfNull(scopeProvider);
+ _ = Throw.IfNull(enrichers);
+ _ = Throw.IfNull(staticEnrichers);
+ _ = Throw.IfNull(redactorProvider);
+
+ _loggerFactory = new LoggerFactory(providers, filterOptions, options, scopeProvider);
+ _enrichers = enrichers.ToArray();
+ _staticEnrichers = staticEnrichers.ToArray();
+ _loggingOptions = filterOptions.CurrentValue;
+ _redactorProvider = redactorProvider;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The providers to use in producing instances.
+ /// The filter options to use.
+ /// The dynamic enrichers to augment individual log entries with properties.
+ /// The static enrichers to augment individual log entries with properties.
+ /// The redactor provider that enables per-property redaction.
+ public ExtendedLoggerFactory(
+ IEnumerable providers,
+ IOptionsMonitor filterOptions,
+ IEnumerable enrichers,
+ IEnumerable staticEnrichers,
+ IRedactorProvider redactorProvider)
+ {
+ _ = Throw.IfNull(providers);
+ _ = Throw.IfNull(filterOptions);
+ _ = Throw.IfNull(enrichers);
+ _ = Throw.IfNull(staticEnrichers);
+ _ = Throw.IfNull(redactorProvider);
+
+ _loggerFactory = new LoggerFactory(providers, filterOptions);
+ _enrichers = enrichers.ToArray();
+ _staticEnrichers = staticEnrichers.ToArray();
+ _loggingOptions = filterOptions.CurrentValue;
+ _redactorProvider = redactorProvider;
+ }
+
+ ///
+ /// Creates new instance of configured using provided delegate.
+ ///
+ /// A delegate to configure the .
+ /// The that was created.
+ public static ILoggerFactory Create(Action configure)
+ {
+ var serviceCollection = new ServiceCollection();
+ _ = serviceCollection.AddExtendedLogging(configure);
+ ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
+ ILoggerFactory loggerFactory = serviceProvider.GetRequiredService();
+ return new DisposingLoggerFactory(loggerFactory, serviceProvider);
+ }
+
+ ///
+ /// Creates an with the given .
+ ///
+ /// The category name for messages produced by the logger.
+ /// The that was created.
+ public ILogger CreateLogger(string categoryName)
+ {
+ return _cache.GetOrAdd(categoryName, (name, lfp)
+ => new Logger(
+ lfp._loggerFactory.CreateLogger(name),
+ _enrichers,
+ _staticEnrichers,
+ _loggingOptions.CaptureStackTraces,
+ _loggingOptions.MaxStackTraceLength,
+ _redactorProvider), this);
+ }
+
+ ///
+ /// Adds the given provider to those used in creating instances.
+ ///
+ /// The to add.
+ public void AddProvider(ILoggerProvider provider) => _loggerFactory.AddProvider(provider);
+
+ ///
+ public void Dispose() => _loggerFactory.Dispose();
+
+ private sealed class DisposingLoggerFactory : ILoggerFactory
+ {
+ private readonly ILoggerFactory _loggerFactory;
+
+ private readonly ServiceProvider _serviceProvider;
+
+ public DisposingLoggerFactory(ILoggerFactory loggerFactory, ServiceProvider serviceProvider)
+ {
+ _loggerFactory = loggerFactory;
+ _serviceProvider = serviceProvider;
+ }
+
+ public void Dispose()
+ {
+ _serviceProvider.Dispose();
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return _loggerFactory.CreateLogger(categoryName);
+ }
+
+ public void AddProvider(ILoggerProvider provider)
+ {
+ _loggerFactory.AddProvider(provider);
+ }
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFilterOptions.cs
similarity index 65%
rename from src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptions.cs
rename to src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFilterOptions.cs
index 1188d2f7e11..1f4eec675b1 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptions.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFilterOptions.cs
@@ -3,39 +3,20 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.Telemetry.Logging;
///
-/// Options for logger.
+/// Options for extended logging.
///
-public class LoggingOptions
+[Experimental]
+public class ExtendedLoggerFilterOptions : LoggerFilterOptions
{
private const int MaxDefinedStackTraceLength = 32768;
private const int MinDefinedStackTraceLength = 2048;
private const int DefaultStackTraceLength = 4096;
- ///
- /// Gets or sets a value indicating whether to include log scopes in
- /// captured log state.
- ///
- ///
- /// The default value is .
- ///
- public bool IncludeScopes { get; set; }
-
- ///
- /// Gets or sets a value indicating whether to format the message included in captured log state.
- ///
- ///
- /// The default value is .
- ///
- ///
- /// When set to the placeholders in the message will be replaced by the actual values,
- /// otherwise the message template will be included as-is without replacements.
- ///
- public bool UseFormattedMessage { get; set; }
-
///
/// Gets or sets a value indicating whether to include stack trace when exception is logged.
///
@@ -48,7 +29,7 @@ public class LoggingOptions
/// defaults to 4096 characters and can be modified by setting the property.
/// The stack trace beyond the current limit will be truncated.
///
- public bool IncludeStackTrace { get; set; }
+ public bool CaptureStackTraces { get; set; }
///
/// Gets or sets the maximum stack trace length configured by the user.
@@ -59,7 +40,6 @@ public class LoggingOptions
///
/// When set to a value less than 2 KB or greater than 32 KB, an exception will be thrown.
///
- [Experimental]
[Range(MinDefinedStackTraceLength, MaxDefinedStackTraceLength, ErrorMessage = "Maximum stack trace length should be between 2kb and 32kb")]
public int MaxStackTraceLength { get; set; } = DefaultStackTraceLength;
}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFilterOptionsValidator.cs
similarity index 72%
rename from src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptionsValidator.cs
rename to src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFilterOptionsValidator.cs
index 9d9489c9a66..c7268aea473 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptionsValidator.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFilterOptionsValidator.cs
@@ -7,6 +7,6 @@
namespace Microsoft.Extensions.Telemetry.Logging;
[OptionsValidator]
-internal sealed partial class LoggingOptionsValidator : IValidateOptions
+internal sealed partial class ExtendedLoggerFilterOptionsValidator : IValidateOptions
{
}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggingExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggingExtensions.cs
new file mode 100644
index 00000000000..97231d56de5
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggingExtensions.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+///
+/// Extensions for configuring logging.
+///
+[Experimental]
+public static class ExtendedLoggingExtensions
+{
+ ///
+ /// Configure extended logging, enabling redaction and enrichment.
+ ///
+ /// The dependency injection container to add logging to.
+ /// Logging configuration options.
+ /// The value of .
+ public static IServiceCollection AddExtendedLogging(this IServiceCollection services, Action configure)
+ {
+ _ = Throw.IfNull(services);
+ _ = Throw.IfNull(configure);
+
+ _ = services.AddOptions();
+ services.TryAdd(ServiceDescriptor.Singleton());
+ services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));
+
+ // TODO: need to add option validation hook.
+
+ services.TryAddEnumerable(ServiceDescriptor.Singleton>(
+ new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));
+
+ configure(new LoggingBuilder(services));
+ return services;
+ }
+
+ ///
+ /// Configure extended logging with default options, enabling redaction and enrichment.
+ ///
+ /// The dependency injection container to add logging to.
+ /// The value of .
+ public static IServiceCollection AddExtendedLogging(this IServiceCollection services) => services.AddExtendedLogging(_ => { });
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.PropertyBag.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.PropertyBag.cs
new file mode 100644
index 00000000000..caa1941ecb9
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.PropertyBag.cs
@@ -0,0 +1,74 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.Extensions.Telemetry.Enrichment;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+internal sealed partial class Logger
+{
+ private sealed class PropertyBag : IReadOnlyList>, IEnrichmentPropertyBag
+ {
+ public object? Formatter;
+ public object? State;
+ public IReadOnlyList> DynamicProperties = null!;
+ public KeyValuePair[] StaticProperties = null!;
+
+ private readonly List> _properties = new();
+
+ public void Clear()
+ {
+ DynamicProperties = _properties;
+ _properties.Clear();
+ }
+
+ public KeyValuePair this[int index]
+ => index < DynamicProperties.Count
+ ? DynamicProperties[index]
+ : StaticProperties[index - DynamicProperties.Count];
+
+ public int Count => DynamicProperties.Count + StaticProperties.Length;
+
+ public IEnumerator> GetEnumerator()
+ {
+ foreach (var p in DynamicProperties)
+ {
+ yield return p;
+ }
+
+ foreach (var p in StaticProperties)
+ {
+ yield return p;
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ public void Add(string key, object value) => _properties.Add(new KeyValuePair(key, value));
+ public void Add(string key, string value) => _properties.Add(new KeyValuePair(key, value));
+
+ void IEnrichmentPropertyBag.Add(ReadOnlySpan> properties)
+ {
+ foreach (var p in properties)
+ {
+ // we're going from KVP to KVP which is strictly correct, so ignore the complaint
+#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
+ _properties.Add(p);
+#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
+ }
+ }
+
+ void IEnrichmentPropertyBag.Add(ReadOnlySpan> properties)
+ {
+ foreach (var p in properties)
+ {
+ _properties.Add(new KeyValuePair(p.Key, p.Value));
+ }
+ }
+
+ public void AddRange(IEnumerable> properties) => _properties.AddRange(properties);
+ public KeyValuePair[] ToArray() => _properties.ToArray();
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.PropertyJoiner.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.PropertyJoiner.cs
new file mode 100644
index 00000000000..feb01619a85
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.PropertyJoiner.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+internal sealed partial class Logger
+{
+ ///
+ /// Takes distinct dynamic and static properties and makes 'em look like a single IReadOnlyList.
+ ///
+ private sealed class PropertyJoiner : IReadOnlyList>
+ {
+ public LoggerMessageProperties DynamicProperties = null!;
+ public KeyValuePair[] StaticProperties = null!;
+ public Func Formatter = null!;
+
+ public KeyValuePair this[int index]
+ {
+ get
+ {
+ _ = Throw.IfOutOfRange(index, 0, Count);
+
+ return index < DynamicProperties.Properties.Count
+ ? DynamicProperties.Properties[index]
+ : StaticProperties[index - DynamicProperties.Properties.Count];
+ }
+ }
+
+ public int Count => DynamicProperties.Properties.Count + StaticProperties.Length;
+
+ public IEnumerator> GetEnumerator()
+ {
+ foreach (var p in DynamicProperties.Properties)
+ {
+ yield return p;
+ }
+
+ foreach (var p in StaticProperties)
+ {
+ yield return p;
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.cs
index aaa4a1b88ae..fcb4ceee842 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.cs
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.cs
@@ -3,179 +3,80 @@
using System;
using System.Collections.Generic;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Runtime.CompilerServices;
using System.Text;
+using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Telemetry.Logging;
+using Microsoft.Extensions.Telemetry.Enrichment;
using Microsoft.Shared.Pools;
-using OpenTelemetry.Logs;
namespace Microsoft.Extensions.Telemetry.Logging;
-internal sealed class Logger : ILogger
+internal sealed partial class Logger : ILogger
{
- internal static readonly Func>?, LogRecord> CreateLogRecord = GetLogCreator();
-
private const string ExceptionStackTrace = "stackTrace";
- private readonly string _categoryName;
- private readonly LoggerProvider _provider;
-
- ///
- /// Call OpenTelemetry's LogRecord constructor.
- ///
- ///
- /// Reflection is used because the constructor has 'internal' modifier and cannot be called directly.
- /// This will be replaced with a direct call in one of the two conditions below.
- /// - LogRecord will make its internalsVisible to R9 library.
- /// - LogRecord constructor will become public.
- ///
- private static Func>?, LogRecord> GetLogCreator()
- {
- var logRecordConstructor = typeof(LogRecord).GetConstructor(
-#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
- BindingFlags.Instance | BindingFlags.NonPublic,
-#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
- null,
- new[]
- {
- typeof(IExternalScopeProvider),
- typeof(DateTime),
- typeof(string),
- typeof(LogLevel),
- typeof(EventId),
- typeof(string),
- typeof(object),
- typeof(Exception),
- typeof(IReadOnlyList>)
- },
- null)!;
-
- var val = new[]
- {
- Expression.Parameter(typeof(IExternalScopeProvider)),
- Expression.Parameter(typeof(DateTime)),
- Expression.Parameter(typeof(string)),
- Expression.Parameter(typeof(LogLevel)),
- Expression.Parameter(typeof(EventId)),
- Expression.Parameter(typeof(string)),
- Expression.Parameter(typeof(object)),
- Expression.Parameter(typeof(Exception)),
- Expression.Parameter(typeof(IReadOnlyList>))
- };
-
- var lambdaLogRecord = Expression.Lambda>?,
- LogRecord>>(Expression.New(logRecordConstructor, val), val);
-
- return lambdaLogRecord.Compile();
- }
+ [ThreadStatic]
+ private static PropertyJoiner? _joiner;
- internal static TimeProvider TimeProvider => TimeProvider.System;
+ [ThreadStatic]
+ private static PropertyBag? _bag;
- internal Logger(string categoryName, LoggerProvider provider)
- {
- _categoryName = categoryName;
- _provider = provider;
- }
+ private readonly ILogger _nextLogger;
+ private readonly ILogEnricher[] _enrichers;
+ private readonly bool _includeStackTraces;
+ private readonly int _maxStackTraceLength;
+ private readonly IRedactorProvider? _redactorProvider;
+ private readonly KeyValuePair[] _staticProperties;
- internal IExternalScopeProvider? ScopeProvider { get; set; }
+ // TODO From Noah: For stack traces I'd recommend using new StackTrace(Exception, fNeedFileInfo:false)
+ // or you might incur major perf penalties and contention if a service logs a bunch of stack traces when a PDB is present on disk
- public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ public Logger(ILogger nextLogger, ILogEnricher[] enrichers, IStaticLogEnricher[] staticEnrichers, bool includeStackTraces, int maxStackTraceLength, IRedactorProvider? redactorProvider)
{
- if (!IsEnabled(logLevel))
+ _nextLogger = nextLogger;
+ _enrichers = enrichers;
+ _includeStackTraces = includeStackTraces;
+ _maxStackTraceLength = maxStackTraceLength;
+ _redactorProvider = redactorProvider;
+
+ var bag = new PropertyBag();
+ foreach (var enricher in staticEnrichers)
{
- return;
+ enricher.Enrich(bag);
}
- LogMethodHelper propertyBag;
- LogMethodHelper? rentedHelper = null;
-
- try
- {
- if (state is LogMethodHelper helper && _provider.CanUsePropertyBagPool)
- {
- propertyBag = helper;
- }
- else
- {
- rentedHelper = GetHelper();
- propertyBag = rentedHelper;
-
- switch (state)
- {
- case IReadOnlyList> stateList:
- rentedHelper.AddRange(stateList);
- break;
-
- case IEnumerable> stateList:
- rentedHelper.AddRange(stateList);
- break;
-
- case null:
- break;
-
- default:
- rentedHelper.Add("{OriginalFormat}", state);
- break;
- }
- }
-
- foreach (var enricher in _provider.Enrichers)
- {
- enricher.Enrich(propertyBag);
- }
-
- if (exception != null && _provider.IncludeStackTrace)
- {
- propertyBag.Add(ExceptionStackTrace, GetExceptionStackTrace(exception, _provider.MaxStackTraceLength));
- }
+ _staticProperties = bag.ToArray();
+ }
- var record = CreateLogRecord(
- _provider.IncludeScopes ? ScopeProvider : null,
- TimeProvider.GetUtcNow().UtcDateTime,
- _categoryName,
- logLevel,
- eventId,
- _provider.UseFormattedMessage ? formatter(state, exception) : null,
+ public IDisposable? BeginScope(TState state)
+ where TState : notnull
+ {
+ return _nextLogger.BeginScope(state);
+ }
- // This parameter needs to be null for OpenTelemetry.Exporter.Geneva to pick up LogRecord.StateValues (the last parameter).
- // This is equivalent to using OpenTelemetryLogger with ParseStateValues option set to true.
- null,
- exception,
- propertyBag);
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ // TODO: is this the best we can do here? Should/could the enabled state be tracked in this instance so we can avoid the interface call?
+ return _nextLogger.IsEnabled(logLevel);
+ }
- _provider.Processor?.OnEnd(record);
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ if (typeof(TState) == typeof(LoggerMessageProperties))
+ {
+ ModernPath(logLevel, eventId, (LoggerMessageProperties?)(object?)state, exception, (Func)(object)formatter);
}
- catch (Exception ex)
+ else if (typeof(TState) == typeof(LogMethodHelper))
{
- LoggingEventSource.Log.LogException(ex);
- throw;
+ // temporary support for legacy generated code, until generator is updated
+ R9Path(logLevel, eventId, (LogMethodHelper?)(object?)state, exception, (Func)(object)formatter);
}
- finally
+ else
{
- ReturnHelper(rentedHelper);
+ LegacyPath(logLevel, eventId, state, exception, formatter);
}
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
-
-#pragma warning disable CS8633
-#pragma warning disable CS8766
- public IDisposable? BeginScope(TState state)
- where TState : notnull
-#pragma warning restore CS8633
-#pragma warning restore CS8766
- {
- ScopeProvider ??= new LoggerExternalScopeProvider();
-
- return ScopeProvider.Push(state);
- }
-
private static string GetExceptionStackTrace(Exception exception, int maxStackTraceLength)
{
if (exception.StackTrace == null && exception.InnerException == null)
@@ -219,7 +120,7 @@ private static void GetInnerExceptionTrace(Exception exception, StringBuilder st
_ = stringBuilder.Append("InnerException type:");
_ = stringBuilder.Append(innerException.GetType());
_ = stringBuilder.Append(" message:");
- _ = stringBuilder.Append(innerException.Message);
+ _ = stringBuilder.Append(innerException.Message); // TODO: this should probably be using the exception summarizer
_ = stringBuilder.Append(" stack:");
_ = stringBuilder.Append(innerException.StackTrace);
@@ -227,18 +128,154 @@ private static void GetInnerExceptionTrace(Exception exception, StringBuilder st
}
}
- private LogMethodHelper GetHelper()
+ private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageProperties? properties, Exception? exception, Func formatter)
{
- return _provider.CanUsePropertyBagPool
- ? LogMethodHelper.GetHelper()
- : new LogMethodHelper();
+ if (!IsEnabled(logLevel) || properties == null)
+ {
+ return;
+ }
+
+ // redact
+ if (_redactorProvider != null)
+ {
+ foreach (var cp in properties.ClassifiedProperties)
+ {
+ properties.Add(cp.Name, _redactorProvider.GetRedactor(cp.Classification).Redact(cp.Value));
+ }
+ }
+
+ // enrich
+ foreach (var enricher in _enrichers)
+ {
+ enricher.Enrich(properties.EnrichmentPropertyBag);
+ }
+
+ // one last dedicated bit of enrichment
+ if (exception != null && _includeStackTraces)
+ {
+ properties.Add(ExceptionStackTrace, GetExceptionStackTrace(exception, _maxStackTraceLength));
+ }
+
+ var joiner = _joiner;
+ if (joiner == null)
+ {
+ joiner = new()
+ {
+ StaticProperties = _staticProperties
+ };
+
+#pragma warning disable S2696 // Instance members should not write to "static" fields
+ _joiner = joiner;
+#pragma warning restore S2696 // Instance members should not write to "static" fields
+ }
+
+ joiner.DynamicProperties = properties;
+ joiner.Formatter = formatter;
+
+ try
+ {
+ _nextLogger.Log(logLevel, eventId, joiner, exception, static (s, e) => s.Formatter(s.DynamicProperties, e));
+ }
+ catch (Exception ex)
+ {
+ LoggingEventSource.Log.LogException(ex);
+ throw;
+ }
}
- private void ReturnHelper(LogMethodHelper? helper)
+ private void R9Path(LogLevel logLevel, EventId eventId, LogMethodHelper? state, Exception? exception, Func formatter)
{
- if (_provider.CanUsePropertyBagPool && helper != null)
+ if (!IsEnabled(logLevel) || state == null)
+ {
+ return;
+ }
+
+ // no redaction, it's assumed to be done in the generated code
+
+ // enrich
+ foreach (var enricher in _enrichers)
+ {
+ enricher.Enrich(state);
+ }
+
+ // one last dedicated bit of enrichment
+ if (exception != null && _includeStackTraces)
{
- LogMethodHelper.ReturnHelper(helper);
+ state.Add(ExceptionStackTrace, GetExceptionStackTrace(exception, _maxStackTraceLength));
+ }
+
+ try
+ {
+ _nextLogger.Log(logLevel, eventId, state, exception, formatter);
+ }
+ catch (Exception ex)
+ {
+ LoggingEventSource.Log.LogException(ex);
+ throw;
+ }
+ }
+
+ private void LegacyPath(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ var bag = _bag;
+ if (bag == null)
+ {
+ bag = new()
+ {
+ StaticProperties = _staticProperties
+ };
+
+#pragma warning disable S2696 // Instance members should not write to "static" fields
+ _bag = bag;
+#pragma warning restore S2696 // Instance members should not write to "static" fields
+ }
+
+ bag.Clear();
+ bag.Formatter = formatter;
+ bag.State = state;
+
+ switch (state)
+ {
+ case IReadOnlyList> stateList:
+ bag.DynamicProperties = stateList;
+ break;
+
+ case IEnumerable> stateList:
+ bag.AddRange(stateList);
+ break;
+
+ case null:
+ break;
+
+ default:
+ bag.Add("{OriginalFormat}", state);
+ break;
+ }
+
+ // enrich
+ foreach (var enricher in _enrichers)
+ {
+ enricher.Enrich(bag);
+ }
+
+ // one last dedicated bit of enrichment
+ if (exception != null && _includeStackTraces)
+ {
+ bag.Add(ExceptionStackTrace, GetExceptionStackTrace(exception, _maxStackTraceLength));
+ }
+
+ try
+ {
+ _nextLogger.Log(logLevel, eventId, bag, exception, static (s, e) =>
+ {
+ var fmt = (Func)s.Formatter!;
+ return fmt((TState)s.State!, e);
+ });
+ }
+ catch (Exception ex)
+ {
+ LoggingEventSource.Log.LogException(ex);
+ throw;
}
}
}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerProvider.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerProvider.cs
deleted file mode 100644
index b913abc9027..00000000000
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerProvider.cs
+++ /dev/null
@@ -1,129 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using Microsoft.Extensions.Telemetry.Enrichment;
-using Microsoft.Extensions.Telemetry.Internal;
-using Microsoft.Shared.Diagnostics;
-using OpenTelemetry;
-using OpenTelemetry.Logs;
-
-namespace Microsoft.Extensions.Telemetry.Logging;
-
-///
-/// OpenTelemetry Logger provider class.
-///
-[ProviderAlias("R9")]
-[Experimental]
-public sealed class LoggerProvider : BaseProvider, ILoggerProvider, ISupportExternalScope
-{
- private const int ProcessorShutdownGracePeriodInMs = 5000;
- private readonly ConcurrentDictionary _loggers = new();
- private bool _disposed;
- private IExternalScopeProvider? _scopeProvider;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Logger options.
- /// Collection of enrichers.
- /// Collection of processors.
- internal LoggerProvider(
- IOptions loggingOptions,
- IEnumerable enrichers,
- IEnumerable> processors)
- {
- var options = Throw.IfMemberNull(loggingOptions, loggingOptions.Value);
-
- // Accessing Sdk class https://github.com/open-telemetry/opentelemetry-dotnet/blob/7fd37833711e27a02e169de09f3816d1d9557be4/src/OpenTelemetry/Sdk.cs
- // is just to activate OpenTelemetry .NET SDK defaults along with its Self-Diagnostics.
- _ = Sdk.SuppressInstrumentation;
-
- SelfDiagnostics.EnsureInitialized();
-
- var allProcessors = processors.ToList();
-
- Processor = allProcessors.Count switch
- {
- 0 => null,
- 1 => allProcessors[0],
- _ => new CompositeProcessor(allProcessors)
- };
-
- Enrichers = enrichers.ToArray();
- UseFormattedMessage = options.UseFormattedMessage;
- IncludeScopes = options.IncludeScopes;
- IncludeStackTrace = options.IncludeStackTrace;
- MaxStackTraceLength = options.MaxStackTraceLength;
-
- if (!allProcessors.Exists(p => p is BatchExportProcessor))
- {
- CanUsePropertyBagPool = true;
- }
- }
-
- internal bool CanUsePropertyBagPool { get; }
- internal bool UseFormattedMessage { get; }
- internal bool IncludeScopes { get; }
- internal BaseProcessor? Processor { get; }
- internal ILogEnricher[] Enrichers { get; }
- internal bool IncludeStackTrace { get; }
- internal int MaxStackTraceLength { get; }
-
- ///
- /// Sets external scope information source for logger provider.
- ///
- /// scope provider object.
- public void SetScopeProvider(IExternalScopeProvider scopeProvider)
- {
- _scopeProvider = scopeProvider;
-
- foreach (KeyValuePair entry in _loggers)
- {
- if (entry.Value is Logger logger)
- {
- logger.ScopeProvider = _scopeProvider;
- }
- }
- }
-
- ///
- /// Creates a new Microsoft.Extensions.Logging.ILogger instance.
- ///
- /// The category name for message produced by the logger.
- /// ILogger object.
- public ILogger CreateLogger(string categoryName)
- {
- return _loggers.GetOrAdd(categoryName, static (name, t) => new Logger(name, t)
- {
- ScopeProvider = t._scopeProvider,
- }, this);
- }
-
- ///
- /// Performs tasks related to freeing up resources.
- ///
- /// Parameter indicating whether resources need disposing.
- protected override void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- _ = Processor?.Shutdown(ProcessorShutdownGracePeriodInMs);
- Processor?.Dispose();
- }
-
- _disposed = true;
-
- base.Dispose(disposing);
- }
-}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingBuilder.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingBuilder.cs
new file mode 100644
index 00000000000..5737807f0bc
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingBuilder.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.Telemetry.Logging;
+
+internal sealed class LoggingBuilder : ILoggingBuilder
+{
+ public LoggingBuilder(IServiceCollection services)
+ {
+ Services = services;
+ }
+
+ public IServiceCollection Services { get; }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingExtensions.cs
deleted file mode 100644
index 220972508e4..00000000000
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingExtensions.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using Microsoft.Extensions.Options.Validation;
-using Microsoft.Extensions.Telemetry.Enrichment;
-using Microsoft.Shared.Diagnostics;
-using OpenTelemetry;
-using OpenTelemetry.Logs;
-
-namespace Microsoft.Extensions.Telemetry.Logging;
-
-///
-/// Extensions for configuring logging.
-///
-public static class LoggingExtensions
-{
- ///
- /// Configure logging.
- ///
- /// Logging builder.
- /// Configuration section that contains .
- /// Logging .
- public static ILoggingBuilder AddOpenTelemetryLogging(this ILoggingBuilder builder, IConfigurationSection section)
- {
- _ = Throw.IfNull(builder);
- _ = Throw.IfNull(section);
-
- builder.Services.TryAddLoggerProvider();
- _ = builder.Services.AddValidatedOptions().Bind(section);
-
- return builder;
- }
-
- ///
- /// Configure logging.
- ///
- /// Logging builder.
- /// Logging configuration options.
- /// Logging .
- public static ILoggingBuilder AddOpenTelemetryLogging(this ILoggingBuilder builder, Action configure)
- {
- _ = Throw.IfNull(builder);
- _ = Throw.IfNull(configure);
-
- builder.Services.TryAddLoggerProvider();
-
- _ = builder.Services.AddValidatedOptions().Configure(configure);
-
- return builder;
- }
-
- ///
- /// Configure logging with default options.
- ///
- /// Logging builder.
- /// Logging .
- public static ILoggingBuilder AddOpenTelemetryLogging(this ILoggingBuilder builder) => builder.AddOpenTelemetryLogging(_ => { });
-
- ///
- /// Adds a logging processor to the builder.
- ///
- /// The builder to add the processor to.
- /// Log processor to add.
- /// Returns for chaining.
- public static ILoggingBuilder AddProcessor(this ILoggingBuilder builder, BaseProcessor processor)
- {
- _ = Throw.IfNull(builder);
- _ = Throw.IfNull(processor);
-
- _ = builder.Services.AddSingleton(processor);
-
- return builder;
- }
-
- ///
- /// Adds a logging processor to the builder.
- ///
- /// Type of processor to add.
- /// The builder to add the processor to.
- /// Returns for chaining.
- public static ILoggingBuilder AddProcessor(this ILoggingBuilder builder)
- where T : BaseProcessor
- {
- _ = Throw.IfNull(builder);
-
- _ = builder.Services.AddSingleton, T>();
-
- return builder;
- }
-
- private static void TryAddLoggerProvider(this IServiceCollection services)
- {
- services.TryAddEnumerable(ServiceDescriptor.Singleton(
- sp => new LoggerProvider(
- sp.GetRequiredService>(),
- sp.GetServices(),
- sp.GetServices>())));
- }
-}
diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Log/LoggingOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Log/LoggingOptionsTests.cs
index 81d3195adff..43d813cac46 100644
--- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Log/LoggingOptionsTests.cs
+++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Log/LoggingOptionsTests.cs
@@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Telemetry.Logging.Test.Log;
public class LoggingOptionsTests
{
- private readonly LoggingOptions _sut = new();
+ private readonly ExtendedLoggerFilterOptions _sut = new();
[Fact]
public void CanSetAndGetIncludeScopes()
diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LoggerTests.cs
index 28f2c9eba05..4a7ba34e211 100644
--- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LoggerTests.cs
+++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LoggerTests.cs
@@ -26,17 +26,17 @@ namespace Microsoft.Extensions.Telemetry.Logging.Test;
public sealed class LoggerTests
{
private static LoggerProvider CreateLoggerProvider(
- IOptions? loggingOptions = null,
+ IOptions? loggingOptions = null,
IEnumerable? enrichers = null,
IEnumerable>? processors = null) => new(
- loggingOptions ?? IOptions.Create(new LoggingOptions()),
+ loggingOptions ?? IOptions.Create(new ExtendedLoggerFilterOptions()),
enrichers ?? Enumerable.Empty(),
processors ?? Enumerable.Empty>());
[Fact]
public void CreateLoggerWithNullConfigurationActionThrows()
{
- Action? nullAction = null;
+ Action? nullAction = null;
Assert.Throws(() => LoggerFactory.Create(builder => builder.AddOpenTelemetryLogging(nullAction!)));
}
@@ -426,7 +426,7 @@ public void CreateLoggerReturnsExistingLoggerWhenExists()
[Fact]
public void CreateLoggerProviderWithNullOptions()
{
- Assert.Throws(() => CreateLoggerProvider(IOptions.Create(null!)));
+ Assert.Throws(() => CreateLoggerProvider(IOptions.Create(null!)));
}
[Fact]
@@ -549,7 +549,7 @@ public void DependencyInjectionSetup()
.AddProcessor(new TestProcessor()))
.ConfigureServices(services => services
.AddLogEnricher()
- .Configure(options => options.IncludeScopes = true))
+ .Configure(options => options.IncludeScopes = true))
.Build();
var loggerProvider = (LoggerProvider)host.Services.GetRequiredService();