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();