From 0d3aacd01cc0b8130f8d1af9660ec5c6b226962e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 8 May 2023 11:52:37 +0800 Subject: [PATCH 01/26] WIP --- ...crosoft.Extensions.Logging.Abstractions.cs | 101 ++++++- ...oft.Extensions.Logging.Abstractions.csproj | 5 + .../src/BufferWriter.cs | 124 ++++++++ .../src/ByReference.cs | 31 ++ .../src/LogEntryNew.cs | 146 ++++++++++ .../src/LogEntryPipeline.cs | 44 +++ .../src/LoggerT.cs | 53 +++- .../src/EnrichmentExtensions.cs | 270 ++++++++++++++++++ .../src/LoggingBuilderExtensions.cs | 12 + 9 files changed, 783 insertions(+), 3 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferWriter.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/ByReference.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index 72e6ae6afce77a..ee0666c91d4b16 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -6,6 +6,18 @@ namespace Microsoft.Extensions.Logging { + public ref partial struct BufferWriter + { + private object _dummy; + private int _dummyPrimitive; + public BufferWriter(System.Buffers.IBufferWriter bufferWriter) { throw null; } + public System.Span CurrentSpan { get { throw null; } } + public System.Buffers.IBufferWriter Writer { get { throw null; } } + public void Advance(int len) { } + public void EnsureSize(int minSize) { } + public void Flush() { } + public void Grow(int minSize) { } + } public readonly partial struct EventId : System.IEquatable { private readonly object _dummy; @@ -21,11 +33,23 @@ namespace Microsoft.Extensions.Logging public static bool operator !=(Microsoft.Extensions.Logging.EventId left, Microsoft.Extensions.Logging.EventId right) { throw null; } public override string ToString() { throw null; } } + public delegate void FormatPropertyAction(PropType propertyValue, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); + public delegate void FormatPropertyListAction(ref TState state, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); + public delegate void FormatSpanPropertyAction(System.ReadOnlySpan propertyValue, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); public partial interface IExternalScopeProvider { void ForEachScope(System.Action callback, TState state); System.IDisposable Push(object? state); } + public partial interface ILogEntryProcessor + { + Microsoft.Extensions.Logging.LogEntryHandler GetLogEntryHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); + bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel); + } + public partial interface ILogEntryProcessorFactory + { + Microsoft.Extensions.Logging.ProcessorContext GetProcessor(); + } public partial interface ILogger { System.IDisposable? BeginScope(TState state) where TState : notnull; @@ -41,9 +65,34 @@ public partial interface ILoggerProvider : System.IDisposable { Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName); } + public partial interface ILoggerStateWithMetadata + { + Microsoft.Extensions.Logging.ILogMetadata Metadata { get; } + } public partial interface ILogger : Microsoft.Extensions.Logging.ILogger { } + public partial interface ILogMetadata + { + Microsoft.Extensions.Logging.EventId EventId { get; } + Microsoft.Extensions.Logging.LogLevel LogLevel { get; } + string OriginalFormat { get; } + int PropertyCount { get; } + void AppendFormattedMessage(in TState state, System.Buffers.IBufferWriter buffer); + System.Action> GetMessageFormatter(Microsoft.Extensions.Logging.PropertyCustomFormatter[] customFormatters); + Microsoft.Extensions.Logging.FormatPropertyListAction GetPropertyListFormatter(Microsoft.Extensions.Logging.IPropertyFormatterFactory propertyFormatterFactory); + Microsoft.Extensions.Logging.LogPropertyMetadata GetPropertyMetadata(int index); + System.Func GetStringMessageFormatter(); + } + public partial interface IProcessorFactory + { + Microsoft.Extensions.Logging.ILogEntryProcessor GetProcessor(Microsoft.Extensions.Logging.ILogEntryProcessor nextProcessor); + } + public partial interface IPropertyFormatterFactory + { + Microsoft.Extensions.Logging.FormatPropertyAction GetPropertyFormatter(int propertyIndex, Microsoft.Extensions.Logging.LogPropertyMetadata metadata); + Microsoft.Extensions.Logging.FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, Microsoft.Extensions.Logging.LogPropertyMetadata metadata); + } public partial interface ISupportExternalScope { void SetScopeProvider(Microsoft.Extensions.Logging.IExternalScopeProvider scopeProvider); @@ -53,6 +102,24 @@ public partial class LogDefineOptions public LogDefineOptions() { } public bool SkipEnabledCheck { get { throw null; } set { } } } + public abstract partial class LogEntryHandler + { + protected LogEntryHandler() { } + public abstract void HandleLogEntry(ref Microsoft.Extensions.Logging.LogEntry logEntry); + public abstract bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level); + } + public readonly ref partial struct LogEntry + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public LogEntry(Microsoft.Extensions.Logging.LogLevel level, Microsoft.Extensions.Logging.EventId eventId, ref TState state, ref TEnrichmentProperties enrichmentProperties, System.Exception? exception, System.Func? formatter) { throw null; } + public ref TEnrichmentProperties EnrichmentProperties { get { throw null; } } + public Microsoft.Extensions.Logging.EventId EventId { get { throw null; } } + public System.Exception? Exception { get { throw null; } } + public System.Func? Formatter { get { throw null; } } + public Microsoft.Extensions.Logging.LogLevel LogLevel { get { throw null; } } + public ref TState State { get { throw null; } } + } public static partial class LoggerExtensions { public static System.IDisposable? BeginScope(this Microsoft.Extensions.Logging.ILogger logger, string messageFormat, params object?[] args) { throw null; } @@ -134,9 +201,9 @@ public LoggerMessageAttribute(int eventId, Microsoft.Extensions.Logging.LogLevel public partial class Logger : Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.ILogger { public Logger(Microsoft.Extensions.Logging.ILoggerFactory factory) { } - System.IDisposable? Microsoft.Extensions.Logging.ILogger.BeginScope(TState state) { throw null; } + System.IDisposable Microsoft.Extensions.Logging.ILogger.BeginScope(TState state) { throw null; } bool Microsoft.Extensions.Logging.ILogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) { throw null; } - void Microsoft.Extensions.Logging.ILogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func formatter) { } + void Microsoft.Extensions.Logging.ILogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func formatter) { } } public enum LogLevel { @@ -148,6 +215,36 @@ public enum LogLevel Critical = 5, None = 6, } + public partial struct LogPropertyMetadata + { + private object _dummy; + private int _dummyPrimitive; + public LogPropertyMetadata(string name, string? formatSpecifier, System.Attribute[]? attributes) { throw null; } + public readonly System.Attribute[]? Attributes { get { throw null; } } + public readonly string? FormatSpecifier { get { throw null; } } + public readonly string Name { get { throw null; } } + } + public readonly partial struct ProcessorContext + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public ProcessorContext(Microsoft.Extensions.Logging.ILogEntryProcessor processor, System.Threading.CancellationToken cancellationToken) { throw null; } + public System.Threading.CancellationToken CancellationToken { get { throw null; } } + public Microsoft.Extensions.Logging.ILogEntryProcessor Processor { get { throw null; } } + } + public partial class ProcessorFactory : Microsoft.Extensions.Logging.IProcessorFactory where T : Microsoft.Extensions.Logging.ILogEntryProcessor + { + public ProcessorFactory(System.Func getProcessor) { } + public Microsoft.Extensions.Logging.ILogEntryProcessor GetProcessor(Microsoft.Extensions.Logging.ILogEntryProcessor nextProcessor) { throw null; } + } + public abstract partial class PropertyCustomFormatter + { + protected PropertyCustomFormatter() { } + public virtual void AppendFormatted(int index, int value, System.Buffers.IBufferWriter buffer) { } + public virtual void AppendFormatted(int index, System.ReadOnlySpan value, System.Buffers.IBufferWriter buffer) { } + public virtual void AppendFormatted(int index, string value, System.Buffers.IBufferWriter buffer) { } + public abstract void AppendFormatted(int index, T value, System.Buffers.IBufferWriter buffer); + } } namespace Microsoft.Extensions.Logging.Abstractions { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.csproj index 0b3042fa01cbfd..9725fb48a0f444 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.csproj @@ -6,4 +6,9 @@ + + + + + diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferWriter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferWriter.cs new file mode 100644 index 00000000000000..4e0ebb0f0fd477 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferWriter.cs @@ -0,0 +1,124 @@ +// 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.Buffers; + +namespace Microsoft.Extensions.Logging +{ + public ref struct BufferWriter + { + private Span _currentSpan; + private IBufferWriter _writer; + private int _allocated; + + public BufferWriter(IBufferWriter bufferWriter) + { + _writer = bufferWriter; + _currentSpan = bufferWriter.GetSpan(); + _allocated = _currentSpan.Length; + } + + public IBufferWriter Writer => _writer; + public Span CurrentSpan => _currentSpan; + + public void Advance(int len) + { + _currentSpan = _currentSpan.Slice(len); + } + + public void EnsureSize(int minSize) + { + if (_currentSpan.Length < minSize) + { + Grow(minSize); + } + } + + public void Grow(int minSize) + { + if (_allocated != _currentSpan.Length) + { + Flush(); + } + _currentSpan = _writer.GetSpan(minSize); + _allocated = _currentSpan.Length; + } + + public void Flush() + { + _writer.Advance(_allocated - _currentSpan.Length); + _currentSpan = default; + _allocated = 0; + } + } + + internal static class BufferWriterExtensions + { + public static void Write(ref this BufferWriter writer, ReadOnlySpan value) + { + if (!value.TryCopyTo(writer.CurrentSpan)) + { + writer.Grow(value.Length); + value.CopyTo(writer.CurrentSpan); + } + writer.Advance(value.Length); + } + + public static void Write(ref this BufferWriter writer, ReadOnlySpan value, int alignment) + { + int valueLen = value.Length; + bool leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + int lenNeeded = Math.Max(alignment, valueLen); + int paddingNeeded = lenNeeded - valueLen; + if (writer.CurrentSpan.Length < lenNeeded) + { + writer.Grow(lenNeeded); + } + Span currentSpan = writer.CurrentSpan; + if (leftAlign) + { + currentSpan.Slice(0, paddingNeeded).Fill(' '); + value.CopyTo(currentSpan.Slice(paddingNeeded)); + } + else + { + value.CopyTo(currentSpan.Slice(paddingNeeded)); + currentSpan.Slice(valueLen, paddingNeeded).Fill(' '); + } + writer.Advance(lenNeeded); + } + + /* + public void Append(int value) + { + if (_unusedChars.Length < 20) + { + Grow(20); + } + value.TryFormat(_unusedChars, out int written); + _unusedChars = _unusedChars.Slice(written); + } + + public void Append(T value) + { + if (value is string strVal) + { + Append(strVal); + } + else if(value is int intVal) + { + Append(intVal); + } + else if(value != null) + { + Append(value.ToString()); + } + }*/ + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/ByReference.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/ByReference.cs new file mode 100644 index 00000000000000..ef259e1e63db58 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/ByReference.cs @@ -0,0 +1,31 @@ +// 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.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Logging +{ + // ByReference is meant to be used to represent "ref T" fields. It is working + // around lack of first class support for byref fields in C# and IL. The JIT and + // type loader has special handling for it that turns it into a thin wrapper around ref T. + internal struct ByReference + { + private IntPtr _value; + + public ByReference(ref T value) + { + // TODO-SPAN: This has GC hole. It needs to be JIT intrinsic instead + unsafe { _value = (IntPtr)Unsafe.AsPointer(ref value); } + } + + public ref T Value + { + get + { + // TODO-SPAN: This has GC hole. It needs to be JIT intrinsic instead + unsafe { return ref Unsafe.As(ref *(IntPtr*)_value); } + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs new file mode 100644 index 00000000000000..5303e0496722d1 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -0,0 +1,146 @@ +// 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.Buffers; +using System.Threading; + +namespace Microsoft.Extensions.Logging +{ + public interface ILogEntryProcessorFactory + { + //TODO: should CancellationToken be an IChangeToken or something else? + ProcessorContext GetProcessor(); + } + + public readonly struct ProcessorContext + { + public ILogEntryProcessor Processor { get; } + public CancellationToken CancellationToken { get; } + + public ProcessorContext(ILogEntryProcessor processor, CancellationToken cancellationToken) + { + Processor = processor; + CancellationToken = cancellationToken; + } + } + + public interface ILogEntryProcessor + { + LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); + bool IsEnabled(LogLevel logLevel); + } + + //TODO: Merge with other factory? Handle cancellation? + //Are these factories even needed or could we do it with a Func? + public interface IProcessorFactory + { + ILogEntryProcessor GetProcessor(ILogEntryProcessor nextProcessor); + } + + public class ProcessorFactory : IProcessorFactory where T : ILogEntryProcessor + { + public ProcessorFactory(Func getProcessor) + { + _getProcessor = getProcessor; + } + + private readonly Func _getProcessor; + public ILogEntryProcessor GetProcessor(ILogEntryProcessor nextProcessor) => _getProcessor(nextProcessor); + } + + public abstract class LogEntryHandler + { + public abstract bool IsEnabled(LogLevel level); + public abstract void HandleLogEntry(ref LogEntry logEntry); + } + + //TODO: Not sure if we need to keep this? + public interface ILoggerStateWithMetadata + { + public ILogMetadata Metadata { get; } + } + + public struct LogPropertyMetadata + { + public LogPropertyMetadata(string name, string? formatSpecifier, Attribute[]? attributes) + { + Name = name; + FormatSpecifier = formatSpecifier; + Attributes = attributes; + } + public string Name { get; } + public string? FormatSpecifier { get; } + public Attribute[]? Attributes { get; } + } + + public interface ILogMetadata + { + LogLevel LogLevel { get; } + EventId EventId { get; } + string OriginalFormat { get; } + int PropertyCount { get; } + LogPropertyMetadata GetPropertyMetadata(int index); + void AppendFormattedMessage(in TState state, IBufferWriter buffer); + Action> GetMessageFormatter(PropertyCustomFormatter[] customFormatters); + FormatPropertyListAction GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory); + Func GetStringMessageFormatter(); + } + + public delegate void FormatPropertyListAction(ref TState state, ref BufferWriter bufferWriter); + public delegate void FormatPropertyAction(PropType propertyValue, ref BufferWriter bufferWriter); + public delegate void FormatSpanPropertyAction(scoped ReadOnlySpan propertyValue, ref BufferWriter bufferWriter); + + public interface IPropertyFormatterFactory + { + FormatPropertyAction GetPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata); + FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata); + } + + public abstract class PropertyCustomFormatter + { + //TODO: we can expand this with overrides for other commonly logged value types + public virtual void AppendFormatted(int index, ReadOnlySpan value, IBufferWriter buffer) => AppendFormatted(index, value.ToString(), buffer); + public virtual void AppendFormatted(int index, int value, IBufferWriter buffer) => AppendFormatted(index, value, buffer); + public virtual void AppendFormatted(int index, string value, IBufferWriter buffer) => AppendFormatted(index, value, buffer); + public abstract void AppendFormatted(int index, T value, IBufferWriter buffer); + } + + public readonly ref struct LogEntry + { +#if NET8_0_OR_GREATER + private readonly ref TState _state; + private readonly ref TEnrichmentProperties _enrichmentProperties; +#else + private readonly ByReference _state; + private readonly ByReference _enrichmentProperties; +#endif + + public LogEntry(LogLevel level, EventId eventId, ref TState state, ref TEnrichmentProperties enrichmentProperties, Exception? exception, Func? formatter) + { + LogLevel = level; + EventId = eventId; +#if NET8_0_OR_GREATER + _state = ref state; + _enrichmentProperties = ref enrichmentProperties; +#else + _state = new ByReference(ref state); + _enrichmentProperties = new ByReference(ref enrichmentProperties); +#endif + Exception = exception; + Formatter = formatter; + } + +#if NET8_0_OR_GREATER + public ref TState State => ref _state; + public ref TEnrichmentProperties EnrichmentProperties => ref _enrichmentProperties; +#else + public ref TState State => ref _state.Value; + public ref TEnrichmentProperties EnrichmentProperties => ref _enrichmentProperties.Value; +#endif + public LogLevel LogLevel { get; } + public EventId EventId { get; } + public Exception? Exception { get; } + public Func? Formatter { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs new file mode 100644 index 00000000000000..556f9c05f52328 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Logging +{ + internal interface ILogEntryPipelineFactory + { + public LogEntryPipeline? GetPipeline(ILogMetadata? metadata, object? userState); + } + + internal class LogEntryPipeline + { + public LogEntryPipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) + { + UserState = userState; + IsEnabled = isEnabled; + IsDynamicLevelCheckRequired = isDynamicLevelCheckRequired; + IsUpToDate = true; + } + + public object? UserState { get; } + public bool IsEnabled { get; } + public bool IsDynamicLevelCheckRequired { get; } + public bool IsUpToDate { get; set; } + } + + internal class LogEntryPipeline : LogEntryPipeline + { + public LogEntryPipeline(LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : + base(userState, isEnabled, isDynamicLevelCheckRequired) + { + _firstHandler = handler; + } + + private LogEntryHandler _firstHandler; + + public bool IsEnabledDynamic(LogLevel level) => _firstHandler.IsEnabled(level); + public void HandleLogEntry(ref LogEntry logEntry) => _firstHandler.HandleLogEntry(ref logEntry); + } + + internal struct EmptyEnrichmentPropertyValues + { + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs index b6bf6f882f6990..6f2533d159256e 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using Microsoft.Extensions.Internal; namespace Microsoft.Extensions.Logging @@ -11,8 +12,10 @@ namespace Microsoft.Extensions.Logging /// provided . /// /// The type. - public class Logger : ILogger + public class Logger : ILogger, ILogEntryPipelineFactory { + //private Dictionary _metadataPipelines = new Dictionary(); // metadata -> LogEntryPipeline + //private Dictionary _noMetadataPipelines = new Dictionary(); private readonly ILogger _logger; /// @@ -43,5 +46,53 @@ void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Excep { _logger.Log(logLevel, eventId, state, exception, formatter); } + + LogEntryPipeline? ILogEntryPipelineFactory.GetPipeline(ILogMetadata? metadata, object? userState) + { + if (_logger is ILogEntryPipelineFactory factory) + { + return factory.GetPipeline(metadata, userState); + } + else + { + return null; + } + + /* + if (_logger is not ILogEntryProcessor) + return null; + object pipeline; + if (metadata != null) + { + lock (_metadataPipelines) + { + if (!_metadataPipelines.TryGetValue(metadata, out pipeline)) + { + pipeline = BuildPipeline(metadata); + _metadataPipelines[metadata] = pipeline; + } + } + } + else + { + lock (_noMetadataPipelines) + { + if (!_noMetadataPipelines.TryGetValue(typeof(TState), out pipeline)) + { + pipeline = BuildPipeline(null); + _noMetadataPipelines[typeof(TState)] = pipeline; + } + } + } + return (LogEntryPipeline)pipeline; + */ + } + + /* + private LogEntryPipeline BuildPipeline(ILogMetadata? metadata) + { + return new LogEntryPipeline(((ILogEntryProcessor)_logger).GetLogEntryHandler(metadata), this, true, false); + }*/ } + } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs new file mode 100644 index 00000000000000..f0c735ee3756fb --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs @@ -0,0 +1,270 @@ +// 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.Buffers; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ConsoleApp31.Prototype +{ + /* + public static class EnrichmentExtensions + { + public static ILoggingBuilder Enrich(this ILoggingBuilder builder, string propertyName, Func valueFunc) + { + builder.AddProcessor((sp, next) => + { + IEnumerable props = sp.GetServices(); + EnrichmentPropertiesCollection collection = new EnrichmentPropertiesCollection(); + foreach(var prop in props) + { + collection = prop.AppendTo(collection); + } + return new EnrichmentProcessor(collection, next); + }); + builder.Services.AddSingleton(new EnrichmentProperty(propertyName, valueFunc)); + return builder; + } + + internal abstract class EnrichmentProperty + { + internal abstract EnrichmentPropertiesCollection AppendTo(EnrichmentPropertiesCollection collection); + } + + internal class EnrichmentProperty : EnrichmentProperty + { + public EnrichmentProperty(string name, Func getValue) + { + Name = name; + GetValue = getValue; + } + + public string Name { get; } + public Func GetValue { get; } + + internal override EnrichmentPropertiesCollection AppendTo(EnrichmentPropertiesCollection collection) + { + return collection.AddProperty(Name, GetValue); + } + } + } + + internal class EnrichmentPropertiesCollection + { + internal virtual EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) + { + var collection = new EnrichmentPropertiesCollection(); + collection.Name0 = propertyName; + collection.GetValue0 = getValue; + return collection; + } + + internal virtual LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + return nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + } + } + + internal class EnrichmentProcessor : ILogEntryProcessor + { + private ILogEntryProcessor _nextProcessor; + private EnrichmentPropertiesCollection _propCollection; + + public EnrichmentProcessor(EnrichmentPropertiesCollection collection, ILogEntryProcessor nextProcessor) + { + _propCollection = collection; + _nextProcessor = nextProcessor; + } + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + return _propCollection.GetLogEntryHandler(_nextProcessor, metadata, out enabled, out dynamicEnabledCheckRequired); + } + + public bool IsEnabled(LogLevel logLevel) + { + return _nextProcessor.IsEnabled(logLevel); + } + } + + internal class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection + { + internal string Name0; + internal Func GetValue0; + + internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) + { + var collection = new EnrichmentPropertiesCollection(); + collection.Name0 = Name0; + collection.GetValue0 = GetValue0; + collection.Name1 = propertyName; + collection.GetValue1 = getValue; + return collection; + } + + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); + } + + private class EnrichmentHandler : LogEntryHandler + { + private LogEntryHandler> _nextHandler; + private EnrichmentPropertiesCollection _propertyCollection; + + public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) + { + _nextHandler = nextHandler; + _propertyCollection = propertyCollection; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { +#pragma warning disable SA1129 // Do not use default value type constructor + EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); +#pragma warning restore SA1129 // Do not use default value type constructor + enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; + enrichmentProperties.Value0 = _propertyCollection.GetValue0(); + var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + _nextHandler.HandleLogEntry(ref newLogEntry); + } + + public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); + } + } + + internal class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection + { + internal string Name0; + internal Func GetValue0; + internal string Name1; + internal Func GetValue1; + + internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) + { + var collection = new UnboundedEnrichmentPropertiesCollection(); + collection.Name0 = Name0; + collection.GetValue0 = GetValue0; + collection.Name1 = Name1; + collection.GetValue1 = GetValue1; + collection.OverflowProperties.Add((propertyName, () => getValue())); + return collection; + } + + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); + } + + private class EnrichmentHandler : LogEntryHandler + { + private LogEntryHandler> _nextHandler; + private EnrichmentPropertiesCollection _propertyCollection; + + public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) + { + _nextHandler = nextHandler; + _propertyCollection = propertyCollection; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { +#pragma warning disable SA1129 // Do not use default value type constructor + EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); +#pragma warning restore SA1129 // Do not use default value type constructor + enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; + enrichmentProperties.Value0 = _propertyCollection.GetValue0(); + enrichmentProperties.Value1 = _propertyCollection.GetValue1(); + var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + _nextHandler.HandleLogEntry(ref newLogEntry); + } + + public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); + } + } + + internal class UnboundedEnrichmentPropertiesCollection : EnrichmentPropertiesCollection + { + internal string Name0; + internal Func GetValue0; + internal string Name1; + internal Func GetValue1; + internal List<(string, Func)> OverflowProperties = new List<(string, Func)>(); + + internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) + { + OverflowProperties.Add((propertyName, () => getValue())); + return this; + } + + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); + } + + private class EnrichmentHandler : LogEntryHandler + { + private LogEntryHandler> _nextHandler; + private UnboundedEnrichmentPropertiesCollection _propertyCollection; + + public EnrichmentHandler(LogEntryHandler> nextHandler, UnboundedEnrichmentPropertiesCollection propertyCollection) + { + _nextHandler = nextHandler; + _propertyCollection = propertyCollection; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { +#pragma warning disable SA1129 // Do not use default value type constructor + UnboundedEnrichmentPropertyValues enrichmentProperties = new UnboundedEnrichmentPropertyValues(); +#pragma warning disable SA1129 // Do not use default value type constructor + enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; + enrichmentProperties.Value0 = _propertyCollection.GetValue0(); + enrichmentProperties.Value1 = _propertyCollection.GetValue1(); + int overflowProps = _propertyCollection.OverflowProperties.Count; + enrichmentProperties.ExtraValues = ArrayPool.Shared.Rent(overflowProps); + for (int i = 0; i < overflowProps; i++) + { + enrichmentProperties.ExtraValues[i] = _propertyCollection.OverflowProperties[i].Item2(); + } + var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + _nextHandler.HandleLogEntry(ref newLogEntry); + } + + public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); + } + } + + internal struct EmptyEnrichmentPropertyValues + { + } + + internal struct EnrichmentPropertyValues + { + internal TEnrichmentProperties NestedProperties; + internal T0 Value0; + } + + internal struct EnrichmentPropertyValues + { + internal TEnrichmentProperties NestedProperties; + internal T0 Value0; + internal T1 Value1; + } + + internal struct UnboundedEnrichmentPropertyValues + { + internal TEnrichmentProperties NestedProperties; + internal T0 Value0; + internal T1 Value1; + internal object?[] ExtraValues; + } + */ +} diff --git a/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs index ea3b7b0cb2b36f..59a3f742b8b0e3 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs @@ -60,5 +60,17 @@ public static ILoggingBuilder Configure(this ILoggingBuilder builder, Action(this ILoggingBuilder builder, Func getProcessor) where T : ILogEntryProcessor + //{ + // builder.Services.TryAddSingleton(new ProcessorFactory(getProcessor)); + // return builder; + //} + + //public static ILoggingBuilder AddProcessor(this ILoggingBuilder builder, Func getProcessor) where T : ILogEntryProcessor + //{ + // builder.Services.TryAddSingleton(sp => new ProcessorFactory((next) => getProcessor(sp, next))); + // return builder; + //} } } From b0b9f7fb903e32ae2981e85bb21d981a06edf6f1 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 9 May 2023 13:25:52 +0800 Subject: [PATCH 02/26] WIP --- ...crosoft.Extensions.Logging.Abstractions.cs | 29 +- .../src/CompositeFormat.cs | 357 ++++++++++ .../src/FormattedLogValues.cs | 2 +- .../src/LogDefineOptions.cs | 2 + .../src/LogEntryNew.cs | 1 + .../src/LogEntryPipeline.cs | 8 +- .../src/LogValuesFormatter.cs | 257 +++++-- .../src/LoggerMessage.cs | 635 +++++++++++++++++- ...oft.Extensions.Logging.Abstractions.csproj | 1 + .../ref/Microsoft.Extensions.Logging.cs | 21 +- .../src/DispatchProcessor.cs | 180 +++++ .../src/Logger.cs | 259 +++---- .../src/LoggerFactory.cs | 144 ++-- .../src/LoggerInformation.cs | 44 +- .../src/LoggingBuilderExtensions.cs | 20 +- .../src/PipelineManager.cs | 47 ++ .../tests/Common/ProcessorTests.cs | 103 +++ 17 files changed, 1829 insertions(+), 281 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/CompositeFormat.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index ee0666c91d4b16..09ef7bac5bb45f 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -18,6 +18,9 @@ public void EnsureSize(int minSize) { } public void Flush() { } public void Grow(int minSize) { } } + public partial struct EmptyEnrichmentPropertyValues + { + } public readonly partial struct EventId : System.IEquatable { private readonly object _dummy; @@ -41,6 +44,10 @@ public partial interface IExternalScopeProvider void ForEachScope(System.Action callback, TState state); System.IDisposable Push(object? state); } + public partial interface ILogEntryPipelineFactory + { + Microsoft.Extensions.Logging.LogEntryPipeline? GetPipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState); + } public partial interface ILogEntryProcessor { Microsoft.Extensions.Logging.LogEntryHandler GetLogEntryHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); @@ -100,6 +107,7 @@ public partial interface ISupportExternalScope public partial class LogDefineOptions { public LogDefineOptions() { } + public System.Attribute[]?[]? ParameterAttributes { get { throw null; } set { } } public bool SkipEnabledCheck { get { throw null; } set { } } } public abstract partial class LogEntryHandler @@ -108,6 +116,20 @@ protected LogEntryHandler() { } public abstract void HandleLogEntry(ref Microsoft.Extensions.Logging.LogEntry logEntry); public abstract bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level); } + public partial class LogEntryPipeline + { + public LogEntryPipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) { } + public bool IsDynamicLevelCheckRequired { get { throw null; } } + public bool IsEnabled { get { throw null; } } + public bool IsUpToDate { get { throw null; } set { } } + public object? UserState { get { throw null; } } + } + public partial class LogEntryPipeline : Microsoft.Extensions.Logging.LogEntryPipeline + { + public LogEntryPipeline(Microsoft.Extensions.Logging.LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base (default(object), default(bool), default(bool)) { } + public void HandleLogEntry(ref Microsoft.Extensions.Logging.LogEntry logEntry) { } + public bool IsEnabledDynamic(Microsoft.Extensions.Logging.LogLevel level) { throw null; } + } public readonly ref partial struct LogEntry { private readonly object _dummy; @@ -174,10 +196,12 @@ public static partial class LoggerMessage public static System.Func DefineScope(string formatString) { throw null; } public static System.Func DefineScope(string formatString) { throw null; } public static System.Func DefineScope(string formatString) { throw null; } + public static Microsoft.Extensions.Logging.LoggerMessage.Log Define(Microsoft.Extensions.Logging.ILogMetadata metadata, Microsoft.Extensions.Logging.LogDefineOptions? options = null) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } + public static Microsoft.Extensions.Logging.LoggerMessage.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options = null) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString) { throw null; } @@ -186,6 +210,8 @@ public static partial class LoggerMessage public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } + public delegate void Action(T0 v0, T1 v1, T2 v2, T3 v3, T4 v4, T5 v5, T6 v6, T7 v7, T8 v8, T9 v9, T10 v10, T11 v11, T12 v12, T13 v13, T14 v14, T15 v15, T16 v16, T17 v17, T18 v18, T19 v19, T20 v20, T21 v21); + public delegate void Log(Microsoft.Extensions.Logging.ILogger logger, ref TState state, System.Exception? exception); } [System.AttributeUsageAttribute(System.AttributeTargets.Method)] public sealed partial class LoggerMessageAttribute : System.Attribute @@ -198,9 +224,10 @@ public LoggerMessageAttribute(int eventId, Microsoft.Extensions.Logging.LogLevel public string Message { get { throw null; } set { } } public bool SkipEnabledCheck { get { throw null; } set { } } } - public partial class Logger : Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.ILogger + public partial class Logger : Microsoft.Extensions.Logging.ILogEntryPipelineFactory, Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.ILogger { public Logger(Microsoft.Extensions.Logging.ILoggerFactory factory) { } + Microsoft.Extensions.Logging.LogEntryPipeline Microsoft.Extensions.Logging.ILogEntryPipelineFactory.GetPipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState) { throw null; } System.IDisposable Microsoft.Extensions.Logging.ILogger.BeginScope(TState state) { throw null; } bool Microsoft.Extensions.Logging.ILogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) { throw null; } void Microsoft.Extensions.Logging.ILogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func formatter) { } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/CompositeFormat.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/CompositeFormat.cs new file mode 100644 index 00000000000000..3c450c72d1aba1 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/CompositeFormat.cs @@ -0,0 +1,357 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Microsoft.Extensions.Logging +{ + /// Represents a parsed composite format string. + [DebuggerDisplay("{Format}")] + internal sealed class InternalCompositeFormat + { + /// The parsed segments that make up the composite format string. + /// + /// Every segment represents either a literal or a format hole, based on whether Literal + /// is non-null or ArgIndex is non-negative. + /// + internal readonly (string? Literal, int ArgIndex, int Alignment, string? Format)[] _segments; + /// The sum of the lengths of all of the literals in . + internal readonly int _literalLength; + /// The number of segments in that represent format holes. + internal readonly int _formattedCount; + /// The number of args required to satisfy the format holes. + /// This is equal to one more than the largest index required by any format hole. + internal readonly int _argsRequired; + + /// Initializes the instance. + /// The composite format string that was parsed. + /// The parsed segments. + private InternalCompositeFormat(string format, (string? Literal, int ArgIndex, int Alignment, string? Format)[] segments) + { + // Store the format. + Debug.Assert(format is not null); + Format = format; + + // Store the segments. + Debug.Assert(segments is not null); + _segments = segments; + + // Compute derivative information from the segments. + int literalLength = 0, formattedCount = 0, argsRequired = 0; + foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in segments) + { + Debug.Assert((segment.Literal is not null) ^ (segment.ArgIndex >= 0), "The segment should represent a literal or a format hole, but not both."); + + if (segment.Literal is string literal) + { + literalLength += literal.Length; // no concern about overflow as these were parsed out of a single string + } + else if (segment.ArgIndex >= 0) + { + formattedCount++; + argsRequired = Math.Max(argsRequired, segment.ArgIndex + 1); + } + } + + // Store the derivative information. + Debug.Assert(literalLength >= 0); + Debug.Assert(formattedCount >= 0); + Debug.Assert(formattedCount == 0 || argsRequired > 0); + _literalLength = literalLength; + _formattedCount = formattedCount; + _argsRequired = argsRequired; + } + + /// Parse the composite format string . + /// The string to parse. + /// The parsed . + /// is null. + /// A format item in is invalid. + public static InternalCompositeFormat Parse(string format) + { + ThrowHelper.ThrowIfNull(format); + + if (!TryParse(format, out InternalCompositeFormat? compositeFormat)) + { + throw new FormatException("placeholder"); + } + + return compositeFormat; + } + + /// Try to parse the composite format string . + /// The string to parse. + /// The parsed if parsing was successful; otherwise, null. + /// the can be parsed successfully; otherwise, . + public static bool TryParse(string? format, [NotNullWhen(true)] out InternalCompositeFormat? compositeFormat) + { + if (format is not null) + { + var segments = new List<(string? Literal, int ArgIndex, int Alignment, string? Format)>(); + if (TryParseLiterals(format.AsSpan(), segments)) + { + compositeFormat = new InternalCompositeFormat(format, segments.ToArray()); + return true; + } + } + + compositeFormat = null; + return false; + } + + /// Gets the original composite format string used to create this instance. + public string Format { get; } + + /// Throws an exception if the specified number of arguments is fewer than the number required. + /// The number of arguments provided by the caller. + /// An insufficient number of arguments were provided. + internal void ValidateNumberOfArgs(int numArgs) + { + if (numArgs < _argsRequired) + { + throw new FormatException("placeholder"); + } + } + + /// Parse the composite format string into segments. + /// The format string. + /// The list into which to store the segments. + /// true if the format string can be parsed successfully; otherwise, false. + private static bool TryParseLiterals(ReadOnlySpan format, List<(string? Literal, int ArgIndex, int Alignment, string? Format)> segments) + { + // This parsing logic is copied from string.Format. It's the same code modified to not format + // as part of parsing and instead store the parsed literals and argument specifiers (alignment + // and format) for later use. + + // Rather than parsing directly into the segments list, literals are parsed into a reusable builder. + // Due to the nature of the parsing logic copied from string.Format, and our desire not to veer from + // it significantly in order to maintain compatibility and accidental regression, multiple literals + // next to each other might be parsed separately due to braces in between them. This builder then + // allows us to merge those segments back together easily prior to their being appended to the list. + var vsb = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); + + // Repeatedly find the next hole and process it. + int pos = 0; + char ch; + while (true) + { + // Skip until either the end of the input or the first unescaped opening brace, whichever comes first. + // Along the way we need to also unescape escaped closing braces. + while (true) + { + // Find the next brace. If there isn't one, the remainder of the input is text to be appended, and we're done. + ReadOnlySpan remainder = format.Slice(pos); + int countUntilNextBrace = remainder.IndexOfAny('{', '}'); + if (countUntilNextBrace < 0) + { + vsb.Append(remainder); + segments.Add((vsb.ToString(), -1, 0, null)); + return true; + } + + // Append the text until the brace. + vsb.Append(remainder.Slice(0, countUntilNextBrace)); + pos += countUntilNextBrace; + + // Get the brace. It must be followed by another character, either a copy of itself in the case of being + // escaped, or an arbitrary character that's part of the hole in the case of an opening brace. + char brace = format[pos]; + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + if (brace == ch) + { + vsb.Append(ch); + pos++; + continue; + } + + // This wasn't an escape, so it must be an opening brace. + if (brace != '{') + { + return false; + } + + // Proceed to parse the hole. + segments.Add((vsb.ToString(), -1, 0, null)); + vsb.Length = 0; + break; + } + + // We're now positioned just after the opening brace of an argument hole, which consists of + // an opening brace, an index, an optional width preceded by a comma, and an optional format + // preceded by a colon, with arbitrary amounts of spaces throughout. + int width = 0; + string? itemFormat = null; // used if itemFormat is null + + // First up is the index parameter, which is of the form: + // at least on digit + // optional any number of spaces + // We've already read the first digit into ch. + Debug.Assert(format[pos - 1] == '{'); + Debug.Assert(ch != '{'); + int index = ch - '0'; + if ((uint)index >= 10u) + { + return false; + } + + // Common case is a single digit index followed by a closing brace. If it's not a closing brace, + // proceed to finish parsing the full hole format. + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + if (ch != '}') + { + // Continue consuming optional additional digits. + while (IsAsciiDigit(ch)) + { + index = index * 10 + ch - '0'; + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + + // Consume optional whitespace. + while (ch == ' ') + { + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + + // Parse the optional alignment, which is of the form: + // comma + // optional any number of spaces + // optional - + // at least one digit + // optional any number of spaces + if (ch == ',') + { + // Consume optional whitespace. + do + { + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + while (ch == ' '); + + // Consume an optional minus sign indicating left alignment. + int leftJustify = 1; + if (ch == '-') + { + leftJustify = -1; + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + + // Parse alignment digits. The read character must be a digit. + width = ch - '0'; + if ((uint)width >= 10u) + { + return false; + } + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + while (IsAsciiDigit(ch)) + { + width = width * 10 + ch - '0'; + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + width *= leftJustify; + + // Consume optional whitespace + while (ch == ' ') + { + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + } + } + + // The next character needs to either be a closing brace for the end of the hole, + // or a colon indicating the start of the format. + if (ch != '}') + { + if (ch != ':') + { + // Unexpected character + return false; + } + + // Search for the closing brace; everything in between is the format, + // but opening braces aren't allowed. + int startingPos = pos; + while (true) + { + if (!TryMoveNext(format, ref pos, out ch)) + { + return false; + } + + if (ch == '}') + { + // Argument hole closed + break; + } + + if (ch == '{') + { + // Braces inside the argument hole are not supported + return false; + } + } + + startingPos++; + itemFormat = format.Slice(startingPos, pos - startingPos).ToString(); + } + } + + Debug.Assert(format[pos] == '}'); + pos++; + + segments.Add((null, index, width, itemFormat)); + + // Continue parsing the rest of the format string. + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool TryMoveNext(ReadOnlySpan format, ref int pos, out char nextChar) + { + pos++; + if ((uint)pos >= (uint)format.Length) + { + nextChar = '\0'; + return false; + } + + nextChar = format[pos]; + return true; + } + } + + internal const int StackallocCharBufferSizeLimit = 256; + private static bool IsAsciiDigit(char c) => IsBetween(c, '0', '9'); + private static bool IsBetween(char c, char minInclusive, char maxInclusive) => + (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs index 782f9bce5ab987..037ec63bdcd3ee 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs @@ -84,7 +84,7 @@ public int Count return 1; } - return _formatter.ValueNames.Count + 1; + return _formatter.PropertyCount + 1; } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogDefineOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogDefineOptions.cs index 48016fcd2b7814..6537051cbf8b85 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogDefineOptions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogDefineOptions.cs @@ -14,5 +14,7 @@ public class LogDefineOptions /// Gets or sets the flag to skip IsEnabled check for the logging method. /// public bool SkipEnabledCheck { get; set; } + + public Attribute[]?[]? ParameterAttributes { get; set; } } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index 5303e0496722d1..0571ffd02a2719 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -112,6 +112,7 @@ public readonly ref struct LogEntry private readonly ref TState _state; private readonly ref TEnrichmentProperties _enrichmentProperties; #else + // TODO: Explore making intrinsic. Or convert to regular fields. private readonly ByReference _state; private readonly ByReference _enrichmentProperties; #endif diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs index 556f9c05f52328..87c0636d2c75f4 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs @@ -3,12 +3,12 @@ namespace Microsoft.Extensions.Logging { - internal interface ILogEntryPipelineFactory + public interface ILogEntryPipelineFactory { public LogEntryPipeline? GetPipeline(ILogMetadata? metadata, object? userState); } - internal class LogEntryPipeline + public class LogEntryPipeline { public LogEntryPipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) { @@ -24,7 +24,7 @@ public LogEntryPipeline(object? userState, bool isEnabled, bool isDynamicLevelCh public bool IsUpToDate { get; set; } } - internal class LogEntryPipeline : LogEntryPipeline + public class LogEntryPipeline : LogEntryPipeline { public LogEntryPipeline(LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base(userState, isEnabled, isDynamicLevelCheckRequired) @@ -38,7 +38,7 @@ public LogEntryPipeline(LogEntryHandler h public void HandleLogEntry(ref LogEntry logEntry) => _firstHandler.HandleLogEntry(ref logEntry); } - internal struct EmptyEnrichmentPropertyValues + public struct EmptyEnrichmentPropertyValues { } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs index a833220ed57fed..e771ceedd98bc8 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs @@ -4,37 +4,49 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Runtime.CompilerServices; using System.Text; namespace Microsoft.Extensions.Logging { + internal class LogValuesMetadata : LogValuesFormatter + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, Attribute[]?[]? attributes) : base(format, attributes) + { + LogLevel = level; + EventId = eventId; + } + + public string FinalFormat => CompositeFormat.Format; + public LogLevel LogLevel { get; } + public EventId EventId { get; } + } + /// /// Formatter to convert the named format items like {NamedformatItem} to format. /// - internal sealed class LogValuesFormatter + internal class LogValuesFormatter { private const string NullValue = "(null)"; private static readonly char[] FormatDelimiters = { ',', ':' }; - private readonly List _valueNames = new List(); -#if NET8_0_OR_GREATER - private readonly CompositeFormat _format; -#else - private readonly string _format; -#endif + private readonly LogPropertyMetadata[]? _metadata; + private readonly InternalCompositeFormat _format; // NOTE: If this assembly ever builds for netcoreapp, the below code should change to: // - Be annotated as [SkipLocalsInit] to avoid zero'ing the stackalloc'd char span // - Format _valueNames.Count directly into a span - public LogValuesFormatter(string format) + public LogValuesFormatter(string format, Attribute[]?[]? attributes = null) { ThrowHelper.ThrowIfNull(format); OriginalFormat = format; var vsb = new ValueStringBuilder(stackalloc char[256]); + List metadata = new List(); int scanIndex = 0; int endIndex = format.Length; @@ -44,12 +56,7 @@ public LogValuesFormatter(string format) if (scanIndex == 0 && openBraceIndex == endIndex) { // No holes found. - _format = -#if NET8_0_OR_GREATER - CompositeFormat.Parse(format); -#else - format; -#endif + _format = InternalCompositeFormat.Parse(format); return; } @@ -64,26 +71,33 @@ public LogValuesFormatter(string format) { // Format item syntax : { index[,alignment][ :formatString] }. int formatDelimiterIndex = FindIndexOfAny(format, FormatDelimiters, openBraceIndex, closeBraceIndex); + int colonIndex = format.IndexOf(':', openBraceIndex, closeBraceIndex - openBraceIndex); vsb.Append(format.AsSpan(scanIndex, openBraceIndex - scanIndex + 1)); - vsb.Append(_valueNames.Count.ToString()); - _valueNames.Add(format.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1)); + vsb.Append(metadata.Count.ToString()); + string propName = format.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1); vsb.Append(format.AsSpan(formatDelimiterIndex, closeBraceIndex - formatDelimiterIndex + 1)); - + string? propFormat = null; + if (colonIndex != -1) + { + propFormat = format.Substring(colonIndex + 1, closeBraceIndex - colonIndex - 1); + } + Attribute[]? propAttributes = attributes != null && attributes.Length >= metadata.Count ? attributes[metadata.Count] : null; + metadata.Add(new LogPropertyMetadata(propName, propFormat, propAttributes)); scanIndex = closeBraceIndex + 1; } } - _format = -#if NET8_0_OR_GREATER - CompositeFormat.Parse(vsb.ToString()); -#else - vsb.ToString(); -#endif + _metadata = metadata.ToArray(); + _format = InternalCompositeFormat.Parse(vsb.ToString()); } public string OriginalFormat { get; private set; } - public List ValueNames => _valueNames; + public InternalCompositeFormat CompositeFormat => _format; + public int PropertyCount => _metadata != null ? _metadata.Length : 0; + public string GetValueName(int index) => _metadata![index].Name; + + public LogPropertyMetadata GetPropertyMetadata(int index) => _metadata![index]; private static int FindBraceIndex(string format, char brace, int startIndex, int endIndex) { @@ -163,7 +177,7 @@ public string Format(object?[]? values) } } - return string.Format(CultureInfo.InvariantCulture, _format, formattedValues ?? Array.Empty()); + return string.Format(CultureInfo.InvariantCulture, _format.Format, formattedValues ?? Array.Empty()); } // NOTE: This method mutates the items in the array if needed to avoid extra allocations, and should only be used when caller expects this to happen @@ -177,69 +191,53 @@ internal string FormatWithOverwrite(object?[]? values) } } - return string.Format(CultureInfo.InvariantCulture, _format, values ?? Array.Empty()); + return string.Format(CultureInfo.InvariantCulture, _format.Format, values ?? Array.Empty()); } internal string Format() { -#if NET8_0_OR_GREATER return _format.Format; -#else - return _format; -#endif } - -#if NET8_0_OR_GREATER internal string Format(TArg0 arg0) { - object? arg0String = null; + string? arg0String = null; return !TryFormatArgumentIfNullOrEnumerable(arg0, ref arg0String) ? - string.Format(CultureInfo.InvariantCulture, _format, arg0) : - string.Format(CultureInfo.InvariantCulture, _format, arg0String); + string.Format(CultureInfo.InvariantCulture, _format.Format, arg0) : + string.Format(CultureInfo.InvariantCulture, _format.Format, arg0String); } internal string Format(TArg0 arg0, TArg1 arg1) { - object? arg0String = null, arg1String = null; + string? arg0String = null, arg1String = null; return !TryFormatArgumentIfNullOrEnumerable(arg0, ref arg0String) && !TryFormatArgumentIfNullOrEnumerable(arg1, ref arg1String) ? - string.Format(CultureInfo.InvariantCulture, _format, arg0, arg1) : - string.Format(CultureInfo.InvariantCulture, _format, arg0String ?? arg0, arg1String ?? arg1); + string.Format(CultureInfo.InvariantCulture, _format.Format, arg0, arg1) : + string.Format(CultureInfo.InvariantCulture, _format.Format, (object?)arg0String ?? arg0, (object?)arg1String ?? arg1); } internal string Format(TArg0 arg0, TArg1 arg1, TArg2 arg2) { - object? arg0String = null, arg1String = null, arg2String = null; + string? arg0String = null, arg1String = null, arg2String = null; return !TryFormatArgumentIfNullOrEnumerable(arg0, ref arg0String) && !TryFormatArgumentIfNullOrEnumerable(arg1, ref arg1String) && !TryFormatArgumentIfNullOrEnumerable(arg2, ref arg2String) ? - string.Format(CultureInfo.InvariantCulture, _format, arg0, arg1, arg2) : - string.Format(CultureInfo.InvariantCulture, _format, arg0String ?? arg0, arg1String ?? arg1, arg2String ?? arg2); + string.Format(CultureInfo.InvariantCulture, _format.Format, arg0, arg1, arg2) : + string.Format(CultureInfo.InvariantCulture, _format.Format, (object?)arg0String ?? arg0, (object?)arg1String ?? arg1, (object?)arg2String ?? arg2); } -#else - internal string Format(object? arg0) => - string.Format(CultureInfo.InvariantCulture, _format, FormatArgument(arg0)); - - internal string Format(object? arg0, object? arg1) => - string.Format(CultureInfo.InvariantCulture, _format, FormatArgument(arg0), FormatArgument(arg1)); - - internal string Format(object? arg0, object? arg1, object? arg2) => - string.Format(CultureInfo.InvariantCulture, _format, FormatArgument(arg0), FormatArgument(arg1), FormatArgument(arg2)); -#endif public KeyValuePair GetValue(object?[] values, int index) { - if (index < 0 || index > _valueNames.Count) + if (index < 0 || index > PropertyCount) { throw new IndexOutOfRangeException(nameof(index)); } - if (_valueNames.Count > index) + if (PropertyCount > index) { - return new KeyValuePair(_valueNames[index], values[index]); + return new KeyValuePair(_metadata![index].Name, values[index]); } return new KeyValuePair("{OriginalFormat}", OriginalFormat); @@ -248,22 +246,165 @@ internal string Format(object? arg0, object? arg1, object? arg2) => public IEnumerable> GetValues(object[] values) { var valueArray = new KeyValuePair[values.Length + 1]; - for (int index = 0; index != _valueNames.Count; ++index) + for (int index = 0; index != PropertyCount; ++index) { - valueArray[index] = new KeyValuePair(_valueNames[index], values[index]); + valueArray[index] = new KeyValuePair(_metadata![index].Name, values[index]); } valueArray[valueArray.Length - 1] = new KeyValuePair("{OriginalFormat}", OriginalFormat); return valueArray; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static void AppendFormattedPropertyValue(T value, ref BufferWriter bufferWriter, int alignment, string? format) + { + if (value is string valueString) + { + if (alignment == 0) + { + bufferWriter.Write(valueString.AsSpan()); + } + else + { + AppendFormattedPropertyValueAligned(valueString, ref bufferWriter, alignment, format); + } + } + else + { + AppendFormattedPropertyValueNonString(value, ref bufferWriter, alignment, format); + } + } + + + protected static void AppendFormattedPropertyValueNonString(T value, ref BufferWriter bufferWriter, int alignment, string? format) + { + if (alignment == 0) + { +#if NET8_0_OR_GREATER + if (value is ISpanFormattable) + { + bufferWriter.EnsureSize(32); + if (((ISpanFormattable)value).TryFormat(bufferWriter.CurrentSpan, out int charsWritten, format, null)) + { + bufferWriter.Advance(charsWritten); + return; + } + } +#endif + bufferWriter.Write(FormatPropertyValue(value, format).AsSpan()); + } + else + { + AppendFormattedPropertyValueAligned(value, ref bufferWriter, alignment, format); + } + } + + protected static void AppendFormattedPropertyValueAligned(T value, ref BufferWriter bufferWriter, int alignment, string? format) + { + bool leftAlign = false; + int paddingNeeded; + Span span; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } +#if NET8_0_OR_GREATER + if (value is ISpanFormattable) + { + bufferWriter.EnsureSize(Math.Max(32, alignment)); + span = bufferWriter.CurrentSpan; + if (((ISpanFormattable)value).TryFormat(span, out int charsWritten, format, CultureInfo.InvariantCulture)) + { + paddingNeeded = alignment - charsWritten; + if (paddingNeeded <= 0) + { + bufferWriter.Advance(charsWritten); + return; + } + if (leftAlign) + { + span.Slice(charsWritten, paddingNeeded).Fill(' '); + } + else + { + span.Slice(0, charsWritten).CopyTo(span.Slice(paddingNeeded)); + span.Slice(0, paddingNeeded).Fill(' '); + } + bufferWriter.Advance(alignment); + return; + } + } +#endif + + string unpadded = FormatPropertyValue(value, format); + paddingNeeded = alignment - unpadded.Length; + bufferWriter.EnsureSize(Math.Max(unpadded.Length, alignment)); + span = bufferWriter.CurrentSpan; + if (paddingNeeded <= 0) + { + bufferWriter.Write(unpadded.AsSpan()); + return; + } + + if (leftAlign) + { + unpadded.AsSpan().CopyTo(span); + span.Slice(unpadded.Length, paddingNeeded).Fill(' '); + } + else + { + span.Slice(0, paddingNeeded).Fill(' '); + unpadded.AsSpan().CopyTo(span.Slice(paddingNeeded)); + } + bufferWriter.Advance(alignment); + + } + + protected static string FormatPropertyValue(T value, string? format) + { + string? s; + if (value is IFormattable) + { + s = ((IFormattable)value).ToString(format, null); + } + else if (value is not string && value is IEnumerable enumerable) + { + var vsb = new ValueStringBuilder(stackalloc char[256]); + bool first = true; + foreach (object? e in enumerable) + { + if (!first) + { + vsb.Append(", "); + } + + vsb.Append(e != null ? e.ToString() : NullValue); + first = false; + } + return vsb.ToString(); + } + else + { + s = value?.ToString(); + } + if (s == null) + { + return NullValue; + } + else + { + return s; + } + } + private static object FormatArgument(object? value) { - object? stringValue = null; + string? stringValue = null; return TryFormatArgumentIfNullOrEnumerable(value, ref stringValue) ? stringValue : value!; } - private static bool TryFormatArgumentIfNullOrEnumerable(object? value, [NotNullWhen(true)] ref object? stringValue) + private static bool TryFormatArgumentIfNullOrEnumerable(object? value, [NotNullWhen(true)] ref string? stringValue) { if (value == null) { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs index 1f010e66470328..1a3c8e1f8a3fbf 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using Microsoft.Extensions.Logging.Abstractions; +using static Microsoft.Extensions.Logging.LoggerMessage; namespace Microsoft.Extensions.Logging { @@ -51,7 +53,10 @@ public static class LoggerMessage { LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 2); - return (logger, arg1, arg2) => logger.BeginScope(new LogValues(formatter, arg1, arg2)); + return (logger, arg1, arg2) => + { + return logger.BeginScope(new LogValues(formatter, arg1, arg2)); + }; } /// @@ -204,6 +209,58 @@ void Log(ILogger logger, T1 arg1, Exception? exception) }; } + public delegate void Log(ILogger logger, ref TState state, Exception? exception); + + public static Log Define(ILogMetadata metadata, LogDefineOptions? options = null) + { + LogEntryPipeline? pipeline = null; + bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); + return Log; + + void Log(ILogger logger, ref TState state, Exception? exception) + { + LogEntryPipeline? pipelineSnapshot = pipeline; + if (pipelineSnapshot != null && pipelineSnapshot.UserState == logger && pipelineSnapshot.IsUpToDate) + { + if (!pipelineSnapshot.IsEnabled || + (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(metadata.LogLevel))) + return; + EmptyEnrichmentPropertyValues props = default; + LogEntry entry = new LogEntry(metadata.LogLevel, metadata.EventId, ref state, ref props, exception, null); + pipelineSnapshot.HandleLogEntry(ref entry); + } + else + { + LogSlowPath(logger, ref state, exception); + } + } + + void LogSlowPath(ILogger logger, ref TState state, Exception? exception) + { + LogEntryPipeline? pipelineSnapshot = null; + EmptyEnrichmentPropertyValues properties = default; + LogEntry entry = new LogEntry(metadata.LogLevel, metadata.EventId, ref state, ref properties, exception, null); + if (logger is ILogEntryPipelineFactory) + { + pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetPipeline(metadata, logger); + pipeline = pipelineSnapshot; + } + if (pipelineSnapshot != null) + { + if (!pipelineSnapshot.IsEnabled || + (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(metadata.LogLevel))) + return; + pipelineSnapshot.HandleLogEntry(ref entry); + } + else + { + if (needFullEnabledCheck && logger.IsEnabled(metadata.LogLevel)) + return; + logger.Log(entry.LogLevel, entry.EventId, entry.State, entry.Exception, metadata.GetStringMessageFormatter()); + } + } + } + /// /// Creates a delegate which can be invoked for logging a message. /// @@ -228,25 +285,153 @@ void Log(ILogger logger, T1 arg1, Exception? exception) /// A delegate which when invoked creates a log message. public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 2); + LogValuesMetadata metadata = LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterAttributes); + LogEntryPipeline>? pipeline = null; + bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); + return Log; void Log(ILogger logger, T1 arg1, T2 arg2, Exception? exception) { - logger.Log(logLevel, eventId, new LogValues(formatter, arg1, arg2), exception, LogValues.Callback); + LogEntryPipeline>? pipelineSnapshot = pipeline; + if (pipelineSnapshot != null && pipelineSnapshot.UserState == logger && pipelineSnapshot.IsUpToDate) + { + if (!pipelineSnapshot.IsEnabled || + (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) + return; + LogValues state = new LogValues(metadata, arg1, arg2); + EmptyEnrichmentPropertyValues props = default; + LogEntry, EmptyEnrichmentPropertyValues> entry = new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref props, exception, LogValues.Callback); + pipelineSnapshot.HandleLogEntry(ref entry); + } + else + { + LogSlowPath(logger, arg1, arg2, exception); + } + } + + void LogSlowPath(ILogger logger, T1 arg1, T2 arg2, Exception? exception) + { + LogEntryPipeline>? pipelineSnapshot = null; + LogValues state = new LogValues(metadata, arg1, arg2); + EmptyEnrichmentPropertyValues properties = default; + LogEntry, EmptyEnrichmentPropertyValues> entry = new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref properties, exception, LogValues.Callback); + if (logger is ILogEntryPipelineFactory) + { + pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetPipeline(metadata, logger); + pipeline = pipelineSnapshot; + } + if (pipelineSnapshot != null) + { + if (!pipelineSnapshot.IsEnabled || + (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) + return; + pipelineSnapshot.HandleLogEntry(ref entry); + } + else + { + if (needFullEnabledCheck && logger.IsEnabled(logLevel)) + return; + logger.Log(entry.LogLevel, entry.EventId, entry.State, entry.Exception, LogValues.Callback); + } } + } - if (options != null && options.SkipEnabledCheck) + public delegate void Action(T0 v0, T1 v1, T2 v2, T3 v3, T4 v4, T5 v5, T6 v6, T7 v7, T8 v8, T9 v9, T10 v10, T11 v11, T12 v12, T13 v13, T14 v14, T15 v15, T16 v16, T17 v17, T18 v18, T19 v19, T20 v20, T21 v21); + + public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options = null) + { + LogValuesMetadata metadata = + LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterAttributes); + LogEntryPipeline>? pipeline = null; + bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); + return Log; + + void Log(ILogger logger, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16, T17 arg17, T18 arg18, T19 arg19, Exception? exception) { - return Log; + LogEntryPipeline>? pipelineSnapshot = pipeline; + if (pipelineSnapshot != null && pipelineSnapshot.UserState == logger && pipelineSnapshot.IsUpToDate) + { + if (!pipelineSnapshot.IsEnabled || + (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) + return; + LogValues state = new LogValues(metadata); + state._value0 = arg0; + state._value1 = arg1; + state._value2 = arg2; + state._value3 = arg3; + state._value4 = arg4; + state._value5 = arg5; + state._value6 = arg6; + state._value7 = arg7; + state._value8 = arg8; + state._value9 = arg9; + state._value10 = arg10; + state._value11 = arg11; + state._value12 = arg12; + state._value13 = arg13; + state._value14 = arg14; + state._value15 = arg15; + state._value16 = arg16; + state._value17 = arg17; + state._value18 = arg18; + state._value19 = arg19; + EmptyEnrichmentPropertyValues properties = default; + LogEntry, EmptyEnrichmentPropertyValues> entry = + new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref properties, exception, null); + pipelineSnapshot.HandleLogEntry(ref entry); + } + else + { + LogSlowPath(logger, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, exception); + } } - return (logger, arg1, arg2, exception) => + void LogSlowPath(ILogger logger, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16, T17 arg17, T18 arg18, T19 arg19, Exception? exception) { - if (logger.IsEnabled(logLevel)) + LogEntryPipeline>? pipelineSnapshot = null; + LogValues state = new LogValues(metadata); + state._value0 = arg0; + state._value1 = arg1; + state._value2 = arg2; + state._value3 = arg3; + state._value4 = arg4; + state._value5 = arg5; + state._value6 = arg6; + state._value7 = arg7; + state._value8 = arg8; + state._value9 = arg9; + state._value10 = arg10; + state._value11 = arg11; + state._value12 = arg12; + state._value13 = arg13; + state._value14 = arg14; + state._value15 = arg15; + state._value16 = arg16; + state._value17 = arg17; + state._value18 = arg18; + state._value19 = arg19; + EmptyEnrichmentPropertyValues properties = default; + LogEntry, EmptyEnrichmentPropertyValues> entry = + new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref properties, exception, null); + if (logger is ILogEntryPipelineFactory) { - Log(logger, arg1, arg2, exception); + pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetPipeline(metadata, logger); + pipeline = pipelineSnapshot; } - }; + if (pipelineSnapshot != null) + { + if (!pipelineSnapshot.IsEnabled || + (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) + return; + pipelineSnapshot.HandleLogEntry(ref entry); + } + else + { + if (needFullEnabledCheck && logger.IsEnabled(logLevel)) + return; + logger.Log(entry.LogLevel, entry.EventId, entry.State, entry.Exception, LogValues.Callback); + } + } } /// @@ -453,11 +638,10 @@ private static LogValuesFormatter CreateLogValuesFormatter(string formatString, { var logValuesFormatter = new LogValuesFormatter(formatString); - int actualCount = logValuesFormatter.ValueNames.Count; + int actualCount = logValuesFormatter.PropertyCount; if (actualCount != expectedNamedParameterCount) { - throw new ArgumentException( - SR.Format(SR.UnexpectedNumberOfNamedParameters, formatString, expectedNamedParameterCount, actualCount)); + throw new ArgumentException("placeholder"); } return logValuesFormatter; @@ -521,7 +705,7 @@ public LogValues(LogValuesFormatter formatter, T0 value0) switch (index) { case 0: - return new KeyValuePair(_formatter.ValueNames[0], _value0); + return new KeyValuePair(_formatter.GetValueName(0), _value0); case 1: return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); default: @@ -549,13 +733,107 @@ IEnumerator IEnumerable.GetEnumerator() } } - private readonly struct LogValues : IReadOnlyList> + internal class LogValuesMetadata : LogValuesMetadata, ILogMetadata> + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, Attribute[]?[]? attributes = null) : base(format, level, eventId, attributes) { } + + public void AppendFormattedMessage(in LogValues state, IBufferWriter buffer) + { + BufferWriter writer = new BufferWriter(buffer); + foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in CompositeFormat._segments) + { + int index = segment.ArgIndex; + switch (index) + { + case 0: + AppendFormattedPropertyValue(state._value0, ref writer, segment.Alignment, segment.Format); + break; + case 1: + AppendFormattedPropertyValue(state._value1, ref writer, segment.Alignment, segment.Format); + break; + default: + writer.Write(segment.Literal.AsSpan()); + break; + } + } + writer.Flush(); + } + + public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + { + FormatPropertyAction formatter0 = propertyFormatterFactory.GetPropertyFormatter(0, GetPropertyMetadata(0)); + FormatPropertyAction formatter1 = propertyFormatterFactory.GetPropertyFormatter(1, GetPropertyMetadata(1)); + return FormatPropertyList; + + void FormatPropertyList(ref LogValues tstate, ref BufferWriter writer) + { + formatter0(tstate._value0, ref writer); + formatter1(tstate._value1, ref writer); + } + } + + + public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customPropertyFormatters) => + (state, buffer) => AppendFormattedMessage(state, buffer, customPropertyFormatters); + + private void AppendFormattedMessage(in LogValues state, IBufferWriter buffer, PropertyCustomFormatter[] customFormatters) + { + BufferWriter writer = new BufferWriter(buffer); + foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in CompositeFormat._segments) + { + int index = segment.ArgIndex; + switch (index) + { + case 0: + AppendCustomFormattedProperty(index, state._value0, ref writer, segment.Alignment, segment.Format, customFormatters[index]); + break; + case 1: + AppendCustomFormattedProperty(index, state._value1, ref writer, segment.Alignment, segment.Format, customFormatters[index]); + break; + default: + writer.Write(segment.Literal.AsSpan()); + break; + } + } + writer.Flush(); + } + + private static void AppendCustomFormattedProperty(int index, T value, ref BufferWriter writer, int alignment, string? format, PropertyCustomFormatter? formatter) + { + if (formatter == null) + { + AppendFormattedPropertyValue(value, ref writer, alignment, format); + } + else + { + writer.Flush(); + if (value is string strVal) + { + formatter.AppendFormatted(index, strVal, writer.Writer); + } + else if (value is int intVal) + { + formatter.AppendFormatted(index, intVal, writer.Writer); + } + else + { + formatter.AppendFormatted(index, value, writer.Writer); + } + } + } + + public Func, Exception?, string> GetStringMessageFormatter() => LogValues.Callback; + + + } + + internal readonly struct LogValues : IReadOnlyList> { public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); private readonly LogValuesFormatter _formatter; - private readonly T0 _value0; - private readonly T1 _value1; + internal readonly T0 _value0; + internal readonly T1 _value1; public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1) { @@ -564,6 +842,8 @@ public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1) _value1 = value1; } + public ILogMetadata>? Metadata => _formatter as LogValuesMetadata; + public KeyValuePair this[int index] { get @@ -571,9 +851,9 @@ public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1) switch (index) { case 0: - return new KeyValuePair(_formatter.ValueNames[0], _value0); + return new KeyValuePair(_formatter.GetValueName(0), _value0); case 1: - return new KeyValuePair(_formatter.ValueNames[1], _value1); + return new KeyValuePair(_formatter.GetValueName(1), _value1); case 2: return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); default: @@ -598,6 +878,289 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, Attribute[]?[]? parameterAttributes = null) + { + var metadata = new LogValuesMetadata(formatString, level, eventId, parameterAttributes); + if (metadata.PropertyCount != 2) + { + throw new ArgumentException("placeholder"); + } + return metadata; + } + } + + internal class LogValuesMetadata : + LogValuesMetadata, ILogMetadata> + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, Attribute[]?[]? attributes = null) : base(format, level, eventId, attributes) { } + + public void AppendFormattedMessage(in LogValues state, IBufferWriter buffer) + { + BufferWriter writer = new BufferWriter(buffer); + foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in CompositeFormat._segments) + { + int index = segment.ArgIndex; + switch (index) + { + case 0: + AppendFormattedPropertyValue(state._value0, ref writer, segment.Alignment, segment.Format); + break; + case 1: + AppendFormattedPropertyValue(state._value1, ref writer, segment.Alignment, segment.Format); + break; + default: + writer.Write(segment.Literal.AsSpan()); + break; + } + } + writer.Flush(); + } + + public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customPropertyFormatters) => + (state, buffer) => AppendFormattedMessage(state, buffer, customPropertyFormatters); + + private void AppendFormattedMessage(in LogValues state, IBufferWriter buffer, PropertyCustomFormatter[] customFormatters) + { + BufferWriter writer = new BufferWriter(buffer); + foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in CompositeFormat._segments) + { + int index = segment.ArgIndex; + switch (index) + { + case 0: + AppendCustomFormattedProperty(index, state._value0, ref writer, segment.Alignment, segment.Format, customFormatters[index]); + break; + case 1: + AppendCustomFormattedProperty(index, state._value1, ref writer, segment.Alignment, segment.Format, customFormatters[index]); + break; + default: + writer.Write(segment.Literal.AsSpan()); + break; + } + } + writer.Flush(); + } + + public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + { + FormatPropertyAction formatter0 = propertyFormatterFactory.GetPropertyFormatter(0, GetPropertyMetadata(0)); + FormatPropertyAction formatter1 = propertyFormatterFactory.GetPropertyFormatter(1, GetPropertyMetadata(1)); + FormatPropertyAction formatter2 = propertyFormatterFactory.GetPropertyFormatter(2, GetPropertyMetadata(2)); + FormatPropertyAction formatter3 = propertyFormatterFactory.GetPropertyFormatter(3, GetPropertyMetadata(3)); + FormatPropertyAction formatter4 = propertyFormatterFactory.GetPropertyFormatter(4, GetPropertyMetadata(4)); + FormatPropertyAction formatter5 = propertyFormatterFactory.GetPropertyFormatter(5, GetPropertyMetadata(5)); + FormatPropertyAction formatter6 = propertyFormatterFactory.GetPropertyFormatter(6, GetPropertyMetadata(6)); + FormatPropertyAction formatter7 = propertyFormatterFactory.GetPropertyFormatter(7, GetPropertyMetadata(7)); + FormatPropertyAction formatter8 = propertyFormatterFactory.GetPropertyFormatter(8, GetPropertyMetadata(8)); + FormatPropertyAction formatter9 = propertyFormatterFactory.GetPropertyFormatter(9, GetPropertyMetadata(9)); + FormatPropertyAction formatter10 = propertyFormatterFactory.GetPropertyFormatter(10, GetPropertyMetadata(10)); + FormatPropertyAction formatter11 = propertyFormatterFactory.GetPropertyFormatter(11, GetPropertyMetadata(11)); + FormatPropertyAction formatter12 = propertyFormatterFactory.GetPropertyFormatter(12, GetPropertyMetadata(12)); + FormatPropertyAction formatter13 = propertyFormatterFactory.GetPropertyFormatter(13, GetPropertyMetadata(13)); + FormatPropertyAction formatter14 = propertyFormatterFactory.GetPropertyFormatter(14, GetPropertyMetadata(14)); + FormatPropertyAction formatter15 = propertyFormatterFactory.GetPropertyFormatter(15, GetPropertyMetadata(15)); + FormatPropertyAction formatter16 = propertyFormatterFactory.GetPropertyFormatter(16, GetPropertyMetadata(16)); + FormatPropertyAction formatter17 = propertyFormatterFactory.GetPropertyFormatter(17, GetPropertyMetadata(17)); + FormatPropertyAction formatter18 = propertyFormatterFactory.GetPropertyFormatter(18, GetPropertyMetadata(18)); + FormatPropertyAction formatter19 = propertyFormatterFactory.GetPropertyFormatter(19, GetPropertyMetadata(19)); + return FormatPropertyList; + + void FormatPropertyList(ref LogValues tstate, ref BufferWriter writer) + { + formatter0(tstate._value0, ref writer); + formatter1(tstate._value1, ref writer); + formatter2(tstate._value2, ref writer); + formatter3(tstate._value3, ref writer); + formatter4(tstate._value4, ref writer); + formatter5(tstate._value5, ref writer); + formatter6(tstate._value6, ref writer); + formatter7(tstate._value7, ref writer); + formatter8(tstate._value8, ref writer); + formatter9(tstate._value9, ref writer); + formatter10(tstate._value10, ref writer); + formatter11(tstate._value11, ref writer); + formatter12(tstate._value12, ref writer); + formatter13(tstate._value13, ref writer); + formatter14(tstate._value14, ref writer); + formatter15(tstate._value15, ref writer); + formatter16(tstate._value16, ref writer); + formatter17(tstate._value17, ref writer); + formatter18(tstate._value18, ref writer); + formatter19(tstate._value19, ref writer); + } + } + + private static void AppendCustomFormattedProperty(int index, T value, ref BufferWriter writer, int alignment, string? format, PropertyCustomFormatter? formatter) + { + if (formatter == null) + { + AppendFormattedPropertyValue(value, ref writer, alignment, format); + } + else + { + writer.Flush(); + if (value is string strVal) + { + formatter.AppendFormatted(index, strVal, writer.Writer); + } + else if (value is int intVal) + { + formatter.AppendFormatted(index, intVal, writer.Writer); + } + else + { + formatter.AppendFormatted(index, value, writer.Writer); + } + } + } + + public Func, Exception?, string> GetStringMessageFormatter() => + LogValues.Callback; + } + + internal struct LogValues : IReadOnlyList> + { + public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); + + private readonly LogValuesFormatter _formatter; + internal T0 _value0; + internal T1 _value1; + internal T2 _value2; + internal T3 _value3; + internal T4 _value4; + internal T5 _value5; + internal T6 _value6; + internal T7 _value7; + internal T8 _value8; + internal T9 _value9; + internal T10 _value10; + internal T11 _value11; + internal T12 _value12; + internal T13 _value13; + internal T14 _value14; + internal T15 _value15; + internal T16 _value16; + internal T17 _value17; + internal T18 _value18; + internal T19 _value19; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public LogValues(LogValuesFormatter formatter) +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + _formatter = formatter; + } + + public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8, T9 value9, T10 value10, + T11 value11, T12 value12, T13 value13, T14 value14, T15 value15, T16 value16, T17 value17, T18 value18, T19 value19) + { + _formatter = formatter; + _value0 = value0; + _value1 = value1; + _value2 = value2; + _value3 = value3; + _value4 = value4; + _value5 = value5; + _value6 = value6; + _value7 = value7; + _value8 = value8; + _value9 = value9; + _value10 = value10; + _value11 = value11; + _value12 = value12; + _value13 = value13; + _value14 = value14; + _value15 = value15; + _value16 = value16; + _value17 = value17; + _value18 = value18; + _value19 = value19; + } + + public ILogMetadata>? Metadata => + _formatter as LogValuesMetadata; + + public KeyValuePair this[int index] + { + get + { + switch (index) + { + case 0: + return new KeyValuePair(_formatter.GetValueName(0), _value0); + case 1: + return new KeyValuePair(_formatter.GetValueName(1), _value1); + case 2: + return new KeyValuePair(_formatter.GetValueName(2), _value2); + case 3: + return new KeyValuePair(_formatter.GetValueName(3), _value3); + case 4: + return new KeyValuePair(_formatter.GetValueName(4), _value4); + case 5: + return new KeyValuePair(_formatter.GetValueName(5), _value5); + case 6: + return new KeyValuePair(_formatter.GetValueName(6), _value6); + case 7: + return new KeyValuePair(_formatter.GetValueName(7), _value7); + case 8: + return new KeyValuePair(_formatter.GetValueName(8), _value8); + case 9: + return new KeyValuePair(_formatter.GetValueName(9), _value9); + case 10: + return new KeyValuePair(_formatter.GetValueName(10), _value10); + case 11: + return new KeyValuePair(_formatter.GetValueName(11), _value11); + case 12: + return new KeyValuePair(_formatter.GetValueName(12), _value12); + case 13: + return new KeyValuePair(_formatter.GetValueName(13), _value13); + case 14: + return new KeyValuePair(_formatter.GetValueName(14), _value14); + case 15: + return new KeyValuePair(_formatter.GetValueName(15), _value15); + case 16: + return new KeyValuePair(_formatter.GetValueName(16), _value16); + case 17: + return new KeyValuePair(_formatter.GetValueName(17), _value17); + case 18: + return new KeyValuePair(_formatter.GetValueName(18), _value18); + case 19: + return new KeyValuePair(_formatter.GetValueName(19), _value19); + case 20: + return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); + default: + throw new IndexOutOfRangeException(nameof(index)); + } + } + } + + public int Count => 21; + + public IEnumerator> GetEnumerator() + { + for (int i = 0; i < Count; ++i) + { + yield return this[i]; + } + } + + public override string ToString() => throw new NotImplementedException(); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, Attribute[]?[]? parameterAttributes = null) + { + var metadata = new LogValuesMetadata(formatString, level, eventId, parameterAttributes); + if (metadata.PropertyCount != 20) + { + throw new ArgumentException("placeholder"); + } + return metadata; + } } private readonly struct LogValues : IReadOnlyList> @@ -618,11 +1181,11 @@ IEnumerator IEnumerable.GetEnumerator() switch (index) { case 0: - return new KeyValuePair(_formatter.ValueNames[0], _value0); + return new KeyValuePair(_formatter.GetValueName(0), _value0); case 1: - return new KeyValuePair(_formatter.ValueNames[1], _value1); + return new KeyValuePair(_formatter.GetValueName(1), _value1); case 2: - return new KeyValuePair(_formatter.ValueNames[2], _value2); + return new KeyValuePair(_formatter.GetValueName(2), _value2); case 3: return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); default: @@ -674,13 +1237,13 @@ IEnumerator IEnumerable.GetEnumerator() switch (index) { case 0: - return new KeyValuePair(_formatter.ValueNames[0], _value0); + return new KeyValuePair(_formatter.GetValueName(0), _value0); case 1: - return new KeyValuePair(_formatter.ValueNames[1], _value1); + return new KeyValuePair(_formatter.GetValueName(1), _value1); case 2: - return new KeyValuePair(_formatter.ValueNames[2], _value2); + return new KeyValuePair(_formatter.GetValueName(2), _value2); case 3: - return new KeyValuePair(_formatter.ValueNames[3], _value3); + return new KeyValuePair(_formatter.GetValueName(3), _value3); case 4: return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); default: @@ -736,15 +1299,15 @@ IEnumerator IEnumerable.GetEnumerator() switch (index) { case 0: - return new KeyValuePair(_formatter.ValueNames[0], _value0); + return new KeyValuePair(_formatter.GetValueName(0), _value0); case 1: - return new KeyValuePair(_formatter.ValueNames[1], _value1); + return new KeyValuePair(_formatter.GetValueName(1), _value1); case 2: - return new KeyValuePair(_formatter.ValueNames[2], _value2); + return new KeyValuePair(_formatter.GetValueName(2), _value2); case 3: - return new KeyValuePair(_formatter.ValueNames[3], _value3); + return new KeyValuePair(_formatter.GetValueName(3), _value3); case 4: - return new KeyValuePair(_formatter.ValueNames[4], _value4); + return new KeyValuePair(_formatter.GetValueName(4), _value4); case 5: return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); default: @@ -802,17 +1365,17 @@ IEnumerator IEnumerable.GetEnumerator() switch (index) { case 0: - return new KeyValuePair(_formatter.ValueNames[0], _value0); + return new KeyValuePair(_formatter.GetValueName(0), _value0); case 1: - return new KeyValuePair(_formatter.ValueNames[1], _value1); + return new KeyValuePair(_formatter.GetValueName(1), _value1); case 2: - return new KeyValuePair(_formatter.ValueNames[2], _value2); + return new KeyValuePair(_formatter.GetValueName(2), _value2); case 3: - return new KeyValuePair(_formatter.ValueNames[3], _value3); + return new KeyValuePair(_formatter.GetValueName(3), _value3); case 4: - return new KeyValuePair(_formatter.ValueNames[4], _value4); + return new KeyValuePair(_formatter.GetValueName(4), _value4); case 5: - return new KeyValuePair(_formatter.ValueNames[5], _value5); + return new KeyValuePair(_formatter.GetValueName(5), _value5); case 6: return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); default: diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/Microsoft.Extensions.Logging.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/Microsoft.Extensions.Logging.Abstractions.csproj index afd69709ca517e..15abbdda1651ac 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/Microsoft.Extensions.Logging.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/Microsoft.Extensions.Logging.Abstractions.csproj @@ -34,6 +34,7 @@ Microsoft.Extensions.Logging.Abstractions.NullLogger + diff --git a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs index 5b7fade6eaa438..efe5c61d22476e 100644 --- a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs +++ b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs @@ -31,21 +31,21 @@ public static partial class FilterLoggingBuilderExtensions public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Func levelFilter) { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Func categoryLevelFilter) { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Func filter) { throw null; } - public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string? category, System.Func levelFilter) { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string? category, Microsoft.Extensions.Logging.LogLevel level) { throw null; } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string? category, System.Func levelFilter) { throw null; } public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, System.Func levelFilter) { throw null; } public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, System.Func categoryLevelFilter) { throw null; } public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, System.Func filter) { throw null; } - public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, string? category, System.Func levelFilter) { throw null; } public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, string? category, Microsoft.Extensions.Logging.LogLevel level) { throw null; } + public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, string? category, System.Func levelFilter) { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Func levelFilter) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Func categoryLevelFilter) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } - public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string? category, System.Func levelFilter) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string? category, Microsoft.Extensions.Logging.LogLevel level) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string? category, System.Func levelFilter) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, System.Func levelFilter) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, System.Func categoryLevelFilter) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } - public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, string? category, System.Func levelFilter) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, string? category, Microsoft.Extensions.Logging.LogLevel level) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } + public static Microsoft.Extensions.Logging.LoggerFilterOptions AddFilter(this Microsoft.Extensions.Logging.LoggerFilterOptions builder, string? category, System.Func levelFilter) where T : Microsoft.Extensions.Logging.ILoggerProvider { throw null; } } public partial interface ILoggingBuilder { @@ -58,7 +58,7 @@ public LoggerFactory(System.Collections.Generic.IEnumerable providers, Microsoft.Extensions.Logging.LoggerFilterOptions filterOptions) { } public LoggerFactory(System.Collections.Generic.IEnumerable providers, Microsoft.Extensions.Options.IOptionsMonitor filterOption) { } public LoggerFactory(System.Collections.Generic.IEnumerable providers, Microsoft.Extensions.Options.IOptionsMonitor filterOption, Microsoft.Extensions.Options.IOptions? options) { } - public LoggerFactory(System.Collections.Generic.IEnumerable providers, Microsoft.Extensions.Options.IOptionsMonitor filterOption, Microsoft.Extensions.Options.IOptions? options = null, Microsoft.Extensions.Logging.IExternalScopeProvider? scopeProvider = null) { } + public LoggerFactory(System.Collections.Generic.IEnumerable providers, System.Collections.Generic.IEnumerable processorFactories, Microsoft.Extensions.Options.IOptionsMonitor filterOption, Microsoft.Extensions.Options.IOptions? options = null, Microsoft.Extensions.Logging.IExternalScopeProvider? scopeProvider = null) { } public void AddProvider(Microsoft.Extensions.Logging.ILoggerProvider provider) { } protected virtual bool CheckDisposed() { throw null; } public static Microsoft.Extensions.Logging.ILoggerFactory Create(System.Action configure) { throw null; } @@ -88,11 +88,22 @@ public LoggerFilterRule(string? providerName, string? categoryName, Microsoft.Ex } public static partial class LoggingBuilderExtensions { + public static Microsoft.Extensions.Logging.ILoggingBuilder AddProcessor(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Func getProcessor) where T : Microsoft.Extensions.Logging.ILogEntryProcessor { throw null; } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddProcessor(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Func getProcessor) where T : Microsoft.Extensions.Logging.ILogEntryProcessor { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder AddProvider(this Microsoft.Extensions.Logging.ILoggingBuilder builder, Microsoft.Extensions.Logging.ILoggerProvider provider) { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder ClearProviders(this Microsoft.Extensions.Logging.ILoggingBuilder builder) { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder Configure(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action action) { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder SetMinimumLevel(this Microsoft.Extensions.Logging.ILoggingBuilder builder, Microsoft.Extensions.Logging.LogLevel level) { throw null; } } + public readonly partial struct PipelineKey + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public PipelineKey(object typeOrMetadata, Microsoft.Extensions.Logging.ILogEntryProcessor? terminalProcessor, object? userState) { throw null; } + public Microsoft.Extensions.Logging.ILogEntryProcessor? TerminalProcessor { get { throw null; } } + public object TypeOrMetadata { get { throw null; } } + public object? UserState { get { throw null; } } + } [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=false, Inherited=false)] public partial class ProviderAliasAttribute : System.Attribute { diff --git a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs new file mode 100644 index 00000000000000..26118767d9f4f1 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs @@ -0,0 +1,180 @@ +// 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.Linq; + +namespace Microsoft.Extensions.Logging +{ + internal sealed class DispatchProcessor : ILogEntryProcessor + { + private readonly LoggerInformation[] _loggers; + + public DispatchProcessor(LoggerInformation[] loggers) + { + _loggers = loggers; + } + + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicCheckRequired) + { + if (metadata != null) + { + LoggerInformation[] filteredLoggers = _loggers.Where(l => l.IsEnabled(metadata.LogLevel)).ToArray(); + if (filteredLoggers.Length == 0) + { + enabled = false; + dynamicCheckRequired = false; + return NullHandler.Instance; + } + else if (filteredLoggers.Length == 1) + { + LoggerInformation loggerInfo = filteredLoggers[0]; + LogEntryHandler? handler = null; + if (loggerInfo.Processor != null) + { + handler = loggerInfo.Processor.GetLogEntryHandler(metadata, out enabled, out dynamicCheckRequired); + if (handler != null) + { + return new DispatchViaHandler(handler); + } + } + } + //TODO: add a more general purpose case that dispatches to N handlers and M loggers + } + + enabled = true; + dynamicCheckRequired = true; + return new DynamicDispatchToLoggers(this, metadata?.GetStringMessageFormatter()); + } + + public bool IsEnabled(LogLevel logLevel) + { + // We could potentially pre-process loggers to remove those with + // MinLevel = None at the cost of another array for every category + LoggerInformation[]? loggers = _loggers; + if (loggers == null) + { + return false; + } + + List? exceptions = null; + int i = 0; + for (; i < loggers.Length; i++) + { + ref readonly LoggerInformation loggerInfo = ref loggers[i]; + if (!loggerInfo.IsEnabled(logLevel)) + { + continue; + } + + if (LoggerIsEnabled(logLevel, loggerInfo.Logger, ref exceptions)) + { + break; + } + } + + if (exceptions != null && exceptions.Count > 0) + { + Logger.ThrowLoggingError(exceptions); + } + + return i < loggers.Length ? true : false; + + static bool LoggerIsEnabled(LogLevel logLevel, ILogger logger, ref List? exceptions) + { + try + { + if (logger.IsEnabled(logLevel)) + { + return true; + } + } + catch (Exception ex) + { + exceptions ??= new List(); + exceptions.Add(ex); + } + + return false; + } + } + + private sealed class NullHandler : LogEntryHandler + { + public static NullHandler Instance = new NullHandler(); + public override void HandleLogEntry(ref LogEntry logEntry) + { + } + public override bool IsEnabled(LogLevel level) => false; + } + + private sealed class DispatchViaHandler : LogEntryHandler + { + private LogEntryHandler _nestedHandler; + + public DispatchViaHandler(LogEntryHandler handler) + { + _nestedHandler = handler; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { + try + { + _nestedHandler.HandleLogEntry(ref logEntry); + } + catch (Exception ex) + { + Logger.ThrowLoggingError(new List(new Exception[] { ex })); + } + } + + public override bool IsEnabled(LogLevel level) => _nestedHandler.IsEnabled(level); + } + + private sealed class DynamicDispatchToLoggers : LogEntryHandler + { + private DispatchProcessor _processor; + private Func? _formatter; + + public DynamicDispatchToLoggers(DispatchProcessor processor, Func? formatter) + { + _processor = processor; + _formatter = formatter; + } + public override void HandleLogEntry(ref LogEntry logEntry) + { + Func? formatter = logEntry.Formatter ?? _formatter; + formatter ??= (TState s, Exception? _) => s == null ? "" : s.ToString() ?? ""; + LoggerInformation[] loggers = _processor._loggers!; + List? exceptions = null; + for (int i = 0; i < loggers.Length; i++) + { + ref readonly LoggerInformation loggerInfo = ref loggers[i]; + if (!loggerInfo.IsEnabled(logEntry.LogLevel)) + { + continue; + } + + try + { + loggerInfo.Logger.Log(logEntry.LogLevel, logEntry.EventId, logEntry.State, logEntry.Exception, formatter); + } + catch (Exception ex) + { + exceptions ??= new List(); + exceptions.Add(ex); + } + } + + if (exceptions != null && exceptions.Count > 0) + { + Logger.ThrowLoggingError(exceptions); + } + } + + public override bool IsEnabled(LogLevel level) => _processor.IsEnabled(level); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs index 86524a2f03d3ff..192ea48636c977 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs @@ -3,151 +3,103 @@ using System; using System.Collections.Generic; +using System.Diagnostics; namespace Microsoft.Extensions.Logging { - internal sealed class Logger : ILogger + internal sealed class Logger : ILogger, ILogEntryPipelineFactory { - public Logger(LoggerInformation[] loggers) => Loggers = loggers; + private readonly LoggerFactory _loggerFactory; - public LoggerInformation[] Loggers { get; set; } - public MessageLogger[]? MessageLoggers { get; set; } - public ScopeLogger[]? ScopeLoggers { get; set; } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + public Logger(LoggerFactory loggerFactory) { - MessageLogger[]? loggers = MessageLoggers; - if (loggers == null) - { - return; - } - - List? exceptions = null; - for (int i = 0; i < loggers.Length; i++) - { - ref readonly MessageLogger loggerInfo = ref loggers[i]; - if (!loggerInfo.IsEnabled(logLevel)) - { - continue; - } + _loggerFactory = loggerFactory; + } - LoggerLog(logLevel, eventId, loggerInfo.Logger, exception, formatter, ref exceptions, state); - } + public VersionedLoggerState VersionedState { get; set; } = VersionedLoggerState.Default; - if (exceptions != null && exceptions.Count > 0) - { - ThrowLoggingError(exceptions); - } + public Action ProcessorInvalidated => () => _loggerFactory.OnProcessorInvalidated(this); - static void LoggerLog(LogLevel logLevel, EventId eventId, ILogger logger, Exception? exception, Func formatter, ref List? exceptions, in TState state) - { - try - { - logger.Log(logLevel, eventId, state, exception, formatter); - } - catch (Exception ex) - { - exceptions ??= new List(); - exceptions.Add(ex); - } - } + public LogEntryPipeline? GetPipeline(ILogMetadata? metadata, object? userState) + { + return VersionedState.GetPipeline(metadata, userState); } - public bool IsEnabled(LogLevel logLevel) + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - MessageLogger[]? loggers = MessageLoggers; - if (loggers == null) - { - return false; - } - - List? exceptions = null; - int i = 0; - for (; i < loggers.Length; i++) + ILogMetadata? metadata = null; + if (state is ILoggerStateWithMetadata) { - ref readonly MessageLogger loggerInfo = ref loggers[i]; - if (!loggerInfo.IsEnabled(logLevel)) - { - continue; - } - - if (LoggerIsEnabled(logLevel, loggerInfo.Logger, ref exceptions)) - { - break; - } + metadata = ((ILoggerStateWithMetadata)state).Metadata; } + LogEntryPipeline pipeline = GetPipeline(metadata, this)!; + if (!pipeline.IsEnabled || (pipeline.IsDynamicLevelCheckRequired && !pipeline.IsEnabledDynamic(logLevel))) + return; + EmptyEnrichmentPropertyValues props = default; + LogEntry logEntry = new LogEntry(logLevel, eventId, ref state, ref props, exception, formatter); + pipeline.HandleLogEntry(ref logEntry); + } - if (exceptions != null && exceptions.Count > 0) + public bool IsEnabled(LogLevel level) + { + ILogEntryProcessor processor = VersionedState.Processor; + if (processor != null) { - ThrowLoggingError(exceptions); - } - - return i < loggers.Length ? true : false; - - static bool LoggerIsEnabled(LogLevel logLevel, ILogger logger, ref List? exceptions) - { - try - { - if (logger.IsEnabled(logLevel)) - { - return true; - } - } - catch (Exception ex) - { - exceptions ??= new List(); - exceptions.Add(ex); - } - - return false; + return processor.IsEnabled(level); } + return false; } public IDisposable? BeginScope(TState state) where TState : notnull { - ScopeLogger[]? loggers = ScopeLoggers; - - if (loggers == null) - { - return NullScope.Instance; - } - - if (loggers.Length == 1) - { - return loggers[0].CreateScope(state); - } - - var scope = new Scope(loggers.Length); - List? exceptions = null; - for (int i = 0; i < loggers.Length; i++) - { - ref readonly ScopeLogger scopeLogger = ref loggers[i]; - - try - { - scope.SetDisposable(i, scopeLogger.CreateScope(state)); - } - catch (Exception ex) - { - exceptions ??= new List(); - exceptions.Add(ex); - } - } - - if (exceptions != null && exceptions.Count > 0) - { - ThrowLoggingError(exceptions); - } - - return scope; + throw new NotImplementedException(); + //Processors should also handle scopes + //ScopeLogger[]? loggers = ScopeLoggers; + //ScopeLogger[]? loggers = null; + + //if (loggers == null) + //{ + // return NullScope.Instance; + //} + + //if (loggers.Length == 1) + //{ + // return loggers[0].CreateScope(state); + //} + + //var scope = new Scope(loggers.Length); + //List? exceptions = null; + //for (int i = 0; i < loggers.Length; i++) + //{ + // ref readonly ScopeLogger scopeLogger = ref loggers[i]; + + // try + // { + // scope.SetDisposable(i, scopeLogger.CreateScope(state)); + // } + // catch (Exception ex) + // { + // exceptions ??= new List(); + // exceptions.Add(ex); + // } + //} + + //if (exceptions != null && exceptions.Count > 0) + //{ + // ThrowLoggingError(exceptions); + //} + + //return scope; } - private static void ThrowLoggingError(List exceptions) + internal static void ThrowLoggingError(List exceptions) { throw new AggregateException( message: "An error occurred while writing to logger(s).", innerExceptions: exceptions); } + + private sealed class Scope : IDisposable { private bool _isDisposed; @@ -201,4 +153,77 @@ public void Dispose() } } } + + internal sealed class VersionedLoggerState + { + public static readonly VersionedLoggerState Default = new VersionedLoggerState(); + + public VersionedLoggerState() + { + Loggers = Array.Empty(); + Processor = NullLogProcessor.Instance; + } + + public VersionedLoggerState(LoggerInformation[] loggers, ILogEntryProcessor processor) + { + Loggers = loggers; + Processor = processor; + _isUpToDate = true; + } + + private bool _isUpToDate; + public LoggerInformation[] Loggers { get; } + public ILogEntryProcessor Processor { get; } + public Dictionary Pipelines { get; } = new Dictionary(); + + public LogEntryPipeline? GetPipeline(ILogMetadata? metadata, object? userState) + { + // The default versioned state should never be used to create pipelines, it is a shared singleton + // that exists just to satisfy nullability checks + Debug.Assert(this != Default); + + LogEntryPipeline? pipeline; + PipelineKey key = new PipelineKey((metadata == null) ? typeof(TState) : metadata, terminalProcessor: null, userState: userState); + lock (Pipelines) + { + if (!Pipelines.TryGetValue(key, out pipeline)) + { + LogEntryHandler handler = Processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicCheckRequired); + pipeline = new LogEntryPipeline(handler, userState, enabled, dynamicCheckRequired); + // in a multi-threaded race it is possible to create new pipelines after the versioned state is already disposed + // if this happens the pipeline is immediately marked as being not up-to-date. + pipeline.IsUpToDate = _isUpToDate; + Pipelines[key] = pipeline; + } + } + return (LogEntryPipeline?)pipeline; + } + + public void MarkNotUpToDate() + { + lock (Pipelines) + { + _isUpToDate = false; + foreach (LogEntryPipeline pipeline in Pipelines.Values) + { + pipeline.IsUpToDate = false; + } + } + } + + private sealed class NullLogProcessor : ILogEntryProcessor + { + public static readonly NullLogProcessor Instance = new NullLogProcessor(); + + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return false; + } + } + } } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs index eb2a22b2f1a983..2927986d02c0f4 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs @@ -3,12 +3,16 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Logging { + /// /// Produces instances of classes based on the given providers. /// @@ -22,6 +26,7 @@ public class LoggerFactory : ILoggerFactory private LoggerFilterOptions _filterOptions; private IExternalScopeProvider? _scopeProvider; private readonly LoggerFactoryOptions _factoryOptions; + private IProcessorFactory[] _processorFactories; /// /// Creates a new instance. @@ -62,7 +67,8 @@ public LoggerFactory(IEnumerable providers, IOptionsMonitorThe providers to use in producing instances. /// The filter option to use. /// The . - public LoggerFactory(IEnumerable providers, IOptionsMonitor filterOption, IOptions? options) : this(providers, filterOption, options, null) + public LoggerFactory(IEnumerable providers, IOptionsMonitor filterOption, IOptions? options) : + this(providers, Array.Empty(), filterOption, options, null) { } @@ -70,10 +76,15 @@ public LoggerFactory(IEnumerable providers, IOptionsMonitor instance. /// /// The providers to use in producing instances. + /// The processor factories to use in the logging pipeline. /// The filter option to use. /// The . /// The . - public LoggerFactory(IEnumerable providers, IOptionsMonitor filterOption, IOptions? options = null, IExternalScopeProvider? scopeProvider = null) + public LoggerFactory(IEnumerable providers, + IEnumerable processorFactories, + IOptionsMonitor filterOption, + IOptions? options = null, + IExternalScopeProvider? scopeProvider = null) { _scopeProvider = scopeProvider; @@ -86,7 +97,7 @@ public LoggerFactory(IEnumerable providers, IOptionsMonitor providers, IOptionsMonitor registeredLogger in _loggers) { Logger logger = registeredLogger.Value; - (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers); + UpdateLogger(logger); } } } + internal void OnProcessorInvalidated(Logger logger) + { + lock (_sync) + { + UpdateLogger(logger); + } + } + + private void UpdateLogger(Logger logger) + { + Debug.Assert(Monitor.IsEntered(_sync)); + LoggerInformation[] loggerInfos = logger.VersionedState.Loggers; + for (int i = 0; i < loggerInfos.Length; i++) + { + LoggerInformation previousInfo = loggerInfos[i]; + loggerInfos[i] = CreateLoggerInformation(logger, previousInfo.Provider, previousInfo.Category, previousInfo.Logger, previousInfo.Processor, previousInfo.ProcessorCancelRegistration); + } + UpdateLogger(logger, loggerInfos); + } + + private void UpdateLogger(Logger logger, LoggerInformation[] loggerInfos) + { + Debug.Assert(Monitor.IsEntered(_sync)); + VersionedLoggerState oldState = logger.VersionedState; + // Even though we set new versioned state before disposing the old one keep in mind that + // it is still possible for a concurrent operation to capture the versioned state before + // it was replaced and then use it after it was disposed. + logger.VersionedState = new VersionedLoggerState(loggerInfos, GetProcessor(loggerInfos)); + oldState.MarkNotUpToDate(); + } + + private ILogEntryProcessor GetProcessor(LoggerInformation[] loggerInfos) + { + ILogEntryProcessor processor = new DispatchProcessor(loggerInfos); + for (int i = _processorFactories.Length - 1; i >= 0; i--) + { + processor = _processorFactories[i].GetProcessor(processor); + } + return processor; + } + /// /// Creates an with the given . /// @@ -142,10 +196,9 @@ public ILogger CreateLogger(string categoryName) { if (!_loggers.TryGetValue(categoryName, out Logger? logger)) { - logger = new Logger(CreateLoggers(categoryName)); - - (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers); - + logger = new Logger(this); + LoggerInformation[] loggerInfos = CreateLoggers(logger, categoryName); + UpdateLogger(logger, loggerInfos); _loggers[categoryName] = logger; } @@ -164,7 +217,7 @@ public void AddProvider(ILoggerProvider provider) throw new ObjectDisposedException(nameof(LoggerFactory)); } - ThrowHelper.ThrowIfNull(provider); + if (provider == null) throw new ArgumentNullException(nameof(provider)); lock (_sync) { @@ -173,14 +226,12 @@ public void AddProvider(ILoggerProvider provider) foreach (KeyValuePair existingLogger in _loggers) { Logger logger = existingLogger.Value; - LoggerInformation[] loggerInformation = logger.Loggers; - - int newLoggerIndex = loggerInformation.Length; - Array.Resize(ref loggerInformation, loggerInformation.Length + 1); - loggerInformation[newLoggerIndex] = new LoggerInformation(provider, existingLogger.Key); + LoggerInformation[] loggerInformation = logger.VersionedState.Loggers; - logger.Loggers = loggerInformation; - (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers); + int newLoggerIndex = loggerInformation == null ? 0 : loggerInformation.Length; + Array.Resize(ref loggerInformation, newLoggerIndex + 1); + loggerInformation[newLoggerIndex] = CreateLoggerInformation(logger, provider, existingLogger.Key); + UpdateLogger(logger, loggerInformation); } } } @@ -201,48 +252,55 @@ private void AddProviderRegistration(ILoggerProvider provider, bool dispose) } } - private LoggerInformation[] CreateLoggers(string categoryName) + private LoggerInformation[] CreateLoggers(Logger logger, string categoryName) { var loggers = new LoggerInformation[_providerRegistrations.Count]; for (int i = 0; i < _providerRegistrations.Count; i++) { - loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider, categoryName); + loggers[i] = CreateLoggerInformation(logger, _providerRegistrations[i].Provider, categoryName); } return loggers; } - private (MessageLogger[] MessageLoggers, ScopeLogger[]? ScopeLoggers) ApplyFilters(LoggerInformation[] loggers) + private LoggerInformation CreateLoggerInformation( + Logger logger, + ILoggerProvider provider, + string category, + ILogger? existingLogger = null, + ILogEntryProcessor? existingProcessor = null, + CancellationTokenRegistration? existingProcCancelRegistration = null) { - var messageLoggers = new List(); - List? scopeLoggers = _filterOptions.CaptureScopes ? new List() : null; - - foreach (LoggerInformation loggerInformation in loggers) + LoggerRuleSelector.Select(_filterOptions, + provider.GetType(), + category, + out LogLevel? minLevel, + out Func? filter); + + ILogger loggerSink = existingLogger ?? provider.CreateLogger(category); + minLevel ??= LogLevel.Trace; + + ILogEntryProcessor? processor = existingProcessor; + CancellationTokenRegistration? registration = existingProcCancelRegistration; + // TODO: CancellationTokenRegistration.Token isn't available in .NET Standard 2.0. + //if (registration.HasValue && registration.Value.Token.IsCancellationRequested) + //{ + // processor = null; + // registration.Value.Dispose(); + //} + if (processor == null && loggerSink is ILogEntryProcessorFactory factory) { - LoggerRuleSelector.Select(_filterOptions, - loggerInformation.ProviderType, - loggerInformation.Category, - out LogLevel? minLevel, - out Func? filter); - - if (minLevel is not null and > LogLevel.Critical) + var processorContext = factory.GetProcessor(); + if (processorContext.CancellationToken.IsCancellationRequested) { - continue; + processor = null; } - - messageLoggers.Add(new MessageLogger(loggerInformation.Logger, loggerInformation.Category, loggerInformation.ProviderType.FullName, minLevel, filter)); - - if (!loggerInformation.ExternalScope) + else { - scopeLoggers?.Add(new ScopeLogger(logger: loggerInformation.Logger, externalScopeProvider: null)); + processor = processorContext.Processor; + registration = processorContext.CancellationToken.Register(logger.ProcessorInvalidated); } } - - if (_scopeProvider != null) - { - scopeLoggers?.Add(new ScopeLogger(logger: null, externalScopeProvider: _scopeProvider)); - } - - return (messageLoggers.ToArray(), scopeLoggers?.ToArray()); + return new LoggerInformation(provider, category, loggerSink, processor, registration, minLevel.Value, filter); } /// diff --git a/src/libraries/Microsoft.Extensions.Logging/src/LoggerInformation.cs b/src/libraries/Microsoft.Extensions.Logging/src/LoggerInformation.cs index 1a0a01acdcb3b1..615ea73f2c82da 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/LoggerInformation.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/LoggerInformation.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Threading; namespace Microsoft.Extensions.Logging { @@ -71,20 +72,51 @@ public ScopeLogger(ILogger? logger, IExternalScopeProvider? externalScopeProvide internal readonly struct LoggerInformation { - public LoggerInformation(ILoggerProvider provider, string category) : this() + public LoggerInformation(ILoggerProvider provider, string category, ILogger logger, ILogEntryProcessor? processor, CancellationTokenRegistration? processorCancelRegistration, LogLevel minLevel, Func? filter) : this() { - ProviderType = provider.GetType(); - Logger = provider.CreateLogger(category); + Provider = provider; Category = category; + Logger = logger; + Processor = processor; + ProcessorCancelRegistration = processorCancelRegistration; ExternalScope = provider is ISupportExternalScope; + MinLevel = minLevel; + Filter = filter; + if (filter != null) + { + ProviderTypeFullName = provider.GetType().FullName; + } } - public ILogger Logger { get; } - + public ILoggerProvider Provider { get; } public string Category { get; } - public Type ProviderType { get; } + public ILogger Logger { get; } + + public ILogEntryProcessor? Processor { get; } + public CancellationTokenRegistration? ProcessorCancelRegistration { get; } public bool ExternalScope { get; } + + private LogLevel MinLevel { get; } + + private Func? Filter { get; } + + private string? ProviderTypeFullName { get; } + + public bool IsEnabled(LogLevel level) + { + if (level < MinLevel) + { + return false; + } + + if (Filter != null) + { + return Filter(ProviderTypeFullName, Category, level); + } + + return true; + } } } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs index 59a3f742b8b0e3..42c8820222e747 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs @@ -61,16 +61,16 @@ public static ILoggingBuilder Configure(this ILoggingBuilder builder, Action(this ILoggingBuilder builder, Func getProcessor) where T : ILogEntryProcessor - //{ - // builder.Services.TryAddSingleton(new ProcessorFactory(getProcessor)); - // return builder; - //} + public static ILoggingBuilder AddProcessor(this ILoggingBuilder builder, Func getProcessor) where T : ILogEntryProcessor + { + builder.Services.TryAddSingleton(new ProcessorFactory(getProcessor)); + return builder; + } - //public static ILoggingBuilder AddProcessor(this ILoggingBuilder builder, Func getProcessor) where T : ILogEntryProcessor - //{ - // builder.Services.TryAddSingleton(sp => new ProcessorFactory((next) => getProcessor(sp, next))); - // return builder; - //} + public static ILoggingBuilder AddProcessor(this ILoggingBuilder builder, Func getProcessor) where T : ILogEntryProcessor + { + builder.Services.TryAddSingleton(sp => new ProcessorFactory((next) => getProcessor(sp, next))); + return builder; + } } } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs b/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs new file mode 100644 index 00000000000000..f799a0167a400c --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs @@ -0,0 +1,47 @@ +// 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; + +namespace Microsoft.Extensions.Logging +{ + internal sealed class PipelineManager + { + private readonly Dictionary _pipelines = new Dictionary(); + + public LogEntryPipeline? GetPipeline(ILogEntryProcessor processor, ILogMetadata? metadata, object? userState) + { + object? pipeline; + PipelineKey key = new PipelineKey((metadata == null) ? typeof(TState) : metadata, processor, userState); + lock (_pipelines) + { + if (!_pipelines.TryGetValue(key, out pipeline)) + { + pipeline = BuildPipeline(processor, metadata, userState); + _pipelines[key] = pipeline; + } + } + return (LogEntryPipeline?)pipeline; + } + + private static LogEntryPipeline BuildPipeline(ILogEntryProcessor processor, ILogMetadata? metadata, object? userState) + { + LogEntryHandler handler = processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicCheckRequired); + return new LogEntryPipeline(handler, userState, enabled, dynamicCheckRequired); + } + } + + public readonly struct PipelineKey + { + public PipelineKey(object typeOrMetadata, ILogEntryProcessor? terminalProcessor, object? userState) + { + TypeOrMetadata = typeOrMetadata; + TerminalProcessor = terminalProcessor; + UserState = userState; + } + + public object TypeOrMetadata { get; } + public ILogEntryProcessor? TerminalProcessor { get; } + public object? UserState { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs new file mode 100644 index 00000000000000..5934babc619fe8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs @@ -0,0 +1,103 @@ +// 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.DependencyInjection; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.Extensions.Logging.Test +{ + public class ProcessorTests + { + [Fact] + public void AddConsole_BuilderExtensionAddsSingleSetOfServicesWhenCalledTwice() + { + List logMessages = new List(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => builder.AddProcessor((serviceProvider, processor) => new TestLogEntryProcessor(processor, m => logMessages.Add(m)))); + var loggerFactory = serviceCollection.BuildServiceProvider().GetRequiredService(); + var logger = loggerFactory.CreateLogger("Test"); + + logger.LogInformation("Hello {Name}", "John Doe"); + + Assert.Collection(logMessages, m => Assert.Equal("Hello John Doe", m)); + } + + [Fact] + public void sdfsdfsdf() + { + var sink = new TestSink(); + var provider = new TestLoggerProvider(sink, isEnabled: true); + + List logMessages = new List(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddProvider(provider); + builder.AddProcessor((serviceProvider, processor) => new TestLogEntryProcessor(processor, m => logMessages.Add(m))); + }); + var loggerFactory = serviceCollection.BuildServiceProvider().GetRequiredService(); + var logger = loggerFactory.CreateLogger("Test"); + + var sdf = LoggerMessage.Define( + LogLevel.Information, + new EventId(1, "Test"), + "Hello {Name}. You are {Age} years old."); + + sdf(logger, "John Doe", 10, null); + + Assert.Collection(logMessages, m => Assert.Equal("Hello John Doe. You are 10 years old.", m)); + } + + + private sealed class TestLogEntryProcessor : ILogEntryProcessor + { + private readonly ILogEntryProcessor _nextProcessor; + private readonly Action _handleLogEntryCallback; + + public TestLogEntryProcessor(ILogEntryProcessor nextProcessor, Action handleLogEntryCallback) + { + _nextProcessor = nextProcessor; + _handleLogEntryCallback = handleLogEntryCallback; + } + + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return new TestHandler(nextHandler, _handleLogEntryCallback); + } + + public bool IsEnabled(LogLevel logLevel) => true; + } + + private class TestHandler : LogEntryHandler + { + LogEntryHandler _nextHandler; + private readonly Action _handleLogEntryCallback; + + public TestHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) + { + _nextHandler = nextHandler; + _handleLogEntryCallback = handleLogEntryCallback; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { + var message = logEntry.Formatter(logEntry.State, logEntry.Exception); + _handleLogEntryCallback(message); + + _nextHandler.HandleLogEntry(ref logEntry); + } + + public override bool IsEnabled(LogLevel level) + { + return _nextHandler.IsEnabled(level); + } + } + } +} From e363c401dad42783b25e9a1d52b08b71ac9f0acc Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 10 May 2023 10:09:59 +0800 Subject: [PATCH 03/26] Scope pipeline --- ...crosoft.Extensions.Logging.Abstractions.cs | 37 ++-- .../src/CompositeFormat.cs | 4 +- .../src/LogEntryNew.cs | 7 + .../src/LogEntryPipeline.cs | 43 ++++- .../src/LoggerMessage.cs | 38 ++-- .../src/LoggerT.cs | 16 +- .../ref/Microsoft.Extensions.Logging.cs | 3 +- .../src/DispatchProcessor.cs | 137 +++++++++++++- .../src/Logger.cs | 175 +++++++++--------- .../src/LoggerFactory.cs | 4 +- .../src/PipelineManager.cs | 8 +- .../tests/Common/LoggerFactoryTest.cs | 15 +- .../tests/Common/ProcessorTests.cs | 38 +++- 13 files changed, 384 insertions(+), 141 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index 09ef7bac5bb45f..b98ce1a425b74d 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -46,11 +46,13 @@ public partial interface IExternalScopeProvider } public partial interface ILogEntryPipelineFactory { - Microsoft.Extensions.Logging.LogEntryPipeline? GetPipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState); + Microsoft.Extensions.Logging.LogEntryPipeline? GetLoggingPipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState); + Microsoft.Extensions.Logging.ScopePipeline? GetScopePipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState) where TState : notnull; } public partial interface ILogEntryProcessor { Microsoft.Extensions.Logging.LogEntryHandler GetLogEntryHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); + Microsoft.Extensions.Logging.ScopeHandler GetScopeHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull; bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel); } public partial interface ILogEntryProcessorFactory @@ -116,15 +118,7 @@ protected LogEntryHandler() { } public abstract void HandleLogEntry(ref Microsoft.Extensions.Logging.LogEntry logEntry); public abstract bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level); } - public partial class LogEntryPipeline - { - public LogEntryPipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) { } - public bool IsDynamicLevelCheckRequired { get { throw null; } } - public bool IsEnabled { get { throw null; } } - public bool IsUpToDate { get { throw null; } set { } } - public object? UserState { get { throw null; } } - } - public partial class LogEntryPipeline : Microsoft.Extensions.Logging.LogEntryPipeline + public partial class LogEntryPipeline : Microsoft.Extensions.Logging.Pipeline { public LogEntryPipeline(Microsoft.Extensions.Logging.LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base (default(object), default(bool), default(bool)) { } public void HandleLogEntry(ref Microsoft.Extensions.Logging.LogEntry logEntry) { } @@ -227,7 +221,8 @@ public LoggerMessageAttribute(int eventId, Microsoft.Extensions.Logging.LogLevel public partial class Logger : Microsoft.Extensions.Logging.ILogEntryPipelineFactory, Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.ILogger { public Logger(Microsoft.Extensions.Logging.ILoggerFactory factory) { } - Microsoft.Extensions.Logging.LogEntryPipeline Microsoft.Extensions.Logging.ILogEntryPipelineFactory.GetPipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState) { throw null; } + Microsoft.Extensions.Logging.LogEntryPipeline Microsoft.Extensions.Logging.ILogEntryPipelineFactory.GetLoggingPipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState) { throw null; } + Microsoft.Extensions.Logging.ScopePipeline Microsoft.Extensions.Logging.ILogEntryPipelineFactory.GetScopePipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState) { throw null; } System.IDisposable Microsoft.Extensions.Logging.ILogger.BeginScope(TState state) { throw null; } bool Microsoft.Extensions.Logging.ILogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) { throw null; } void Microsoft.Extensions.Logging.ILogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func formatter) { } @@ -251,6 +246,14 @@ public partial struct LogPropertyMetadata public readonly string? FormatSpecifier { get { throw null; } } public readonly string Name { get { throw null; } } } + public partial class Pipeline + { + public Pipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) { } + public bool IsDynamicLevelCheckRequired { get { throw null; } } + public bool IsEnabled { get { throw null; } } + public bool IsUpToDate { get { throw null; } set { } } + public object? UserState { get { throw null; } } + } public readonly partial struct ProcessorContext { private readonly object _dummy; @@ -272,6 +275,18 @@ public virtual void AppendFormatted(int index, System.ReadOnlySpan value, public virtual void AppendFormatted(int index, string value, System.Buffers.IBufferWriter buffer) { } public abstract void AppendFormatted(int index, T value, System.Buffers.IBufferWriter buffer); } + public abstract partial class ScopeHandler where TState : notnull + { + protected ScopeHandler() { } + public abstract System.IDisposable? HandleBeginScope(ref TState state); + public abstract bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level); + } + public partial class ScopePipeline : Microsoft.Extensions.Logging.Pipeline where TState : notnull + { + public ScopePipeline(Microsoft.Extensions.Logging.ScopeHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base (default(object), default(bool), default(bool)) { } + public System.IDisposable? HandleScope(ref TState scope) { throw null; } + public bool IsEnabledDynamic(Microsoft.Extensions.Logging.LogLevel level) { throw null; } + } } namespace Microsoft.Extensions.Logging.Abstractions { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/CompositeFormat.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/CompositeFormat.cs index 3c450c72d1aba1..0dfd1e8dff5f78 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/CompositeFormat.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/CompositeFormat.cs @@ -78,7 +78,7 @@ public static InternalCompositeFormat Parse(string format) if (!TryParse(format, out InternalCompositeFormat? compositeFormat)) { - throw new FormatException("placeholder"); + throw new FormatException("placeholder InternalCompositeFormat.Parse"); } return compositeFormat; @@ -114,7 +114,7 @@ internal void ValidateNumberOfArgs(int numArgs) { if (numArgs < _argsRequired) { - throw new FormatException("placeholder"); + throw new FormatException("placeholder InternalCompositeFormat.ValidateNumberOfArgs"); } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index 0571ffd02a2719..61ac6312d9479f 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -28,6 +28,7 @@ public ProcessorContext(ILogEntryProcessor processor, CancellationToken cancella public interface ILogEntryProcessor { LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); + ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull; bool IsEnabled(LogLevel logLevel); } @@ -55,6 +56,12 @@ public abstract class LogEntryHandler public abstract void HandleLogEntry(ref LogEntry logEntry); } + public abstract class ScopeHandler where TState : notnull + { + public abstract bool IsEnabled(LogLevel level); + public abstract IDisposable? HandleBeginScope(ref TState state); + } + //TODO: Not sure if we need to keep this? public interface ILoggerStateWithMetadata { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs index 87c0636d2c75f4..abd36d70fa8d94 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs @@ -1,16 +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; + namespace Microsoft.Extensions.Logging { public interface ILogEntryPipelineFactory { - public LogEntryPipeline? GetPipeline(ILogMetadata? metadata, object? userState); + public LogEntryPipeline? GetLoggingPipeline(ILogMetadata? metadata, object? userState); + public ScopePipeline? GetScopePipeline(ILogMetadata? metadata, object? userState) where TState : notnull; } - public class LogEntryPipeline + public class Pipeline { - public LogEntryPipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) + public Pipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) { UserState = userState; IsEnabled = isEnabled; @@ -24,7 +27,23 @@ public LogEntryPipeline(object? userState, bool isEnabled, bool isDynamicLevelCh public bool IsUpToDate { get; set; } } - public class LogEntryPipeline : LogEntryPipeline + //public class ScopePipeline + //{ + // public ScopePipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) + // { + // UserState = userState; + // IsEnabled = isEnabled; + // IsDynamicLevelCheckRequired = isDynamicLevelCheckRequired; + // IsUpToDate = true; + // } + + // public object? UserState { get; } + // public bool IsEnabled { get; } + // public bool IsDynamicLevelCheckRequired { get; } + // public bool IsUpToDate { get; set; } + //} + + public class LogEntryPipeline : Pipeline { public LogEntryPipeline(LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base(userState, isEnabled, isDynamicLevelCheckRequired) @@ -32,12 +51,26 @@ public LogEntryPipeline(LogEntryHandler h _firstHandler = handler; } - private LogEntryHandler _firstHandler; + private readonly LogEntryHandler _firstHandler; public bool IsEnabledDynamic(LogLevel level) => _firstHandler.IsEnabled(level); public void HandleLogEntry(ref LogEntry logEntry) => _firstHandler.HandleLogEntry(ref logEntry); } + public class ScopePipeline : Pipeline where TState : notnull + { + public ScopePipeline(ScopeHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : + base(userState, isEnabled, isDynamicLevelCheckRequired) + { + _firstHandler = handler; + } + + private readonly ScopeHandler _firstHandler; + + public bool IsEnabledDynamic(LogLevel level) => _firstHandler.IsEnabled(level); + public IDisposable? HandleScope(ref TState scope) => _firstHandler.HandleBeginScope(ref scope); + } + public struct EmptyEnrichmentPropertyValues { } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs index 1a3c8e1f8a3fbf..9e6fd4beb01ea1 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs @@ -242,7 +242,7 @@ void LogSlowPath(ILogger logger, ref TState state, Exception? exception) LogEntry entry = new LogEntry(metadata.LogLevel, metadata.EventId, ref state, ref properties, exception, null); if (logger is ILogEntryPipelineFactory) { - pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetPipeline(metadata, logger); + pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetLoggingPipeline(metadata, logger); pipeline = pipelineSnapshot; } if (pipelineSnapshot != null) @@ -312,12 +312,10 @@ void Log(ILogger logger, T1 arg1, T2 arg2, Exception? exception) void LogSlowPath(ILogger logger, T1 arg1, T2 arg2, Exception? exception) { LogEntryPipeline>? pipelineSnapshot = null; - LogValues state = new LogValues(metadata, arg1, arg2); EmptyEnrichmentPropertyValues properties = default; - LogEntry, EmptyEnrichmentPropertyValues> entry = new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref properties, exception, LogValues.Callback); if (logger is ILogEntryPipelineFactory) { - pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetPipeline(metadata, logger); + pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetLoggingPipeline(metadata, logger); pipeline = pipelineSnapshot; } if (pipelineSnapshot != null) @@ -325,13 +323,16 @@ void LogSlowPath(ILogger logger, T1 arg1, T2 arg2, Exception? exception) if (!pipelineSnapshot.IsEnabled || (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) return; + LogValues state = new LogValues(metadata, arg1, arg2); + LogEntry, EmptyEnrichmentPropertyValues> entry = new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref properties, exception, LogValues.Callback); pipelineSnapshot.HandleLogEntry(ref entry); } else { - if (needFullEnabledCheck && logger.IsEnabled(logLevel)) + if (needFullEnabledCheck && !logger.IsEnabled(logLevel)) return; - logger.Log(entry.LogLevel, entry.EventId, entry.State, entry.Exception, LogValues.Callback); + LogValues state = new LogValues(metadata, arg1, arg2); + logger.Log(logLevel, eventId, state, exception, LogValues.Callback); } } } @@ -415,7 +416,7 @@ void LogSlowPath(ILogger logger, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref properties, exception, null); if (logger is ILogEntryPipelineFactory) { - pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetPipeline(metadata, logger); + pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetLoggingPipeline(metadata, logger); pipeline = pipelineSnapshot; } if (pipelineSnapshot != null) @@ -638,13 +639,18 @@ private static LogValuesFormatter CreateLogValuesFormatter(string formatString, { var logValuesFormatter = new LogValuesFormatter(formatString); - int actualCount = logValuesFormatter.PropertyCount; + ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount, logValuesFormatter.PropertyCount); + + return logValuesFormatter; + } + + private static void ValidateFormatStringParameterCount(string formatString, int expectedNamedParameterCount, int actualCount) + { if (actualCount != expectedNamedParameterCount) { - throw new ArgumentException("placeholder"); + throw new ArgumentException( + SR.Format(SR.UnexpectedNumberOfNamedParameters, formatString, expectedNamedParameterCount, actualCount)); } - - return logValuesFormatter; } private readonly struct LogValues : IReadOnlyList> @@ -882,10 +888,7 @@ IEnumerator IEnumerable.GetEnumerator() public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, Attribute[]?[]? parameterAttributes = null) { var metadata = new LogValuesMetadata(formatString, level, eventId, parameterAttributes); - if (metadata.PropertyCount != 2) - { - throw new ArgumentException("placeholder"); - } + ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 2, metadata.PropertyCount); return metadata; } } @@ -1155,10 +1158,7 @@ IEnumerator IEnumerable.GetEnumerator() public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, Attribute[]?[]? parameterAttributes = null) { var metadata = new LogValuesMetadata(formatString, level, eventId, parameterAttributes); - if (metadata.PropertyCount != 20) - { - throw new ArgumentException("placeholder"); - } + ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 20, metadata.PropertyCount); return metadata; } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs index 6f2533d159256e..5f2711dfe6e52a 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs @@ -47,11 +47,23 @@ void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Excep _logger.Log(logLevel, eventId, state, exception, formatter); } - LogEntryPipeline? ILogEntryPipelineFactory.GetPipeline(ILogMetadata? metadata, object? userState) + ScopePipeline? ILogEntryPipelineFactory.GetScopePipeline(ILogMetadata? metadata, object? userState) { if (_logger is ILogEntryPipelineFactory factory) { - return factory.GetPipeline(metadata, userState); + return factory.GetScopePipeline(metadata, userState); + } + else + { + return null; + } + } + + LogEntryPipeline? ILogEntryPipelineFactory.GetLoggingPipeline(ILogMetadata? metadata, object? userState) + { + if (_logger is ILogEntryPipelineFactory factory) + { + return factory.GetLoggingPipeline(metadata, userState); } else { diff --git a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs index efe5c61d22476e..2cb8a32b64c6d6 100644 --- a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs +++ b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs @@ -99,7 +99,8 @@ public readonly partial struct PipelineKey { private readonly object _dummy; private readonly int _dummyPrimitive; - public PipelineKey(object typeOrMetadata, Microsoft.Extensions.Logging.ILogEntryProcessor? terminalProcessor, object? userState) { throw null; } + public PipelineKey(bool isLoggingPipeline, object typeOrMetadata, Microsoft.Extensions.Logging.ILogEntryProcessor? terminalProcessor, object? userState) { throw null; } + public bool IsLoggingPipeline { get { throw null; } } public Microsoft.Extensions.Logging.ILogEntryProcessor? TerminalProcessor { get { throw null; } } public object TypeOrMetadata { get { throw null; } } public object? UserState { get { throw null; } } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs index 26118767d9f4f1..24c73ac407a9cf 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs @@ -4,16 +4,19 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; namespace Microsoft.Extensions.Logging { internal sealed class DispatchProcessor : ILogEntryProcessor { private readonly LoggerInformation[] _loggers; + private readonly IExternalScopeProvider? _externalScopeProvider; - public DispatchProcessor(LoggerInformation[] loggers) + public DispatchProcessor(LoggerInformation[] loggers, IExternalScopeProvider? externalScopeProvider) { _loggers = loggers; + _externalScopeProvider = externalScopeProvider; } public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicCheckRequired) @@ -48,6 +51,13 @@ public LogEntryHandler GetLogEntryHandler(this, metadata?.GetStringMessageFormatter()); } + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicCheckRequired) where TState : notnull + { + enabled = true; + dynamicCheckRequired = false; + return new DynamicDispatchScopeToLoggers(this); + } + public bool IsEnabled(LogLevel logLevel) { // We could potentially pre-process loggers to remove those with @@ -102,13 +112,20 @@ static bool LoggerIsEnabled(LogLevel logLevel, ILogger logger, ref List : LogEntryHandler { - public static NullHandler Instance = new NullHandler(); + public static readonly NullHandler Instance = new NullHandler(); public override void HandleLogEntry(ref LogEntry logEntry) { } public override bool IsEnabled(LogLevel level) => false; } + private sealed class NullScopeHandler : ScopeHandler where TState : notnull + { + public static readonly NullScopeHandler Instance = new NullScopeHandler(); + public override IDisposable? HandleBeginScope(ref TState state) => null; + public override bool IsEnabled(LogLevel level) => false; + } + private sealed class DispatchViaHandler : LogEntryHandler { private LogEntryHandler _nestedHandler; @@ -133,6 +150,31 @@ public override void HandleLogEntry(ref LogEntry public override bool IsEnabled(LogLevel level) => _nestedHandler.IsEnabled(level); } + private sealed class DispatchViaScopeHandler : ScopeHandler where TState : notnull + { + private ScopeHandler _nestedHandler; + + public DispatchViaScopeHandler(ScopeHandler handler) + { + _nestedHandler = handler; + } + + public override IDisposable? HandleBeginScope(ref TState state) + { + try + { + return _nestedHandler.HandleBeginScope(ref state); + } + catch (Exception ex) + { + Logger.ThrowLoggingError(new List(new Exception[] { ex })); + return null; + } + } + + public override bool IsEnabled(LogLevel level) => _nestedHandler.IsEnabled(level); + } + private sealed class DynamicDispatchToLoggers : LogEntryHandler { private DispatchProcessor _processor; @@ -143,6 +185,7 @@ public DynamicDispatchToLoggers(DispatchProcessor processor, Func logEntry) { Func? formatter = logEntry.Formatter ?? _formatter; @@ -176,5 +219,95 @@ public override void HandleLogEntry(ref LogEntry public override bool IsEnabled(LogLevel level) => _processor.IsEnabled(level); } + + private sealed class DynamicDispatchScopeToLoggers : ScopeHandler where TState : notnull + { + private DispatchProcessor _processor; + + public DynamicDispatchScopeToLoggers(DispatchProcessor processor) + { + _processor = processor; + } + + public override IDisposable? HandleBeginScope(ref TState state) + { + LoggerInformation[] loggers = _processor._loggers; + + if (loggers.Length == 1) + { + return CreateScope(loggers[0].Logger, ref state); + } + + var scope = new Scope(loggers.Length); + List? exceptions = null; + for (int i = 0; i < loggers.Length; i++) + { + ref readonly LoggerInformation loggerInfo = ref loggers[i]; + + try + { + scope.SetDisposable(i, CreateScope(loggerInfo.Logger, ref state)); + } + catch (Exception ex) + { + exceptions ??= new List(); + exceptions.Add(ex); + } + } + return scope; + } + + private IDisposable? CreateScope(ILogger logger, ref TState state) + { + if (_processor._externalScopeProvider is { } provider) + { + return provider.Push(state); + } + + return logger.BeginScope(state); + } + + + + //Processors should also handle scopes + //ScopeLogger[]? loggers = ScopeLoggers; + //ScopeLogger[]? loggers = null; + + //if (loggers == null) + //{ + // return NullScope.Instance; + //} + + //if (loggers.Length == 1) + //{ + // return loggers[0].CreateScope(state); + //} + + //var scope = new Scope(loggers.Length); + //List? exceptions = null; + //for (int i = 0; i < loggers.Length; i++) + //{ + // ref readonly ScopeLogger scopeLogger = ref loggers[i]; + + // try + // { + // scope.SetDisposable(i, scopeLogger.CreateScope(state)); + // } + // catch (Exception ex) + // { + // exceptions ??= new List(); + // exceptions.Add(ex); + // } + //} + + //if (exceptions != null && exceptions.Count > 0) + //{ + // ThrowLoggingError(exceptions); + //} + + //return scope; + + public override bool IsEnabled(LogLevel level) => _processor.IsEnabled(level); + } } } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs index 192ea48636c977..73f5cfb711aba8 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.Serialization; namespace Microsoft.Extensions.Logging { @@ -20,9 +21,14 @@ public Logger(LoggerFactory loggerFactory) public Action ProcessorInvalidated => () => _loggerFactory.OnProcessorInvalidated(this); - public LogEntryPipeline? GetPipeline(ILogMetadata? metadata, object? userState) + public LogEntryPipeline? GetLoggingPipeline(ILogMetadata? metadata, object? userState) { - return VersionedState.GetPipeline(metadata, userState); + return VersionedState.GetLoggingPipeline(metadata, userState); + } + + public ScopePipeline? GetScopePipeline(ILogMetadata? metadata, object? userState) where TState : notnull + { + return VersionedState.GetScopePipeline(metadata, userState); } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) @@ -32,9 +38,11 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { metadata = ((ILoggerStateWithMetadata)state).Metadata; } - LogEntryPipeline pipeline = GetPipeline(metadata, this)!; + LogEntryPipeline pipeline = GetLoggingPipeline(metadata, this)!; if (!pipeline.IsEnabled || (pipeline.IsDynamicLevelCheckRequired && !pipeline.IsEnabledDynamic(logLevel))) + { return; + } EmptyEnrichmentPropertyValues props = default; LogEntry logEntry = new LogEntry(logLevel, eventId, ref state, ref props, exception, formatter); pipeline.HandleLogEntry(ref logEntry); @@ -52,44 +60,17 @@ public bool IsEnabled(LogLevel level) public IDisposable? BeginScope(TState state) where TState : notnull { - throw new NotImplementedException(); - //Processors should also handle scopes - //ScopeLogger[]? loggers = ScopeLoggers; - //ScopeLogger[]? loggers = null; - - //if (loggers == null) - //{ - // return NullScope.Instance; - //} - - //if (loggers.Length == 1) - //{ - // return loggers[0].CreateScope(state); - //} - - //var scope = new Scope(loggers.Length); - //List? exceptions = null; - //for (int i = 0; i < loggers.Length; i++) - //{ - // ref readonly ScopeLogger scopeLogger = ref loggers[i]; - - // try - // { - // scope.SetDisposable(i, scopeLogger.CreateScope(state)); - // } - // catch (Exception ex) - // { - // exceptions ??= new List(); - // exceptions.Add(ex); - // } - //} - - //if (exceptions != null && exceptions.Count > 0) - //{ - // ThrowLoggingError(exceptions); - //} - - //return scope; + ILogMetadata? metadata = null; + if (state is ILoggerStateWithMetadata) + { + metadata = ((ILoggerStateWithMetadata)state).Metadata; + } + ScopePipeline pipeline = GetScopePipeline(metadata, this)!; + if (!pipeline.IsEnabled) + { + return null; + } + return pipeline.HandleScope(ref state); } internal static void ThrowLoggingError(List exceptions) @@ -97,59 +78,57 @@ internal static void ThrowLoggingError(List exceptions) throw new AggregateException( message: "An error occurred while writing to logger(s).", innerExceptions: exceptions); } + } + internal sealed class Scope : IDisposable + { + private bool _isDisposed; + private IDisposable? _disposable0; + private IDisposable? _disposable1; + private readonly IDisposable?[]? _disposable; - private sealed class Scope : IDisposable + public Scope(int count) { - private bool _isDisposed; - - private IDisposable? _disposable0; - private IDisposable? _disposable1; - private readonly IDisposable?[]? _disposable; - - public Scope(int count) + if (count > 2) { - if (count > 2) - { - _disposable = new IDisposable[count - 2]; - } + _disposable = new IDisposable[count - 2]; } + } - public void SetDisposable(int index, IDisposable? disposable) + public void SetDisposable(int index, IDisposable? disposable) + { + switch (index) { - switch (index) - { - case 0: - _disposable0 = disposable; - break; - case 1: - _disposable1 = disposable; - break; - default: - _disposable![index - 2] = disposable; - break; - } + case 0: + _disposable0 = disposable; + break; + case 1: + _disposable1 = disposable; + break; + default: + _disposable![index - 2] = disposable; + break; } + } - public void Dispose() + public void Dispose() + { + if (!_isDisposed) { - if (!_isDisposed) - { - _disposable0?.Dispose(); - _disposable1?.Dispose(); + _disposable0?.Dispose(); + _disposable1?.Dispose(); - if (_disposable != null) + if (_disposable != null) + { + int count = _disposable.Length; + for (int index = 0; index != count; ++index) { - int count = _disposable.Length; - for (int index = 0; index != count; ++index) - { - _disposable[index]?.Dispose(); - } + _disposable[index]?.Dispose(); } - - _isDisposed = true; } + + _isDisposed = true; } } } @@ -174,16 +153,16 @@ public VersionedLoggerState(LoggerInformation[] loggers, ILogEntryProcessor proc private bool _isUpToDate; public LoggerInformation[] Loggers { get; } public ILogEntryProcessor Processor { get; } - public Dictionary Pipelines { get; } = new Dictionary(); + public Dictionary Pipelines { get; } = new Dictionary(); - public LogEntryPipeline? GetPipeline(ILogMetadata? metadata, object? userState) + public LogEntryPipeline? GetLoggingPipeline(ILogMetadata? metadata, object? userState) { // The default versioned state should never be used to create pipelines, it is a shared singleton // that exists just to satisfy nullability checks Debug.Assert(this != Default); - LogEntryPipeline? pipeline; - PipelineKey key = new PipelineKey((metadata == null) ? typeof(TState) : metadata, terminalProcessor: null, userState: userState); + Pipeline? pipeline; + PipelineKey key = new PipelineKey(isLoggingPipeline: true, (metadata == null) ? typeof(TState) : metadata, terminalProcessor: null, userState: userState); lock (Pipelines) { if (!Pipelines.TryGetValue(key, out pipeline)) @@ -199,12 +178,35 @@ public VersionedLoggerState(LoggerInformation[] loggers, ILogEntryProcessor proc return (LogEntryPipeline?)pipeline; } + public ScopePipeline? GetScopePipeline(ILogMetadata? metadata, object? userState) where TState : notnull + { + // The default versioned state should never be used to create pipelines, it is a shared singleton + // that exists just to satisfy nullability checks + Debug.Assert(this != Default); + + Pipeline? pipeline; + PipelineKey key = new PipelineKey(isLoggingPipeline: false, (metadata == null) ? typeof(TState) : metadata, terminalProcessor: null, userState: userState); + lock (Pipelines) + { + if (!Pipelines.TryGetValue(key, out pipeline)) + { + ScopeHandler handler = Processor.GetScopeHandler(metadata, out bool enabled, out bool dynamicCheckRequired); + pipeline = new ScopePipeline(handler, userState, enabled, dynamicCheckRequired); + // in a multi-threaded race it is possible to create new pipelines after the versioned state is already disposed + // if this happens the pipeline is immediately marked as being not up-to-date. + pipeline.IsUpToDate = _isUpToDate; + Pipelines[key] = pipeline; + } + } + return (ScopePipeline?)pipeline; + } + public void MarkNotUpToDate() { lock (Pipelines) { _isUpToDate = false; - foreach (LogEntryPipeline pipeline in Pipelines.Values) + foreach (Pipeline pipeline in Pipelines.Values) { pipeline.IsUpToDate = false; } @@ -220,6 +222,11 @@ public LogEntryHandler GetLogEntryHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + { + throw new NotImplementedException(); + } + public bool IsEnabled(LogLevel logLevel) { return false; diff --git a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs index 2927986d02c0f4..e861a54e40672a 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs @@ -97,7 +97,7 @@ public LoggerFactory(IEnumerable providers, if ((_factoryOptions.ActivityTrackingOptions & ActivityTrackingOptionsMask) != 0) { - throw new ArgumentException("placeholder"); + throw new ArgumentException("placeholder LoggerFactory.ctor"); } foreach (ILoggerProvider provider in providers) @@ -172,7 +172,7 @@ private void UpdateLogger(Logger logger, LoggerInformation[] loggerInfos) private ILogEntryProcessor GetProcessor(LoggerInformation[] loggerInfos) { - ILogEntryProcessor processor = new DispatchProcessor(loggerInfos); + ILogEntryProcessor processor = new DispatchProcessor(loggerInfos, _scopeProvider); for (int i = _processorFactories.Length - 1; i >= 0; i--) { processor = _processorFactories[i].GetProcessor(processor); diff --git a/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs b/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs index f799a0167a400c..f3f424511d92d1 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs @@ -9,10 +9,10 @@ internal sealed class PipelineManager { private readonly Dictionary _pipelines = new Dictionary(); - public LogEntryPipeline? GetPipeline(ILogEntryProcessor processor, ILogMetadata? metadata, object? userState) + public LogEntryPipeline? GetLoggingPipeline(ILogEntryProcessor processor, ILogMetadata? metadata, object? userState) { object? pipeline; - PipelineKey key = new PipelineKey((metadata == null) ? typeof(TState) : metadata, processor, userState); + PipelineKey key = new PipelineKey(isLoggingPipeline: true, (metadata == null) ? typeof(TState) : metadata, processor, userState); lock (_pipelines) { if (!_pipelines.TryGetValue(key, out pipeline)) @@ -33,13 +33,15 @@ private static LogEntryPipeline BuildPipeline(ILogEntryProcessor public readonly struct PipelineKey { - public PipelineKey(object typeOrMetadata, ILogEntryProcessor? terminalProcessor, object? userState) + public PipelineKey(bool isLoggingPipeline, object typeOrMetadata, ILogEntryProcessor? terminalProcessor, object? userState) { + IsLoggingPipeline = isLoggingPipeline; TypeOrMetadata = typeOrMetadata; TerminalProcessor = terminalProcessor; UserState = userState; } + public bool IsLoggingPipeline { get; } public object TypeOrMetadata { get; } public ILogEntryProcessor? TerminalProcessor { get; } public object? UserState { get; } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/LoggerFactoryTest.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/LoggerFactoryTest.cs index 7c2f39751b903c..64f652aea0c60b 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/LoggerFactoryTest.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/LoggerFactoryTest.cs @@ -423,14 +423,15 @@ public void CallsSetScopeProvider_OnSupportedProviders() } logger.LogInformation("Message2"); - Assert.Equal(loggerProvider.LogText, + Assert.Equal( new[] { "Message", "Scope", "Scope2", "Message2", - }); + }, + loggerProvider.LogText); Assert.NotNull(loggerProvider.ScopeProvider); Assert.Equal(0, loggerProvider.BeginScopeCalledTimes); } @@ -475,23 +476,25 @@ public void BeginScope_ReturnsCompositeToken_ForMultipleLoggers() } logger.LogInformation("Message2"); - Assert.Equal(loggerProvider.LogText, + Assert.Equal( new[] { "Message", "Scope", "Scope2", "Message2", - }); + }, + loggerProvider.LogText); - Assert.Equal(loggerProvider2.LogText, + Assert.Equal( new[] { "Message", "Scope", "Scope2", "Message2", - }); + }, + loggerProvider.LogText); } // Moq heavily utilizes RefEmit, which does not work on most aot workloads diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs index 5934babc619fe8..408710c1820c00 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs @@ -69,18 +69,24 @@ public TestLogEntryProcessor(ILogEntryProcessor nextProcessor, Action ha public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); - return new TestHandler(nextHandler, _handleLogEntryCallback); + return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); + } + + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + { + var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return new TestScopeHandler(nextHandler, _handleLogEntryCallback); } public bool IsEnabled(LogLevel logLevel) => true; } - private class TestHandler : LogEntryHandler + private sealed class TestLogEntryHandler : LogEntryHandler { - LogEntryHandler _nextHandler; + private readonly LogEntryHandler _nextHandler; private readonly Action _handleLogEntryCallback; - public TestHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) + public TestLogEntryHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) { _nextHandler = nextHandler; _handleLogEntryCallback = handleLogEntryCallback; @@ -99,5 +105,29 @@ public override bool IsEnabled(LogLevel level) return _nextHandler.IsEnabled(level); } } + + private sealed class TestScopeHandler : ScopeHandler + { + private readonly ScopeHandler _nextHandler; + private readonly Action _handleLogEntryCallback; + + public TestScopeHandler(ScopeHandler nextHandler, Action handleLogEntryCallback) + { + _nextHandler = nextHandler; + _handleLogEntryCallback = handleLogEntryCallback; + } + + public override IDisposable? HandleBeginScope(ref TState state) + { + _handleLogEntryCallback(state.ToString()); + + return _nextHandler.HandleBeginScope(ref state); + } + + public override bool IsEnabled(LogLevel level) + { + return _nextHandler.IsEnabled(level); + } + } } } From 03c7c4d3bcb5bbd17d6934e11cdc305535ac4c55 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 17 May 2023 17:52:07 +0800 Subject: [PATCH 04/26] Fix tests --- .../src/DispatchProcessor.cs | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs index 24c73ac407a9cf..eee98c4604046d 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs @@ -222,31 +222,51 @@ public override void HandleLogEntry(ref LogEntry private sealed class DynamicDispatchScopeToLoggers : ScopeHandler where TState : notnull { - private DispatchProcessor _processor; + private readonly ScopeLogger[] _scopeLoggers; + private readonly DispatchProcessor _processor; public DynamicDispatchScopeToLoggers(DispatchProcessor processor) { + List scopeLoggers = new List(); + + foreach (LoggerInformation loggerInformation in processor._loggers) + { + if (!loggerInformation.ExternalScope && loggerInformation.IsEnabled(LogLevel.Critical)) + { + scopeLoggers.Add(new ScopeLogger(logger: loggerInformation.Logger, externalScopeProvider: null)); + } + } + if (processor._externalScopeProvider is { } scopeProvider) + { + scopeLoggers.Add(new ScopeLogger(logger: null, externalScopeProvider: scopeProvider)); + } + + _scopeLoggers = scopeLoggers.ToArray(); _processor = processor; } public override IDisposable? HandleBeginScope(ref TState state) { - LoggerInformation[] loggers = _processor._loggers; + ScopeLogger[] loggers = _scopeLoggers; + if (loggers.Length == 0) + { + return null; + } if (loggers.Length == 1) { - return CreateScope(loggers[0].Logger, ref state); + return loggers[0].CreateScope(state); } var scope = new Scope(loggers.Length); List? exceptions = null; for (int i = 0; i < loggers.Length; i++) { - ref readonly LoggerInformation loggerInfo = ref loggers[i]; + ref readonly ScopeLogger loggerInfo = ref loggers[i]; try { - scope.SetDisposable(i, CreateScope(loggerInfo.Logger, ref state)); + scope.SetDisposable(i, loggerInfo.CreateScope(state)); } catch (Exception ex) { @@ -254,17 +274,19 @@ public DynamicDispatchScopeToLoggers(DispatchProcessor processor) exceptions.Add(ex); } } - return scope; - } - private IDisposable? CreateScope(ILogger logger, ref TState state) - { - if (_processor._externalScopeProvider is { } provider) + if (exceptions != null && exceptions.Count > 0) { - return provider.Push(state); + ThrowLoggingError(exceptions); } - return logger.BeginScope(state); + return scope; + } + + private static void ThrowLoggingError(List exceptions) + { + throw new AggregateException( + message: "An error occurred while writing to logger(s).", innerExceptions: exceptions); } From 6f6b24ff1c83e978b21ec0c2862a5cc8cebca3a4 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 17 May 2023 21:03:05 +0800 Subject: [PATCH 05/26] Update tests --- .../Microsoft.Extensions.Logging/src/Logger.cs | 14 +++++++++++--- .../src/LoggerFactory.cs | 2 +- .../tests/Common/ProcessorTests.cs | 8 ++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs index 73f5cfb711aba8..6afe9871eb30f9 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs @@ -65,8 +65,8 @@ public bool IsEnabled(LogLevel level) { metadata = ((ILoggerStateWithMetadata)state).Metadata; } - ScopePipeline pipeline = GetScopePipeline(metadata, this)!; - if (!pipeline.IsEnabled) + ScopePipeline? pipeline = GetScopePipeline(metadata, this); + if (pipeline is null || !pipeline.IsEnabled) { return null; } @@ -141,18 +141,21 @@ public VersionedLoggerState() { Loggers = Array.Empty(); Processor = NullLogProcessor.Instance; + FilterOptions = new LoggerFilterOptions(); } - public VersionedLoggerState(LoggerInformation[] loggers, ILogEntryProcessor processor) + public VersionedLoggerState(LoggerInformation[] loggers, ILogEntryProcessor processor, LoggerFilterOptions filterOptions) { Loggers = loggers; Processor = processor; + FilterOptions = filterOptions; _isUpToDate = true; } private bool _isUpToDate; public LoggerInformation[] Loggers { get; } public ILogEntryProcessor Processor { get; } + public LoggerFilterOptions FilterOptions { get; } public Dictionary Pipelines { get; } = new Dictionary(); public LogEntryPipeline? GetLoggingPipeline(ILogMetadata? metadata, object? userState) @@ -184,6 +187,11 @@ public VersionedLoggerState(LoggerInformation[] loggers, ILogEntryProcessor proc // that exists just to satisfy nullability checks Debug.Assert(this != Default); + if (!FilterOptions.CaptureScopes) + { + return null; + } + Pipeline? pipeline; PipelineKey key = new PipelineKey(isLoggingPipeline: false, (metadata == null) ? typeof(TState) : metadata, terminalProcessor: null, userState: userState); lock (Pipelines) diff --git a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs index e861a54e40672a..1c656305cae638 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs @@ -166,7 +166,7 @@ private void UpdateLogger(Logger logger, LoggerInformation[] loggerInfos) // Even though we set new versioned state before disposing the old one keep in mind that // it is still possible for a concurrent operation to capture the versioned state before // it was replaced and then use it after it was disposed. - logger.VersionedState = new VersionedLoggerState(loggerInfos, GetProcessor(loggerInfos)); + logger.VersionedState = new VersionedLoggerState(loggerInfos, GetProcessor(loggerInfos), _filterOptions); oldState.MarkNotUpToDate(); } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs index 408710c1820c00..397fe115ac9940 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Logging.Test public class ProcessorTests { [Fact] - public void AddConsole_BuilderExtensionAddsSingleSetOfServicesWhenCalledTwice() + public void LogInformation_InvokesProcessor() { List logMessages = new List(); @@ -27,7 +27,7 @@ public void AddConsole_BuilderExtensionAddsSingleSetOfServicesWhenCalledTwice() } [Fact] - public void sdfsdfsdf() + public void DefinedLog_InvokesProcessor() { var sink = new TestSink(); var provider = new TestLoggerProvider(sink, isEnabled: true); @@ -44,12 +44,12 @@ public void sdfsdfsdf() var loggerFactory = serviceCollection.BuildServiceProvider().GetRequiredService(); var logger = loggerFactory.CreateLogger("Test"); - var sdf = LoggerMessage.Define( + var definedLog = LoggerMessage.Define( LogLevel.Information, new EventId(1, "Test"), "Hello {Name}. You are {Age} years old."); - sdf(logger, "John Doe", 10, null); + definedLog(logger, "John Doe", 10, null); Assert.Collection(logMessages, m => Assert.Equal("Hello John Doe. You are 10 years old.", m)); } From 5f89fed11c07bbb1eaa1cc3637823e46aaad30e5 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 18 May 2023 12:26:44 +0800 Subject: [PATCH 06/26] Update --- .../Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs index 397fe115ac9940..d470d0ab547438 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs @@ -78,7 +78,7 @@ public ScopeHandler GetScopeHandler(ILogMetadata? metada return new TestScopeHandler(nextHandler, _handleLogEntryCallback); } - public bool IsEnabled(LogLevel logLevel) => true; + public bool IsEnabled(LogLevel logLevel) => _nextProcessor.IsEnabled(logLevel); } private sealed class TestLogEntryHandler : LogEntryHandler From ea549cbf9fd65f8810c0047e47f0612e1d9628ed Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 22 May 2023 09:58:42 +0800 Subject: [PATCH 07/26] Fix tests --- .../tests/Common/ProcessorTests.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs index d470d0ab547438..64675b761ba41b 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Testing; using Xunit; @@ -14,21 +15,40 @@ public class ProcessorTests [Fact] public void LogInformation_InvokesProcessor() { + // Arrange + var sink = new TestSink(); + var provider = new TestLoggerProvider(sink, isEnabled: true); + List logMessages = new List(); var serviceCollection = new ServiceCollection(); - serviceCollection.AddLogging(builder => builder.AddProcessor((serviceProvider, processor) => new TestLogEntryProcessor(processor, m => logMessages.Add(m)))); + serviceCollection.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddProvider(provider); + builder.AddProcessor((serviceProvider, processor) => new TestLogEntryProcessor(processor, m => logMessages.Add(m))); + }); var loggerFactory = serviceCollection.BuildServiceProvider().GetRequiredService(); var logger = loggerFactory.CreateLogger("Test"); + // Act logger.LogInformation("Hello {Name}", "John Doe"); + // Assert Assert.Collection(logMessages, m => Assert.Equal("Hello John Doe", m)); + + Assert.Equal(1, sink.Writes.Count()); + Assert.True(sink.Writes.TryTake(out var write)); + Assert.Equal(LogLevel.Information, write.LogLevel); + Assert.Equal("Hello John Doe", write.State.ToString()); + Assert.Equal(0, write.EventId); + Assert.Null(write.Exception); } [Fact] public void DefinedLog_InvokesProcessor() { + // Arrange var sink = new TestSink(); var provider = new TestLoggerProvider(sink, isEnabled: true); @@ -49,9 +69,18 @@ public void DefinedLog_InvokesProcessor() new EventId(1, "Test"), "Hello {Name}. You are {Age} years old."); + // Act definedLog(logger, "John Doe", 10, null); + // Assert Assert.Collection(logMessages, m => Assert.Equal("Hello John Doe. You are 10 years old.", m)); + + Assert.Equal(1, sink.Writes.Count()); + Assert.True(sink.Writes.TryTake(out var write)); + Assert.Equal(LogLevel.Information, write.LogLevel); + Assert.Equal("Hello John Doe. You are 10 years old.", write.State.ToString()); + Assert.Equal(1, write.EventId); + Assert.Null(write.Exception); } From f10e6928cdf6c416a00dd56362cd041b19284ce0 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 22 May 2023 10:21:55 +0800 Subject: [PATCH 08/26] Clean up --- .../Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs index 64675b761ba41b..a9c5eae47760a9 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs @@ -83,7 +83,6 @@ public void DefinedLog_InvokesProcessor() Assert.Null(write.Exception); } - private sealed class TestLogEntryProcessor : ILogEntryProcessor { private readonly ILogEntryProcessor _nextProcessor; From b70e514846879625c8e659687243df8f4bce92ed Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 22 May 2023 11:09:50 +0800 Subject: [PATCH 09/26] WIP enrichment --- .../tests/Common/EnrichmentProcessor.cs | 265 ++++++++++++++++++ .../tests/Common/EnrichmentTests.cs | 163 +++++++++++ 2 files changed, 428 insertions(+) create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs new file mode 100644 index 00000000000000..68657a271f8872 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs @@ -0,0 +1,265 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ConsoleApp31.Prototype +{ + public static class EnrichmentExtensions + { + public static ILoggingBuilder Enrich(this ILoggingBuilder builder, string propertyName, Func valueFunc) + { + builder.AddProcessor((sp, next) => + { + IEnumerable props = sp.GetServices(); + EnrichmentPropertiesCollection collection = new EnrichmentPropertiesCollection(); + foreach (var prop in props) + { + collection = prop.AppendTo(collection); + } + return new EnrichmentProcessor(collection, next); + }); + builder.Services.AddSingleton(new EnrichmentProperty(propertyName, valueFunc)); + return builder; + } + + internal abstract class EnrichmentProperty + { + internal abstract EnrichmentPropertiesCollection AppendTo(EnrichmentPropertiesCollection collection); + } + + internal class EnrichmentProperty : EnrichmentProperty + { + public EnrichmentProperty(string name, Func getValue) + { + Name = name; + GetValue = getValue; + } + + public string Name { get; } + public Func GetValue { get; } + + internal override EnrichmentPropertiesCollection AppendTo(EnrichmentPropertiesCollection collection) + { + return collection.AddProperty(Name, GetValue); + } + } + } + + + + internal class EnrichmentProcessor : ILogEntryProcessor + { + ILogEntryProcessor _nextProcessor; + EnrichmentPropertiesCollection _propCollection; + + public EnrichmentProcessor(EnrichmentPropertiesCollection collection, ILogEntryProcessor nextProcessor) + { + _propCollection = collection; + _nextProcessor = nextProcessor; + } + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + return _propCollection.GetLogEntryHandler(_nextProcessor, metadata, out enabled, out dynamicEnabledCheckRequired); + } + + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + { + return _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + } + + public bool IsEnabled(LogLevel logLevel) + { + return _nextProcessor.IsEnabled(logLevel); + } + } + + class EnrichmentPropertiesCollection + { + internal virtual EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) + { + var collection = new EnrichmentPropertiesCollection(); + collection.Name0 = propertyName; + collection.GetValue0 = getValue; + return collection; + } + + internal virtual LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + return nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + } + } + + internal class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection + { + internal string Name0; + internal Func GetValue0; + + internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) + { + var collection = new EnrichmentPropertiesCollection(); + collection.Name0 = Name0; + collection.GetValue0 = GetValue0; + collection.Name1 = propertyName; + collection.GetValue1 = getValue; + return collection; + } + + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); + } + + class EnrichmentHandler : LogEntryHandler + { + LogEntryHandler> _nextHandler; + EnrichmentPropertiesCollection _propertyCollection; + public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) + { + _nextHandler = nextHandler; + _propertyCollection = propertyCollection; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { + EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); + enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; + enrichmentProperties.Value0 = _propertyCollection.GetValue0(); + var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + _nextHandler.HandleLogEntry(ref newLogEntry); + } + + public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); + } + } + + internal class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection + { + internal string Name0; + internal Func GetValue0; + internal string Name1; + internal Func GetValue1; + + internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) + { + var collection = new UnboundedEnrichmentPropertiesCollection(); + collection.Name0 = Name0; + collection.GetValue0 = GetValue0; + collection.Name1 = Name1; + collection.GetValue1 = GetValue1; + collection.OverflowProperties.Add((propertyName, () => getValue())); + return collection; + } + + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); + } + + class EnrichmentHandler : LogEntryHandler + { + LogEntryHandler> _nextHandler; + EnrichmentPropertiesCollection _propertyCollection; + public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) + { + _nextHandler = nextHandler; + _propertyCollection = propertyCollection; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { + EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); + enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; + enrichmentProperties.Value0 = _propertyCollection.GetValue0(); + enrichmentProperties.Value1 = _propertyCollection.GetValue1(); + var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + _nextHandler.HandleLogEntry(ref newLogEntry); + } + + public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); + } + } + + internal class UnboundedEnrichmentPropertiesCollection : EnrichmentPropertiesCollection + { + internal string Name0; + internal Func GetValue0; + internal string Name1; + internal Func GetValue1; + internal List<(string, Func)> OverflowProperties = new List<(string, Func)>(); + + internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) + { + OverflowProperties.Add((propertyName, () => getValue())); + return this; + } + + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); + } + + class EnrichmentHandler : LogEntryHandler + { + LogEntryHandler> _nextHandler; + UnboundedEnrichmentPropertiesCollection _propertyCollection; + public EnrichmentHandler(LogEntryHandler> nextHandler, UnboundedEnrichmentPropertiesCollection propertyCollection) + { + _nextHandler = nextHandler; + _propertyCollection = propertyCollection; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { + UnboundedEnrichmentPropertyValues enrichmentProperties = new UnboundedEnrichmentPropertyValues(); + enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; + enrichmentProperties.Value0 = _propertyCollection.GetValue0(); + enrichmentProperties.Value1 = _propertyCollection.GetValue1(); + int overflowProps = _propertyCollection.OverflowProperties.Count; + enrichmentProperties.ExtraValues = ArrayPool.Shared.Rent(overflowProps); + for(int i = 0; i < overflowProps; i++) + { + enrichmentProperties.ExtraValues[i] = _propertyCollection.OverflowProperties[i].Item2(); + } + var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + _nextHandler.HandleLogEntry(ref newLogEntry); + } + + public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); + } + } + + internal struct EmptyEnrichmentPropertyValues + { + } + + internal struct EnrichmentPropertyValues + { + internal TEnrichmentProperties NestedProperties; + internal T0 Value0; + } + + internal struct EnrichmentPropertyValues + { + internal TEnrichmentProperties NestedProperties; + internal T0 Value0; + internal T1 Value1; + } + + internal struct UnboundedEnrichmentPropertyValues + { + internal TEnrichmentProperties NestedProperties; + internal T0 Value0; + internal T1 Value1; + internal object?[] ExtraValues; + } + + +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs new file mode 100644 index 00000000000000..0917b0e8a230ad --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs @@ -0,0 +1,163 @@ +// 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.Linq; +using ConsoleApp31.Prototype; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.Extensions.Logging.Test +{ + public class EnrichmentTests + { + [Fact] + public void LogInformation_InvokesProcessor() + { + // Arrange + var sink = new TestSink(); + var provider = new TestLoggerProvider(sink, isEnabled: true); + + List logMessages = new List(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddProvider(provider); + builder.Enrich("prop1", () => "Value!"); + builder.AddProcessor((serviceProvider, processor) => new TestLogEntryProcessor(processor, m => logMessages.Add(m))); + }); + var loggerFactory = serviceCollection.BuildServiceProvider().GetRequiredService(); + var logger = loggerFactory.CreateLogger("Test"); + + // Act + logger.LogInformation("Hello {Name}", "John Doe"); + + // Assert + Assert.Collection(logMessages, m => Assert.Equal("Hello John Doe", m)); + + Assert.Equal(1, sink.Writes.Count()); + Assert.True(sink.Writes.TryTake(out var write)); + Assert.Equal(LogLevel.Information, write.LogLevel); + Assert.Equal("Hello John Doe", write.State.ToString()); + Assert.Equal(0, write.EventId); + Assert.Null(write.Exception); + } + + [Fact] + public void DefinedLog_InvokesProcessor() + { + // Arrange + var sink = new TestSink(); + var provider = new TestLoggerProvider(sink, isEnabled: true); + + List logMessages = new List(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddProvider(provider); + builder.AddProcessor((serviceProvider, processor) => new TestLogEntryProcessor(processor, m => logMessages.Add(m))); + }); + var loggerFactory = serviceCollection.BuildServiceProvider().GetRequiredService(); + var logger = loggerFactory.CreateLogger("Test"); + + var definedLog = LoggerMessage.Define( + LogLevel.Information, + new EventId(1, "Test"), + "Hello {Name}. You are {Age} years old."); + + // Act + definedLog(logger, "John Doe", 10, null); + + // Assert + Assert.Collection(logMessages, m => Assert.Equal("Hello John Doe. You are 10 years old.", m)); + + Assert.Equal(1, sink.Writes.Count()); + Assert.True(sink.Writes.TryTake(out var write)); + Assert.Equal(LogLevel.Information, write.LogLevel); + Assert.Equal("Hello John Doe. You are 10 years old.", write.State.ToString()); + Assert.Equal(1, write.EventId); + Assert.Null(write.Exception); + } + + private sealed class TestLogEntryProcessor : ILogEntryProcessor + { + private readonly ILogEntryProcessor _nextProcessor; + private readonly Action _handleLogEntryCallback; + + public TestLogEntryProcessor(ILogEntryProcessor nextProcessor, Action handleLogEntryCallback) + { + _nextProcessor = nextProcessor; + _handleLogEntryCallback = handleLogEntryCallback; + } + + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); + } + + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + { + var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return new TestScopeHandler(nextHandler, _handleLogEntryCallback); + } + + public bool IsEnabled(LogLevel logLevel) => _nextProcessor.IsEnabled(logLevel); + } + + private sealed class TestLogEntryHandler : LogEntryHandler + { + private readonly LogEntryHandler _nextHandler; + private readonly Action _handleLogEntryCallback; + + public TestLogEntryHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) + { + _nextHandler = nextHandler; + _handleLogEntryCallback = handleLogEntryCallback; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { + var message = logEntry.Formatter(logEntry.State, logEntry.Exception); + _handleLogEntryCallback(message); + + _nextHandler.HandleLogEntry(ref logEntry); + } + + public override bool IsEnabled(LogLevel level) + { + return _nextHandler.IsEnabled(level); + } + } + + private sealed class TestScopeHandler : ScopeHandler + { + private readonly ScopeHandler _nextHandler; + private readonly Action _handleLogEntryCallback; + + public TestScopeHandler(ScopeHandler nextHandler, Action handleLogEntryCallback) + { + _nextHandler = nextHandler; + _handleLogEntryCallback = handleLogEntryCallback; + } + + public override IDisposable? HandleBeginScope(ref TState state) + { + _handleLogEntryCallback(state.ToString()); + + return _nextHandler.HandleBeginScope(ref state); + } + + public override bool IsEnabled(LogLevel level) + { + return _nextHandler.IsEnabled(level); + } + } + } +} From aab22f74990ac3e40cf3f0e8d60229f4fad3ca83 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 22 May 2023 17:02:29 +0800 Subject: [PATCH 10/26] Remove extra LogEntry and make changes for enrichment --- ...crosoft.Extensions.Logging.Abstractions.cs | 22 +- .../src/LogEntryNew.cs | 83 ++-- .../src/LogEntryPipeline.cs | 7 +- .../src/LoggerMessage.cs | 22 +- .../src/DispatchProcessor.cs | 32 +- .../src/Logger.cs | 9 +- .../src/PipelineManager.cs | 2 +- .../tests/Common/EnrichmentProcessor.cs | 399 +++++++++++++++--- .../tests/Common/EnrichmentTests.cs | 35 +- .../tests/Common/ProcessorTests.cs | 15 +- 10 files changed, 453 insertions(+), 173 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index b98ce1a425b74d..296f1e886fc59d 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -51,7 +51,7 @@ public partial interface ILogEntryPipelineFactory } public partial interface ILogEntryProcessor { - Microsoft.Extensions.Logging.LogEntryHandler GetLogEntryHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); + Microsoft.Extensions.Logging.LogEntryHandler GetLogEntryHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); Microsoft.Extensions.Logging.ScopeHandler GetScopeHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull; bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel); } @@ -112,30 +112,18 @@ public LogDefineOptions() { } public System.Attribute[]?[]? ParameterAttributes { get { throw null; } set { } } public bool SkipEnabledCheck { get { throw null; } set { } } } - public abstract partial class LogEntryHandler + public abstract partial class LogEntryHandler { protected LogEntryHandler() { } - public abstract void HandleLogEntry(ref Microsoft.Extensions.Logging.LogEntry logEntry); + public abstract void HandleLogEntry(ref Microsoft.Extensions.Logging.Abstractions.LogEntry logEntry); public abstract bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level); } public partial class LogEntryPipeline : Microsoft.Extensions.Logging.Pipeline { - public LogEntryPipeline(Microsoft.Extensions.Logging.LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base (default(object), default(bool), default(bool)) { } - public void HandleLogEntry(ref Microsoft.Extensions.Logging.LogEntry logEntry) { } + public LogEntryPipeline(Microsoft.Extensions.Logging.LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base (default(object), default(bool), default(bool)) { } + public void HandleLogEntry(ref Microsoft.Extensions.Logging.Abstractions.LogEntry logEntry) { } public bool IsEnabledDynamic(Microsoft.Extensions.Logging.LogLevel level) { throw null; } } - public readonly ref partial struct LogEntry - { - private readonly object _dummy; - private readonly int _dummyPrimitive; - public LogEntry(Microsoft.Extensions.Logging.LogLevel level, Microsoft.Extensions.Logging.EventId eventId, ref TState state, ref TEnrichmentProperties enrichmentProperties, System.Exception? exception, System.Func? formatter) { throw null; } - public ref TEnrichmentProperties EnrichmentProperties { get { throw null; } } - public Microsoft.Extensions.Logging.EventId EventId { get { throw null; } } - public System.Exception? Exception { get { throw null; } } - public System.Func? Formatter { get { throw null; } } - public Microsoft.Extensions.Logging.LogLevel LogLevel { get { throw null; } } - public ref TState State { get { throw null; } } - } public static partial class LoggerExtensions { public static System.IDisposable? BeginScope(this Microsoft.Extensions.Logging.ILogger logger, string messageFormat, params object?[] args) { throw null; } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index 61ac6312d9479f..451273b1a032c7 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Threading; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Logging { @@ -27,7 +28,7 @@ public ProcessorContext(ILogEntryProcessor processor, CancellationToken cancella public interface ILogEntryProcessor { - LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); + LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull; bool IsEnabled(LogLevel logLevel); } @@ -50,10 +51,10 @@ public ProcessorFactory(Func getProcessor) public ILogEntryProcessor GetProcessor(ILogEntryProcessor nextProcessor) => _getProcessor(nextProcessor); } - public abstract class LogEntryHandler + public abstract class LogEntryHandler { public abstract bool IsEnabled(LogLevel level); - public abstract void HandleLogEntry(ref LogEntry logEntry); + public abstract void HandleLogEntry(ref LogEntry logEntry); } public abstract class ScopeHandler where TState : notnull @@ -113,42 +114,42 @@ public abstract class PropertyCustomFormatter public abstract void AppendFormatted(int index, T value, IBufferWriter buffer); } - public readonly ref struct LogEntry - { -#if NET8_0_OR_GREATER - private readonly ref TState _state; - private readonly ref TEnrichmentProperties _enrichmentProperties; -#else - // TODO: Explore making intrinsic. Or convert to regular fields. - private readonly ByReference _state; - private readonly ByReference _enrichmentProperties; -#endif - - public LogEntry(LogLevel level, EventId eventId, ref TState state, ref TEnrichmentProperties enrichmentProperties, Exception? exception, Func? formatter) - { - LogLevel = level; - EventId = eventId; -#if NET8_0_OR_GREATER - _state = ref state; - _enrichmentProperties = ref enrichmentProperties; -#else - _state = new ByReference(ref state); - _enrichmentProperties = new ByReference(ref enrichmentProperties); -#endif - Exception = exception; - Formatter = formatter; - } - -#if NET8_0_OR_GREATER - public ref TState State => ref _state; - public ref TEnrichmentProperties EnrichmentProperties => ref _enrichmentProperties; -#else - public ref TState State => ref _state.Value; - public ref TEnrichmentProperties EnrichmentProperties => ref _enrichmentProperties.Value; -#endif - public LogLevel LogLevel { get; } - public EventId EventId { get; } - public Exception? Exception { get; } - public Func? Formatter { get; } - } +// public readonly ref struct LogEntry +// { +//#if NET8_0_OR_GREATER +// private readonly ref TState _state; +// private readonly ref TEnrichmentProperties _enrichmentProperties; +//#else +// // TODO: Explore making intrinsic. Or convert to regular fields. +// private readonly ByReference _state; +// private readonly ByReference _enrichmentProperties; +//#endif + +// public LogEntry(LogLevel level, EventId eventId, ref TState state, ref TEnrichmentProperties enrichmentProperties, Exception? exception, Func? formatter) +// { +// LogLevel = level; +// EventId = eventId; +//#if NET8_0_OR_GREATER +// _state = ref state; +// _enrichmentProperties = ref enrichmentProperties; +//#else +// _state = new ByReference(ref state); +// _enrichmentProperties = new ByReference(ref enrichmentProperties); +//#endif +// Exception = exception; +// Formatter = formatter; +// } + +//#if NET8_0_OR_GREATER +// public ref TState State => ref _state; +// public ref TEnrichmentProperties EnrichmentProperties => ref _enrichmentProperties; +//#else +// public ref TState State => ref _state.Value; +// public ref TEnrichmentProperties EnrichmentProperties => ref _enrichmentProperties.Value; +//#endif +// public LogLevel LogLevel { get; } +// public EventId EventId { get; } +// public Exception? Exception { get; } +// public Func? Formatter { get; } +// } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs index abd36d70fa8d94..e3a36e6d49cbc5 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Logging { @@ -45,16 +46,16 @@ public Pipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequi public class LogEntryPipeline : Pipeline { - public LogEntryPipeline(LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : + public LogEntryPipeline(LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base(userState, isEnabled, isDynamicLevelCheckRequired) { _firstHandler = handler; } - private readonly LogEntryHandler _firstHandler; + private readonly LogEntryHandler _firstHandler; public bool IsEnabledDynamic(LogLevel level) => _firstHandler.IsEnabled(level); - public void HandleLogEntry(ref LogEntry logEntry) => _firstHandler.HandleLogEntry(ref logEntry); + public void HandleLogEntry(ref LogEntry logEntry) => _firstHandler.HandleLogEntry(ref logEntry); } public class ScopePipeline : Pipeline where TState : notnull diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs index 9e6fd4beb01ea1..3b4e4979fb7b3e 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs @@ -225,8 +225,7 @@ void Log(ILogger logger, ref TState state, Exception? exception) if (!pipelineSnapshot.IsEnabled || (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(metadata.LogLevel))) return; - EmptyEnrichmentPropertyValues props = default; - LogEntry entry = new LogEntry(metadata.LogLevel, metadata.EventId, ref state, ref props, exception, null); + LogEntry entry = new LogEntry(metadata.LogLevel, category: null!, metadata.EventId, state, exception, null!); pipelineSnapshot.HandleLogEntry(ref entry); } else @@ -238,8 +237,7 @@ void Log(ILogger logger, ref TState state, Exception? exception) void LogSlowPath(ILogger logger, ref TState state, Exception? exception) { LogEntryPipeline? pipelineSnapshot = null; - EmptyEnrichmentPropertyValues properties = default; - LogEntry entry = new LogEntry(metadata.LogLevel, metadata.EventId, ref state, ref properties, exception, null); + LogEntry entry = new LogEntry(metadata.LogLevel, category: null!, metadata.EventId, state, exception, null!); if (logger is ILogEntryPipelineFactory) { pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetLoggingPipeline(metadata, logger); @@ -299,8 +297,7 @@ void Log(ILogger logger, T1 arg1, T2 arg2, Exception? exception) (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) return; LogValues state = new LogValues(metadata, arg1, arg2); - EmptyEnrichmentPropertyValues props = default; - LogEntry, EmptyEnrichmentPropertyValues> entry = new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref props, exception, LogValues.Callback); + LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, LogValues.Callback); pipelineSnapshot.HandleLogEntry(ref entry); } else @@ -312,7 +309,6 @@ void Log(ILogger logger, T1 arg1, T2 arg2, Exception? exception) void LogSlowPath(ILogger logger, T1 arg1, T2 arg2, Exception? exception) { LogEntryPipeline>? pipelineSnapshot = null; - EmptyEnrichmentPropertyValues properties = default; if (logger is ILogEntryPipelineFactory) { pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetLoggingPipeline(metadata, logger); @@ -324,7 +320,7 @@ void LogSlowPath(ILogger logger, T1 arg1, T2 arg2, Exception? exception) (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) return; LogValues state = new LogValues(metadata, arg1, arg2); - LogEntry, EmptyEnrichmentPropertyValues> entry = new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref properties, exception, LogValues.Callback); + LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, LogValues.Callback); pipelineSnapshot.HandleLogEntry(ref entry); } else @@ -376,9 +372,8 @@ void Log(ILogger logger, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T state._value17 = arg17; state._value18 = arg18; state._value19 = arg19; - EmptyEnrichmentPropertyValues properties = default; - LogEntry, EmptyEnrichmentPropertyValues> entry = - new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref properties, exception, null); + LogEntry> entry = + new LogEntry>(logLevel, category: null!, eventId, state, exception, null!); pipelineSnapshot.HandleLogEntry(ref entry); } else @@ -411,9 +406,8 @@ void LogSlowPath(ILogger logger, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 state._value17 = arg17; state._value18 = arg18; state._value19 = arg19; - EmptyEnrichmentPropertyValues properties = default; - LogEntry, EmptyEnrichmentPropertyValues> entry = - new LogEntry, EmptyEnrichmentPropertyValues>(logLevel, eventId, ref state, ref properties, exception, null); + LogEntry> entry = + new LogEntry>(logLevel, category: null!, eventId, state, exception, null!); if (logger is ILogEntryPipelineFactory) { pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetLoggingPipeline(metadata, logger); diff --git a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs index eee98c4604046d..03005ce8c11443 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Serialization; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Logging { @@ -19,7 +19,7 @@ public DispatchProcessor(LoggerInformation[] loggers, IExternalScopeProvider? ex _externalScopeProvider = externalScopeProvider; } - public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicCheckRequired) + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicCheckRequired) { if (metadata != null) { @@ -28,18 +28,18 @@ public LogEntryHandler GetLogEntryHandler.Instance; + return NullHandler.Instance; } else if (filteredLoggers.Length == 1) { LoggerInformation loggerInfo = filteredLoggers[0]; - LogEntryHandler? handler = null; + LogEntryHandler? handler = null; if (loggerInfo.Processor != null) { - handler = loggerInfo.Processor.GetLogEntryHandler(metadata, out enabled, out dynamicCheckRequired); + handler = loggerInfo.Processor.GetLogEntryHandler(metadata, out enabled, out dynamicCheckRequired); if (handler != null) { - return new DispatchViaHandler(handler); + return new DispatchViaHandler(handler); } } } @@ -48,7 +48,7 @@ public LogEntryHandler GetLogEntryHandler(this, metadata?.GetStringMessageFormatter()); + return new DynamicDispatchToLoggers(this, metadata?.GetStringMessageFormatter()); } public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicCheckRequired) where TState : notnull @@ -110,10 +110,10 @@ static bool LoggerIsEnabled(LogLevel logLevel, ILogger logger, ref List : LogEntryHandler + private sealed class NullHandler : LogEntryHandler { - public static readonly NullHandler Instance = new NullHandler(); - public override void HandleLogEntry(ref LogEntry logEntry) + public static readonly NullHandler Instance = new NullHandler(); + public override void HandleLogEntry(ref LogEntry logEntry) { } public override bool IsEnabled(LogLevel level) => false; @@ -126,16 +126,16 @@ private sealed class NullScopeHandler : ScopeHandler where TStat public override bool IsEnabled(LogLevel level) => false; } - private sealed class DispatchViaHandler : LogEntryHandler + private sealed class DispatchViaHandler : LogEntryHandler { - private LogEntryHandler _nestedHandler; + private LogEntryHandler _nestedHandler; - public DispatchViaHandler(LogEntryHandler handler) + public DispatchViaHandler(LogEntryHandler handler) { _nestedHandler = handler; } - public override void HandleLogEntry(ref LogEntry logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { try { @@ -175,7 +175,7 @@ public DispatchViaScopeHandler(ScopeHandler handler) public override bool IsEnabled(LogLevel level) => _nestedHandler.IsEnabled(level); } - private sealed class DynamicDispatchToLoggers : LogEntryHandler + private sealed class DynamicDispatchToLoggers : LogEntryHandler { private DispatchProcessor _processor; private Func? _formatter; @@ -186,7 +186,7 @@ public DynamicDispatchToLoggers(DispatchProcessor processor, Func logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { Func? formatter = logEntry.Formatter ?? _formatter; formatter ??= (TState s, Exception? _) => s == null ? "" : s.ToString() ?? ""; diff --git a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs index 6afe9871eb30f9..f8716bdba8cccd 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Runtime.Serialization; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Logging { @@ -43,8 +43,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { return; } - EmptyEnrichmentPropertyValues props = default; - LogEntry logEntry = new LogEntry(logLevel, eventId, ref state, ref props, exception, formatter); + LogEntry logEntry = new LogEntry(logLevel, category: null!, eventId, state, exception, formatter); pipeline.HandleLogEntry(ref logEntry); } @@ -170,7 +169,7 @@ public VersionedLoggerState(LoggerInformation[] loggers, ILogEntryProcessor proc { if (!Pipelines.TryGetValue(key, out pipeline)) { - LogEntryHandler handler = Processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicCheckRequired); + LogEntryHandler handler = Processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicCheckRequired); pipeline = new LogEntryPipeline(handler, userState, enabled, dynamicCheckRequired); // in a multi-threaded race it is possible to create new pipelines after the versioned state is already disposed // if this happens the pipeline is immediately marked as being not up-to-date. @@ -225,7 +224,7 @@ private sealed class NullLogProcessor : ILogEntryProcessor { public static readonly NullLogProcessor Instance = new NullLogProcessor(); - public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { throw new NotImplementedException(); } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs b/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs index f3f424511d92d1..ea65b32e6ff4fa 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs @@ -26,7 +26,7 @@ internal sealed class PipelineManager private static LogEntryPipeline BuildPipeline(ILogEntryProcessor processor, ILogMetadata? metadata, object? userState) { - LogEntryHandler handler = processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicCheckRequired); + LogEntryHandler handler = processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicCheckRequired); return new LogEntryPipeline(handler, userState, enabled, dynamicCheckRequired); } } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs index 68657a271f8872..c9b45c0549b839 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs @@ -1,8 +1,10 @@ using System; using System.Buffers; +using System.Collections; using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace ConsoleApp31.Prototype { @@ -59,9 +61,9 @@ public EnrichmentProcessor(EnrichmentPropertiesCollection collection, ILogEntry _propCollection = collection; _nextProcessor = nextProcessor; } - public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - return _propCollection.GetLogEntryHandler(_nextProcessor, metadata, out enabled, out dynamicEnabledCheckRequired); + return _propCollection.GetLogEntryHandler(_nextProcessor, metadata, out enabled, out dynamicEnabledCheckRequired); } public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull @@ -75,7 +77,7 @@ public bool IsEnabled(LogLevel logLevel) } } - class EnrichmentPropertiesCollection + internal class EnrichmentPropertiesCollection { internal virtual EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) { @@ -85,9 +87,9 @@ internal virtual EnrichmentPropertiesCollection AddProperty(string propertyNa return collection; } - internal virtual LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + internal virtual LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - return nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); } } @@ -106,34 +108,68 @@ internal override EnrichmentPropertiesCollection AddProperty(string propertyN return collection; } - internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - LogEntryHandler> nextHandler = - nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); - return new EnrichmentHandler(nextHandler, this); + var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); } - class EnrichmentHandler : LogEntryHandler + private sealed class EnrichmentHandler : LogEntryHandler { - LogEntryHandler> _nextHandler; + LogEntryHandler> _nextHandler; EnrichmentPropertiesCollection _propertyCollection; - public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) + public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) { _nextHandler = nextHandler; _propertyCollection = propertyCollection; } - public override void HandleLogEntry(ref LogEntry logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { - EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); - enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; - enrichmentProperties.Value0 = _propertyCollection.GetValue0(); - var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); + enrichmentProperties.NestedProperties = logEntry.State; + enrichmentProperties.Prop0 = new KeyValuePair(_propertyCollection.Name0, _propertyCollection.GetValue0()); + enrichmentProperties.Formatter = logEntry.Formatter; + var newLogEntry = new LogEntry>(logEntry.LogLevel, category: null, logEntry.EventId, enrichmentProperties, logEntry.Exception, EnrichmentPropertyValues.Format); _nextHandler.HandleLogEntry(ref newLogEntry); } public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); } + + private sealed class EnrichmentLogMetadata : ILogMetadata> + { + private ILogMetadata _innerMetadata { get; } + + public EnrichmentLogMetadata(ILogMetadata innerMetadata) + { + _innerMetadata = innerMetadata; + } + + public LogLevel LogLevel => _innerMetadata.LogLevel; + public EventId EventId => _innerMetadata.EventId; + public string OriginalFormat => _innerMetadata.OriginalFormat; + public int PropertyCount => _innerMetadata.PropertyCount; + public void AppendFormattedMessage(in EnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); + public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) + { + var formatter = _innerMetadata.GetMessageFormatter(customFormatters); + return (e, w) => formatter(e.NestedProperties, w); + } + public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + { + FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); + return new FormatPropertyListAction>((ref EnrichmentPropertyValues s, ref BufferWriter w) => formatter(ref s.NestedProperties, ref w)); + } + public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); + public Func, Exception?, string> GetStringMessageFormatter() + { + var formatter = _innerMetadata.GetStringMessageFormatter(); + return (e, ex) => formatter(e.NestedProperties, ex); + } + } } internal class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection @@ -154,35 +190,70 @@ internal override EnrichmentPropertiesCollection AddProperty(string propertyN return collection; } - internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - LogEntryHandler> nextHandler = - nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); - return new EnrichmentHandler(nextHandler, this); + var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); } - class EnrichmentHandler : LogEntryHandler + private sealed class EnrichmentHandler : LogEntryHandler { - LogEntryHandler> _nextHandler; + LogEntryHandler> _nextHandler; EnrichmentPropertiesCollection _propertyCollection; - public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) + + public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) { _nextHandler = nextHandler; _propertyCollection = propertyCollection; } - public override void HandleLogEntry(ref LogEntry logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { - EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); - enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; - enrichmentProperties.Value0 = _propertyCollection.GetValue0(); - enrichmentProperties.Value1 = _propertyCollection.GetValue1(); - var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); + enrichmentProperties.NestedProperties = logEntry.State; + enrichmentProperties.Prop0 = new KeyValuePair(_propertyCollection.Name0, _propertyCollection.GetValue0()); + enrichmentProperties.Prop1 = new KeyValuePair(_propertyCollection.Name1, _propertyCollection.GetValue1()); + enrichmentProperties.Formatter = logEntry.Formatter; + var newLogEntry = new LogEntry>(logEntry.LogLevel, category: null, logEntry.EventId, enrichmentProperties, logEntry.Exception, EnrichmentPropertyValues.Format); _nextHandler.HandleLogEntry(ref newLogEntry); } public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); } + + private sealed class EnrichmentLogMetadata : ILogMetadata> + { + private ILogMetadata _innerMetadata { get; } + + public EnrichmentLogMetadata(ILogMetadata innerMetadata) + { + _innerMetadata = innerMetadata; + } + + public LogLevel LogLevel => _innerMetadata.LogLevel; + public EventId EventId => _innerMetadata.EventId; + public string OriginalFormat => _innerMetadata.OriginalFormat; + public int PropertyCount => _innerMetadata.PropertyCount; + public void AppendFormattedMessage(in EnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); + public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) + { + var formatter = _innerMetadata.GetMessageFormatter(customFormatters); + return (e, w) => formatter(e.NestedProperties, w); + } + public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + { + FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); + return new FormatPropertyListAction>((ref EnrichmentPropertyValues s, ref BufferWriter w) => formatter(ref s.NestedProperties, ref w)); + } + public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); + public Func, Exception?, string> GetStringMessageFormatter() + { + var formatter = _innerMetadata.GetStringMessageFormatter(); + return (e, ex) => formatter(e.NestedProperties, ex); + } + } } internal class UnboundedEnrichmentPropertiesCollection : EnrichmentPropertiesCollection @@ -199,67 +270,277 @@ internal override EnrichmentPropertiesCollection AddProperty(string propertyN return this; } - internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - LogEntryHandler> nextHandler = - nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); - return new EnrichmentHandler(nextHandler, this); + var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); } - class EnrichmentHandler : LogEntryHandler + private sealed class EnrichmentHandler : LogEntryHandler { - LogEntryHandler> _nextHandler; + LogEntryHandler> _nextHandler; UnboundedEnrichmentPropertiesCollection _propertyCollection; - public EnrichmentHandler(LogEntryHandler> nextHandler, UnboundedEnrichmentPropertiesCollection propertyCollection) + public EnrichmentHandler(LogEntryHandler> nextHandler, UnboundedEnrichmentPropertiesCollection propertyCollection) { _nextHandler = nextHandler; _propertyCollection = propertyCollection; } - public override void HandleLogEntry(ref LogEntry logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { - UnboundedEnrichmentPropertyValues enrichmentProperties = new UnboundedEnrichmentPropertyValues(); - enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; - enrichmentProperties.Value0 = _propertyCollection.GetValue0(); - enrichmentProperties.Value1 = _propertyCollection.GetValue1(); + UnboundedEnrichmentPropertyValues enrichmentProperties = new UnboundedEnrichmentPropertyValues(); + enrichmentProperties.NestedProperties = logEntry.State; + enrichmentProperties.Prop0 = new KeyValuePair(_propertyCollection.Name0, _propertyCollection.GetValue0()); + enrichmentProperties.Prop1 = new KeyValuePair(_propertyCollection.Name1, _propertyCollection.GetValue1()); int overflowProps = _propertyCollection.OverflowProperties.Count; - enrichmentProperties.ExtraValues = ArrayPool.Shared.Rent(overflowProps); - for(int i = 0; i < overflowProps; i++) + enrichmentProperties.ExtraValues = ArrayPool>.Shared.Rent(overflowProps); + for (int i = 0; i < overflowProps; i++) { - enrichmentProperties.ExtraValues[i] = _propertyCollection.OverflowProperties[i].Item2(); + var property = _propertyCollection.OverflowProperties[i]; + enrichmentProperties.ExtraValues[i] = new KeyValuePair(property.Item1, property.Item2()); } - var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + enrichmentProperties.Formatter = logEntry.Formatter; + var newLogEntry = new LogEntry>(logEntry.LogLevel, category: null, logEntry.EventId, enrichmentProperties, logEntry.Exception, UnboundedEnrichmentPropertyValues.Format); _nextHandler.HandleLogEntry(ref newLogEntry); } public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); } - } - internal struct EmptyEnrichmentPropertyValues - { + private sealed class EnrichmentLogMetadata : ILogMetadata> + { + private ILogMetadata _innerMetadata { get; } + + public EnrichmentLogMetadata(ILogMetadata innerMetadata) + { + _innerMetadata = innerMetadata; + } + + public LogLevel LogLevel => _innerMetadata.LogLevel; + public EventId EventId => _innerMetadata.EventId; + public string OriginalFormat => _innerMetadata.OriginalFormat; + public int PropertyCount => _innerMetadata.PropertyCount; + public void AppendFormattedMessage(in UnboundedEnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); + public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) + { + var formatter = _innerMetadata.GetMessageFormatter(customFormatters); + return (e, w) => formatter(e.NestedProperties, w); + } + public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + { + FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); + return new FormatPropertyListAction>((ref UnboundedEnrichmentPropertyValues s, ref BufferWriter w) => formatter(ref s.NestedProperties, ref w)); + } + public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); + public Func, Exception?, string> GetStringMessageFormatter() + { + var formatter = _innerMetadata.GetStringMessageFormatter(); + return (e, ex) => formatter(e.NestedProperties, ex); + } + } } - internal struct EnrichmentPropertyValues + internal struct EnrichmentPropertyValues : IReadOnlyList> { internal TEnrichmentProperties NestedProperties; - internal T0 Value0; + internal KeyValuePair Prop0; + internal Func Formatter; + + public int Count + { + get + { + var nested = NestedProperties as IReadOnlyList>; + return (nested?.Count ?? 0) + 1; + } + } + + public KeyValuePair this[int index] + { + get + { + var nested = NestedProperties as IReadOnlyList>; + if (index == 0) + { + return Prop0; + } + else if (nested != null) + { + return nested[index - 1]; + } + else + { + throw new IndexOutOfRangeException(nameof(index)); + } + } + } + + public override string ToString() => NestedProperties.ToString(); + + public static string Format(EnrichmentPropertyValues state, Exception exception) + { + return state.Formatter(state.NestedProperties, exception); + } + + public IEnumerator> GetEnumerator() + { + yield return Prop0; + + var nested = NestedProperties as IReadOnlyList>; + if (nested != null) + { + foreach (var item in nested) + { + yield return item; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - internal struct EnrichmentPropertyValues + internal struct EnrichmentPropertyValues : IReadOnlyList> { internal TEnrichmentProperties NestedProperties; - internal T0 Value0; - internal T1 Value1; + internal KeyValuePair Prop0; + internal KeyValuePair Prop1; + internal Func Formatter; + + public int Count + { + get + { + var nested = NestedProperties as IReadOnlyList>; + return (nested?.Count ?? 0) + 2; + } + } + + public KeyValuePair this[int index] + { + get + { + var nested = NestedProperties as IReadOnlyList>; + if (index == 0) + { + return Prop0; + } + else if (index == 1) + { + return Prop1; + } + else if (nested != null) + { + return nested[index - 1]; + } + else + { + throw new IndexOutOfRangeException(nameof(index)); + } + } + } + + public override string ToString() => NestedProperties.ToString(); + + public static string Format(EnrichmentPropertyValues state, Exception exception) + { + return state.Formatter(state.NestedProperties, exception); + } + + public IEnumerator> GetEnumerator() + { + yield return Prop0; + yield return Prop1; + + var nested = NestedProperties as IReadOnlyList>; + if (nested != null) + { + for (var i = 0; i < nested.Count; i++) + { + yield return nested[i]; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - internal struct UnboundedEnrichmentPropertyValues + internal struct UnboundedEnrichmentPropertyValues : IReadOnlyList> { internal TEnrichmentProperties NestedProperties; - internal T0 Value0; - internal T1 Value1; - internal object?[] ExtraValues; - } + internal KeyValuePair Prop0; + internal KeyValuePair Prop1; + internal KeyValuePair[] ExtraValues; + internal Func Formatter; + public int Count + { + get + { + var nested = NestedProperties as IReadOnlyList>; + return (nested?.Count ?? 0) + 2 + ExtraValues.Length; + } + } + public KeyValuePair this[int index] + { + get + { + var nested = NestedProperties as IReadOnlyList>; + if (index == 0) + { + return Prop0; + } + else if (index == 1) + { + return Prop1; + } + else + { + var i = index - 2; + if (i < ExtraValues.Length) + { + return ExtraValues[i]; + } + else if (nested != null) + { + return nested[i - ExtraValues.Length]; + } + else + { + throw new IndexOutOfRangeException(nameof(index)); + } + } + } + } + + public override string ToString() => NestedProperties.ToString(); + + public static string Format(UnboundedEnrichmentPropertyValues state, Exception exception) + { + return state.Formatter(state.NestedProperties, exception); + } + + public IEnumerator> GetEnumerator() + { + yield return Prop0; + yield return Prop1; + for (var i = 0; i < ExtraValues.Length; i++) + { + yield return ExtraValues[i]; + } + + var nested = NestedProperties as IReadOnlyList>; + if (nested != null) + { + for (var i = 0; i < nested.Count; i++) + { + yield return nested[i]; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs index 0917b0e8a230ad..97dc332d227e14 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs @@ -7,6 +7,7 @@ using ConsoleApp31.Prototype; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Microsoft.Extensions.Logging.Test @@ -28,7 +29,6 @@ public void LogInformation_InvokesProcessor() builder.SetMinimumLevel(LogLevel.Trace); builder.AddProvider(provider); builder.Enrich("prop1", () => "Value!"); - builder.AddProcessor((serviceProvider, processor) => new TestLogEntryProcessor(processor, m => logMessages.Add(m))); }); var loggerFactory = serviceCollection.BuildServiceProvider().GetRequiredService(); var logger = loggerFactory.CreateLogger("Test"); @@ -37,14 +37,29 @@ public void LogInformation_InvokesProcessor() logger.LogInformation("Hello {Name}", "John Doe"); // Assert - Assert.Collection(logMessages, m => Assert.Equal("Hello John Doe", m)); - Assert.Equal(1, sink.Writes.Count()); Assert.True(sink.Writes.TryTake(out var write)); Assert.Equal(LogLevel.Information, write.LogLevel); Assert.Equal("Hello John Doe", write.State.ToString()); Assert.Equal(0, write.EventId); Assert.Null(write.Exception); + + Assert.Collection((IReadOnlyList>)write.State, + p => + { + Assert.Equal("prop1", p.Key); + Assert.Equal("Value!", p.Value); + }, + p => + { + Assert.Equal("Name", p.Key); + Assert.Equal("John Doe", p.Value); + }, + p => + { + Assert.Equal("{OriginalFormat}", p.Key); + Assert.Equal("Hello {Name}", p.Value); + }); } [Fact] @@ -96,10 +111,10 @@ public TestLogEntryProcessor(ILogEntryProcessor nextProcessor, Action ha _handleLogEntryCallback = handleLogEntryCallback; } - public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); - return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); + var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); } public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull @@ -111,18 +126,18 @@ public ScopeHandler GetScopeHandler(ILogMetadata? metada public bool IsEnabled(LogLevel logLevel) => _nextProcessor.IsEnabled(logLevel); } - private sealed class TestLogEntryHandler : LogEntryHandler + private sealed class TestLogEntryHandler : LogEntryHandler { - private readonly LogEntryHandler _nextHandler; + private readonly LogEntryHandler _nextHandler; private readonly Action _handleLogEntryCallback; - public TestLogEntryHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) + public TestLogEntryHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) { _nextHandler = nextHandler; _handleLogEntryCallback = handleLogEntryCallback; } - public override void HandleLogEntry(ref LogEntry logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { var message = logEntry.Formatter(logEntry.State, logEntry.Exception); _handleLogEntryCallback(message); diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs index a9c5eae47760a9..20b4901ed45938 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; using Xunit; @@ -94,10 +95,10 @@ public TestLogEntryProcessor(ILogEntryProcessor nextProcessor, Action ha _handleLogEntryCallback = handleLogEntryCallback; } - public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); - return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); + var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); } public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull @@ -109,18 +110,18 @@ public ScopeHandler GetScopeHandler(ILogMetadata? metada public bool IsEnabled(LogLevel logLevel) => _nextProcessor.IsEnabled(logLevel); } - private sealed class TestLogEntryHandler : LogEntryHandler + private sealed class TestLogEntryHandler : LogEntryHandler { - private readonly LogEntryHandler _nextHandler; + private readonly LogEntryHandler _nextHandler; private readonly Action _handleLogEntryCallback; - public TestLogEntryHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) + public TestLogEntryHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) { _nextHandler = nextHandler; _handleLogEntryCallback = handleLogEntryCallback; } - public override void HandleLogEntry(ref LogEntry logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { var message = logEntry.Formatter(logEntry.State, logEntry.Exception); _handleLogEntryCallback(message); From f247a78d0ff1c8479614190204e4f2d1242a8a2d Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 24 May 2023 10:20:45 +0800 Subject: [PATCH 11/26] Public API clean up --- .../ref/Microsoft.Extensions.Logging.cs | 11 +---------- .../src/LoggerFactory.cs | 15 +++++++++++++++ .../src/PipelineManager.cs | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs index 2cb8a32b64c6d6..e0662ce435be7d 100644 --- a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs +++ b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs @@ -58,6 +58,7 @@ public LoggerFactory(System.Collections.Generic.IEnumerable providers, Microsoft.Extensions.Logging.LoggerFilterOptions filterOptions) { } public LoggerFactory(System.Collections.Generic.IEnumerable providers, Microsoft.Extensions.Options.IOptionsMonitor filterOption) { } public LoggerFactory(System.Collections.Generic.IEnumerable providers, Microsoft.Extensions.Options.IOptionsMonitor filterOption, Microsoft.Extensions.Options.IOptions? options) { } + public LoggerFactory(System.Collections.Generic.IEnumerable providers, Microsoft.Extensions.Options.IOptionsMonitor filterOption, Microsoft.Extensions.Options.IOptions? options = null, Microsoft.Extensions.Logging.IExternalScopeProvider? scopeProvider = null) { } public LoggerFactory(System.Collections.Generic.IEnumerable providers, System.Collections.Generic.IEnumerable processorFactories, Microsoft.Extensions.Options.IOptionsMonitor filterOption, Microsoft.Extensions.Options.IOptions? options = null, Microsoft.Extensions.Logging.IExternalScopeProvider? scopeProvider = null) { } public void AddProvider(Microsoft.Extensions.Logging.ILoggerProvider provider) { } protected virtual bool CheckDisposed() { throw null; } @@ -95,16 +96,6 @@ public static partial class LoggingBuilderExtensions public static Microsoft.Extensions.Logging.ILoggingBuilder Configure(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action action) { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder SetMinimumLevel(this Microsoft.Extensions.Logging.ILoggingBuilder builder, Microsoft.Extensions.Logging.LogLevel level) { throw null; } } - public readonly partial struct PipelineKey - { - private readonly object _dummy; - private readonly int _dummyPrimitive; - public PipelineKey(bool isLoggingPipeline, object typeOrMetadata, Microsoft.Extensions.Logging.ILogEntryProcessor? terminalProcessor, object? userState) { throw null; } - public bool IsLoggingPipeline { get { throw null; } } - public Microsoft.Extensions.Logging.ILogEntryProcessor? TerminalProcessor { get { throw null; } } - public object TypeOrMetadata { get { throw null; } } - public object? UserState { get { throw null; } } - } [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=false, Inherited=false)] public partial class ProviderAliasAttribute : System.Attribute { diff --git a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs index 1c656305cae638..72c69cde9ede57 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs @@ -72,6 +72,21 @@ public LoggerFactory(IEnumerable providers, IOptionsMonitor + /// Creates a new instance. + /// + /// The providers to use in producing instances. + /// The filter option to use. + /// The . + /// The . + public LoggerFactory(IEnumerable providers, + IOptionsMonitor filterOption, + IOptions? options = null, + IExternalScopeProvider? scopeProvider = null) : + this(providers, Array.Empty(), filterOption, options, scopeProvider) + { + } + /// /// Creates a new instance. /// diff --git a/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs b/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs index ea65b32e6ff4fa..355e4d5bdc9319 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs @@ -31,7 +31,7 @@ private static LogEntryPipeline BuildPipeline(ILogEntryProcessor } } - public readonly struct PipelineKey + internal readonly struct PipelineKey { public PipelineKey(bool isLoggingPipeline, object typeOrMetadata, ILogEntryProcessor? terminalProcessor, object? userState) { From 3adbf030ae2929e402e09ef5e6c2c349a472d65a Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 24 May 2023 11:10:49 +0800 Subject: [PATCH 12/26] Clean up and move enrichment into Extensions.Logging --- ...crosoft.Extensions.Logging.Abstractions.cs | 5 +- .../src/LogEntryNew.cs | 2 +- .../src/LogEntryPipeline.cs | 4 - .../src/LoggerMessage.cs | 4 +- .../ref/Microsoft.Extensions.Logging.cs | 4 + .../src/EnrichmentExtensions.cs | 537 +++++++++++++---- .../tests/Common/EnrichmentProcessor.cs | 546 ------------------ .../tests/Common/EnrichmentTests.cs | 1 - 8 files changed, 433 insertions(+), 670 deletions(-) delete mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index 296f1e886fc59d..2de1bdea4c72e3 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -18,9 +18,6 @@ public void EnsureSize(int minSize) { } public void Flush() { } public void Grow(int minSize) { } } - public partial struct EmptyEnrichmentPropertyValues - { - } public readonly partial struct EventId : System.IEquatable { private readonly object _dummy; @@ -37,7 +34,7 @@ public partial struct EmptyEnrichmentPropertyValues public override string ToString() { throw null; } } public delegate void FormatPropertyAction(PropType propertyValue, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); - public delegate void FormatPropertyListAction(ref TState state, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); + public delegate void FormatPropertyListAction(in TState state, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); public delegate void FormatSpanPropertyAction(System.ReadOnlySpan propertyValue, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); public partial interface IExternalScopeProvider { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index 451273b1a032c7..ada4117687a72c 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -95,7 +95,7 @@ public interface ILogMetadata Func GetStringMessageFormatter(); } - public delegate void FormatPropertyListAction(ref TState state, ref BufferWriter bufferWriter); + public delegate void FormatPropertyListAction(in TState state, ref BufferWriter bufferWriter); public delegate void FormatPropertyAction(PropType propertyValue, ref BufferWriter bufferWriter); public delegate void FormatSpanPropertyAction(scoped ReadOnlySpan propertyValue, ref BufferWriter bufferWriter); diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs index e3a36e6d49cbc5..814f07be8ac50b 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs @@ -71,8 +71,4 @@ public ScopePipeline(ScopeHandler handler, object? userState, bool isEna public bool IsEnabledDynamic(LogLevel level) => _firstHandler.IsEnabled(level); public IDisposable? HandleScope(ref TState scope) => _firstHandler.HandleBeginScope(ref scope); } - - public struct EmptyEnrichmentPropertyValues - { - } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs index 3b4e4979fb7b3e..0bc2f3beeb4c85 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs @@ -765,7 +765,7 @@ public FormatPropertyListAction> GetPropertyListFormatter(IPro FormatPropertyAction formatter1 = propertyFormatterFactory.GetPropertyFormatter(1, GetPropertyMetadata(1)); return FormatPropertyList; - void FormatPropertyList(ref LogValues tstate, ref BufferWriter writer) + void FormatPropertyList(in LogValues tstate, ref BufferWriter writer) { formatter0(tstate._value0, ref writer); formatter1(tstate._value1, ref writer); @@ -963,7 +963,7 @@ public FormatPropertyListAction formatter19 = propertyFormatterFactory.GetPropertyFormatter(19, GetPropertyMetadata(19)); return FormatPropertyList; - void FormatPropertyList(ref LogValues tstate, ref BufferWriter writer) + void FormatPropertyList(in LogValues tstate, ref BufferWriter writer) { formatter0(tstate._value0, ref writer); formatter1(tstate._value1, ref writer); diff --git a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs index e0662ce435be7d..47f6b5ff4969cc 100644 --- a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs +++ b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs @@ -26,6 +26,10 @@ public enum ActivityTrackingOptions Tags = 32, Baggage = 64, } + public static partial class EnrichmentExtensions + { + public static Microsoft.Extensions.Logging.ILoggingBuilder Enrich(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string propertyName, System.Func valueFunc) { throw null; } + } public static partial class FilterLoggingBuilderExtensions { public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Func levelFilter) { throw null; } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs index f0c735ee3756fb..7b0f468227fb19 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs @@ -3,13 +3,13 @@ using System; using System.Buffers; +using System.Collections; using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; -namespace ConsoleApp31.Prototype +namespace Microsoft.Extensions.Logging { - /* public static class EnrichmentExtensions { public static ILoggingBuilder Enrich(this ILoggingBuilder builder, string propertyName, Func valueFunc) @@ -18,7 +18,7 @@ public static ILoggingBuilder Enrich(this ILoggingBuilder builder, string pro { IEnumerable props = sp.GetServices(); EnrichmentPropertiesCollection collection = new EnrichmentPropertiesCollection(); - foreach(var prop in props) + foreach (var prop in props) { collection = prop.AppendTo(collection); } @@ -33,7 +33,7 @@ internal abstract class EnrichmentProperty internal abstract EnrichmentPropertiesCollection AppendTo(EnrichmentPropertiesCollection collection); } - internal class EnrichmentProperty : EnrichmentProperty + internal sealed class EnrichmentProperty : EnrichmentProperty { public EnrichmentProperty(string name, Func getValue) { @@ -51,150 +51,228 @@ internal override EnrichmentPropertiesCollection AppendTo(EnrichmentPropertiesCo } } - internal class EnrichmentPropertiesCollection + internal sealed class EnrichmentProcessor : ILogEntryProcessor { - internal virtual EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) + private readonly ILogEntryProcessor _nextProcessor; + private readonly EnrichmentPropertiesCollection _propCollection; + + public EnrichmentProcessor(EnrichmentPropertiesCollection collection, ILogEntryProcessor nextProcessor) { - var collection = new EnrichmentPropertiesCollection(); - collection.Name0 = propertyName; - collection.GetValue0 = getValue; - return collection; + _propCollection = collection; + _nextProcessor = nextProcessor; } - - internal virtual LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - return nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return _propCollection.GetLogEntryHandler(_nextProcessor, metadata, out enabled, out dynamicEnabledCheckRequired); } - } - internal class EnrichmentProcessor : ILogEntryProcessor - { - private ILogEntryProcessor _nextProcessor; - private EnrichmentPropertiesCollection _propCollection; + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + { + return _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + } - public EnrichmentProcessor(EnrichmentPropertiesCollection collection, ILogEntryProcessor nextProcessor) + public bool IsEnabled(LogLevel logLevel) { - _propCollection = collection; - _nextProcessor = nextProcessor; + return _nextProcessor.IsEnabled(logLevel); } - public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + } + + internal class EnrichmentPropertiesCollection + { + internal virtual EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) { - return _propCollection.GetLogEntryHandler(_nextProcessor, metadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentPropertiesCollection(propertyName, getValue); } - public bool IsEnabled(LogLevel logLevel) + internal virtual LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - return _nextProcessor.IsEnabled(logLevel); + return nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); } } - internal class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection + internal sealed class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection { - internal string Name0; - internal Func GetValue0; + internal readonly string Name0; + internal readonly Func GetValue0; + + public EnrichmentPropertiesCollection(string name0, Func getValue0) + { + Name0 = name0; + GetValue0 = getValue0; + } internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) { - var collection = new EnrichmentPropertiesCollection(); - collection.Name0 = Name0; - collection.GetValue0 = GetValue0; - collection.Name1 = propertyName; - collection.GetValue1 = getValue; - return collection; + return new EnrichmentPropertiesCollection(Name0, GetValue0, propertyName, getValue); } - internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - LogEntryHandler> nextHandler = - nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); - return new EnrichmentHandler(nextHandler, this); + var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); } - private class EnrichmentHandler : LogEntryHandler + private sealed class EnrichmentHandler : LogEntryHandler { - private LogEntryHandler> _nextHandler; + private LogEntryHandler> _nextHandler; private EnrichmentPropertiesCollection _propertyCollection; - public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) + public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) { _nextHandler = nextHandler; _propertyCollection = propertyCollection; } - public override void HandleLogEntry(ref LogEntry logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { -#pragma warning disable SA1129 // Do not use default value type constructor - EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); -#pragma warning restore SA1129 // Do not use default value type constructor - enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; - enrichmentProperties.Value0 = _propertyCollection.GetValue0(); - var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(logEntry.State, new KeyValuePair(_propertyCollection.Name0, _propertyCollection.GetValue0()), logEntry.Formatter); + var newLogEntry = new LogEntry>(logEntry.LogLevel, category: null!, logEntry.EventId, enrichmentProperties, logEntry.Exception, EnrichmentPropertyValues.Format); _nextHandler.HandleLogEntry(ref newLogEntry); } public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); } + + private sealed class EnrichmentLogMetadata : ILogMetadata> + { + private ILogMetadata _innerMetadata { get; } + + public EnrichmentLogMetadata(ILogMetadata innerMetadata) + { + _innerMetadata = innerMetadata; + } + + public LogLevel LogLevel => _innerMetadata.LogLevel; + public EventId EventId => _innerMetadata.EventId; + public string OriginalFormat => _innerMetadata.OriginalFormat; + public int PropertyCount => _innerMetadata.PropertyCount; + public void AppendFormattedMessage(in EnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); + public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) + { + var formatter = _innerMetadata.GetMessageFormatter(customFormatters); + return (e, w) => formatter(e.NestedProperties, w); + } + public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + { + FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); + return new FormatPropertyListAction>((in EnrichmentPropertyValues s, ref BufferWriter w) => formatter(in s.NestedProperties, ref w)); + } + public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); + public Func, Exception?, string> GetStringMessageFormatter() + { + var formatter = _innerMetadata.GetStringMessageFormatter(); + return (e, ex) => formatter(e.NestedProperties, ex); + } + } } - internal class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection + internal sealed class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection { internal string Name0; internal Func GetValue0; internal string Name1; internal Func GetValue1; + public EnrichmentPropertiesCollection(string name0, Func getValue0, string name1, Func getValue1) + { + Name0 = name0; + GetValue0 = getValue0; + Name1 = name1; + GetValue1 = getValue1; + } + internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) { - var collection = new UnboundedEnrichmentPropertiesCollection(); - collection.Name0 = Name0; - collection.GetValue0 = GetValue0; - collection.Name1 = Name1; - collection.GetValue1 = GetValue1; - collection.OverflowProperties.Add((propertyName, () => getValue())); + var overflowProperties = new List<(string, Func)>(); + overflowProperties.Add((propertyName, () => getValue())); + var collection = new UnboundedEnrichmentPropertiesCollection(Name0, GetValue0, Name1, GetValue1, overflowProperties); return collection; } - internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - LogEntryHandler> nextHandler = - nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); - return new EnrichmentHandler(nextHandler, this); + var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); } - private class EnrichmentHandler : LogEntryHandler + private sealed class EnrichmentHandler : LogEntryHandler { - private LogEntryHandler> _nextHandler; - private EnrichmentPropertiesCollection _propertyCollection; + private readonly LogEntryHandler> _nextHandler; + private readonly EnrichmentPropertiesCollection _propertyCollection; - public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) + public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) { _nextHandler = nextHandler; _propertyCollection = propertyCollection; } - public override void HandleLogEntry(ref LogEntry logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { -#pragma warning disable SA1129 // Do not use default value type constructor - EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); -#pragma warning restore SA1129 // Do not use default value type constructor - enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; - enrichmentProperties.Value0 = _propertyCollection.GetValue0(); - enrichmentProperties.Value1 = _propertyCollection.GetValue1(); - var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues( + logEntry.State, + new KeyValuePair(_propertyCollection.Name0, _propertyCollection.GetValue0()), + new KeyValuePair(_propertyCollection.Name1, _propertyCollection.GetValue1()), + logEntry.Formatter); + var newLogEntry = new LogEntry>(logEntry.LogLevel, category: null!, logEntry.EventId, enrichmentProperties, logEntry.Exception, EnrichmentPropertyValues.Format); _nextHandler.HandleLogEntry(ref newLogEntry); } public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); } + + private sealed class EnrichmentLogMetadata : ILogMetadata> + { + private ILogMetadata _innerMetadata { get; } + + public EnrichmentLogMetadata(ILogMetadata innerMetadata) + { + _innerMetadata = innerMetadata; + } + + public LogLevel LogLevel => _innerMetadata.LogLevel; + public EventId EventId => _innerMetadata.EventId; + public string OriginalFormat => _innerMetadata.OriginalFormat; + public int PropertyCount => _innerMetadata.PropertyCount; + public void AppendFormattedMessage(in EnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); + public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) + { + var formatter = _innerMetadata.GetMessageFormatter(customFormatters); + return (e, w) => formatter(e.NestedProperties, w); + } + public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + { + FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); + return new FormatPropertyListAction>((in EnrichmentPropertyValues s, ref BufferWriter w) => formatter(in s.NestedProperties, ref w)); + } + public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); + public Func, Exception?, string> GetStringMessageFormatter() + { + var formatter = _innerMetadata.GetStringMessageFormatter(); + return (e, ex) => formatter(e.NestedProperties, ex); + } + } } - internal class UnboundedEnrichmentPropertiesCollection : EnrichmentPropertiesCollection + internal sealed class UnboundedEnrichmentPropertiesCollection : EnrichmentPropertiesCollection { - internal string Name0; - internal Func GetValue0; - internal string Name1; - internal Func GetValue1; - internal List<(string, Func)> OverflowProperties = new List<(string, Func)>(); + internal readonly string Name0; + internal readonly Func GetValue0; + internal readonly string Name1; + internal readonly Func GetValue1; + internal readonly List<(string, Func)> OverflowProperties; + + public UnboundedEnrichmentPropertiesCollection(string name0, Func getValue0, string name1, Func getValue1, List<(string, Func)> overflowProperties) + { + Name0 = name0; + GetValue0 = getValue0; + Name1 = name1; + GetValue1 = getValue1; + OverflowProperties = overflowProperties; + } internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) { @@ -202,69 +280,304 @@ internal override EnrichmentPropertiesCollection AddProperty(string propertyN return this; } - internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - LogEntryHandler> nextHandler = - nextProcessor.GetLogEntryHandler>(metadata, out enabled, out dynamicEnabledCheckRequired); - return new EnrichmentHandler(nextHandler, this); + var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; + LogEntryHandler> nextHandler = + nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); + return new EnrichmentHandler(nextHandler, this); } - private class EnrichmentHandler : LogEntryHandler + private sealed class EnrichmentHandler : LogEntryHandler { - private LogEntryHandler> _nextHandler; - private UnboundedEnrichmentPropertiesCollection _propertyCollection; + private readonly LogEntryHandler> _nextHandler; + private readonly UnboundedEnrichmentPropertiesCollection _propertyCollection; - public EnrichmentHandler(LogEntryHandler> nextHandler, UnboundedEnrichmentPropertiesCollection propertyCollection) + public EnrichmentHandler(LogEntryHandler> nextHandler, UnboundedEnrichmentPropertiesCollection propertyCollection) { _nextHandler = nextHandler; _propertyCollection = propertyCollection; } - public override void HandleLogEntry(ref LogEntry logEntry) + public override void HandleLogEntry(ref LogEntry logEntry) { -#pragma warning disable SA1129 // Do not use default value type constructor - UnboundedEnrichmentPropertyValues enrichmentProperties = new UnboundedEnrichmentPropertyValues(); -#pragma warning disable SA1129 // Do not use default value type constructor - enrichmentProperties.NestedProperties = logEntry.EnrichmentProperties; - enrichmentProperties.Value0 = _propertyCollection.GetValue0(); - enrichmentProperties.Value1 = _propertyCollection.GetValue1(); int overflowProps = _propertyCollection.OverflowProperties.Count; - enrichmentProperties.ExtraValues = ArrayPool.Shared.Rent(overflowProps); + var extraValues = new KeyValuePair[overflowProps]; for (int i = 0; i < overflowProps; i++) { - enrichmentProperties.ExtraValues[i] = _propertyCollection.OverflowProperties[i].Item2(); + var property = _propertyCollection.OverflowProperties[i]; + extraValues[i] = new KeyValuePair(property.Item1, property.Item2()); } - var newLogEntry = new LogEntry>(logEntry.LogLevel, logEntry.EventId, ref logEntry.State, ref enrichmentProperties, logEntry.Exception, logEntry.Formatter); + + UnboundedEnrichmentPropertyValues enrichmentProperties = new UnboundedEnrichmentPropertyValues( + logEntry.State, + new KeyValuePair(_propertyCollection.Name0, _propertyCollection.GetValue0()), + new KeyValuePair(_propertyCollection.Name1, _propertyCollection.GetValue1()), + extraValues, + logEntry.Formatter); + var newLogEntry = new LogEntry>(logEntry.LogLevel, category: null!, logEntry.EventId, enrichmentProperties, logEntry.Exception, UnboundedEnrichmentPropertyValues.Format); _nextHandler.HandleLogEntry(ref newLogEntry); } public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); } - } - internal struct EmptyEnrichmentPropertyValues - { + private sealed class EnrichmentLogMetadata : ILogMetadata> + { + private ILogMetadata _innerMetadata { get; } + + public EnrichmentLogMetadata(ILogMetadata innerMetadata) + { + _innerMetadata = innerMetadata; + } + + public LogLevel LogLevel => _innerMetadata.LogLevel; + public EventId EventId => _innerMetadata.EventId; + public string OriginalFormat => _innerMetadata.OriginalFormat; + public int PropertyCount => _innerMetadata.PropertyCount; + public void AppendFormattedMessage(in UnboundedEnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); + public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) + { + var formatter = _innerMetadata.GetMessageFormatter(customFormatters); + return (e, w) => formatter(e.NestedProperties, w); + } + public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + { + FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); + return new FormatPropertyListAction>((in UnboundedEnrichmentPropertyValues s, ref BufferWriter w) => formatter(in s.NestedProperties, ref w)); + } + public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); + public Func, Exception?, string> GetStringMessageFormatter() + { + var formatter = _innerMetadata.GetStringMessageFormatter(); + return (e, ex) => formatter(e.NestedProperties, ex); + } + } } - internal struct EnrichmentPropertyValues + internal readonly struct EnrichmentPropertyValues : IReadOnlyList> { - internal TEnrichmentProperties NestedProperties; - internal T0 Value0; + internal readonly TEnrichmentProperties NestedProperties; + internal readonly KeyValuePair Prop0; + internal readonly Func Formatter; + + public EnrichmentPropertyValues(TEnrichmentProperties nestedProperties, KeyValuePair prop0, Func formatter) + { + NestedProperties = nestedProperties; + Prop0 = prop0; + Formatter = formatter; + } + + public int Count + { + get + { + var nested = NestedProperties as IReadOnlyList>; + return (nested?.Count ?? 0) + 1; + } + } + + public KeyValuePair this[int index] + { + get + { + var nested = NestedProperties as IReadOnlyList>; + if (index == 0) + { + return Prop0; + } + else if (nested != null) + { + return nested[index - 1]; + } + else + { + throw new IndexOutOfRangeException(nameof(index)); + } + } + } + + public override string ToString() => NestedProperties?.ToString() ?? string.Empty; + + public static string Format(EnrichmentPropertyValues state, Exception? exception) + { + return state.Formatter(state.NestedProperties, exception); + } + + public IEnumerator> GetEnumerator() + { + yield return Prop0; + + var nested = NestedProperties as IReadOnlyList>; + if (nested != null) + { + foreach (var item in nested) + { + yield return item; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - internal struct EnrichmentPropertyValues + internal readonly struct EnrichmentPropertyValues : IReadOnlyList> { - internal TEnrichmentProperties NestedProperties; - internal T0 Value0; - internal T1 Value1; + internal readonly TEnrichmentProperties NestedProperties; + internal readonly KeyValuePair Prop0; + internal readonly KeyValuePair Prop1; + internal readonly Func Formatter; + + public EnrichmentPropertyValues(TEnrichmentProperties nestedProperties, KeyValuePair prop0, KeyValuePair prop1, Func formatter) + { + NestedProperties = nestedProperties; + Prop0 = prop0; + Prop1 = prop1; + Formatter = formatter; + } + + public int Count + { + get + { + var nested = NestedProperties as IReadOnlyList>; + return (nested?.Count ?? 0) + 2; + } + } + + public KeyValuePair this[int index] + { + get + { + var nested = NestedProperties as IReadOnlyList>; + if (index == 0) + { + return Prop0; + } + else if (index == 1) + { + return Prop1; + } + else if (nested != null) + { + return nested[index - 1]; + } + else + { + throw new IndexOutOfRangeException(nameof(index)); + } + } + } + + public override string ToString() => NestedProperties?.ToString() ?? string.Empty; + + public static string Format(EnrichmentPropertyValues state, Exception? exception) + { + return state.Formatter(state.NestedProperties, exception); + } + + public IEnumerator> GetEnumerator() + { + yield return Prop0; + yield return Prop1; + + var nested = NestedProperties as IReadOnlyList>; + if (nested != null) + { + for (var i = 0; i < nested.Count; i++) + { + yield return nested[i]; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - internal struct UnboundedEnrichmentPropertyValues + internal readonly struct UnboundedEnrichmentPropertyValues : IReadOnlyList> { - internal TEnrichmentProperties NestedProperties; - internal T0 Value0; - internal T1 Value1; - internal object?[] ExtraValues; + internal readonly TEnrichmentProperties NestedProperties; + internal readonly KeyValuePair Prop0; + internal readonly KeyValuePair Prop1; + internal readonly KeyValuePair[] ExtraValues; + internal readonly Func Formatter; + + public UnboundedEnrichmentPropertyValues(TEnrichmentProperties nestedProperties, KeyValuePair prop0, KeyValuePair prop1, KeyValuePair[] extraValues, Func formatter) + { + NestedProperties = nestedProperties; + Prop0 = prop0; + Prop1 = prop1; + ExtraValues = extraValues; + Formatter = formatter; + } + + public int Count + { + get + { + var nested = NestedProperties as IReadOnlyList>; + return (nested?.Count ?? 0) + 2 + ExtraValues.Length; + } + } + + public KeyValuePair this[int index] + { + get + { + var nested = NestedProperties as IReadOnlyList>; + if (index == 0) + { + return Prop0; + } + else if (index == 1) + { + return Prop1; + } + else + { + var i = index - 2; + if (i < ExtraValues.Length) + { + return ExtraValues[i]; + } + else if (nested != null) + { + return nested[i - ExtraValues.Length]; + } + else + { + throw new IndexOutOfRangeException(nameof(index)); + } + } + } + } + + public override string ToString() => NestedProperties?.ToString() ?? string.Empty; + + public static string Format(UnboundedEnrichmentPropertyValues state, Exception? exception) + { + return state.Formatter(state.NestedProperties, exception); + } + + public IEnumerator> GetEnumerator() + { + yield return Prop0; + yield return Prop1; + for (var i = 0; i < ExtraValues.Length; i++) + { + yield return ExtraValues[i]; + } + + var nested = NestedProperties as IReadOnlyList>; + if (nested != null) + { + for (var i = 0; i < nested.Count; i++) + { + yield return nested[i]; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - */ } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs deleted file mode 100644 index c9b45c0549b839..00000000000000 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentProcessor.cs +++ /dev/null @@ -1,546 +0,0 @@ -using System; -using System.Buffers; -using System.Collections; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace ConsoleApp31.Prototype -{ - public static class EnrichmentExtensions - { - public static ILoggingBuilder Enrich(this ILoggingBuilder builder, string propertyName, Func valueFunc) - { - builder.AddProcessor((sp, next) => - { - IEnumerable props = sp.GetServices(); - EnrichmentPropertiesCollection collection = new EnrichmentPropertiesCollection(); - foreach (var prop in props) - { - collection = prop.AppendTo(collection); - } - return new EnrichmentProcessor(collection, next); - }); - builder.Services.AddSingleton(new EnrichmentProperty(propertyName, valueFunc)); - return builder; - } - - internal abstract class EnrichmentProperty - { - internal abstract EnrichmentPropertiesCollection AppendTo(EnrichmentPropertiesCollection collection); - } - - internal class EnrichmentProperty : EnrichmentProperty - { - public EnrichmentProperty(string name, Func getValue) - { - Name = name; - GetValue = getValue; - } - - public string Name { get; } - public Func GetValue { get; } - - internal override EnrichmentPropertiesCollection AppendTo(EnrichmentPropertiesCollection collection) - { - return collection.AddProperty(Name, GetValue); - } - } - } - - - - internal class EnrichmentProcessor : ILogEntryProcessor - { - ILogEntryProcessor _nextProcessor; - EnrichmentPropertiesCollection _propCollection; - - public EnrichmentProcessor(EnrichmentPropertiesCollection collection, ILogEntryProcessor nextProcessor) - { - _propCollection = collection; - _nextProcessor = nextProcessor; - } - public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) - { - return _propCollection.GetLogEntryHandler(_nextProcessor, metadata, out enabled, out dynamicEnabledCheckRequired); - } - - public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull - { - return _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); - } - - public bool IsEnabled(LogLevel logLevel) - { - return _nextProcessor.IsEnabled(logLevel); - } - } - - internal class EnrichmentPropertiesCollection - { - internal virtual EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) - { - var collection = new EnrichmentPropertiesCollection(); - collection.Name0 = propertyName; - collection.GetValue0 = getValue; - return collection; - } - - internal virtual LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) - { - return nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); - } - } - - internal class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection - { - internal string Name0; - internal Func GetValue0; - - internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) - { - var collection = new EnrichmentPropertiesCollection(); - collection.Name0 = Name0; - collection.GetValue0 = GetValue0; - collection.Name1 = propertyName; - collection.GetValue1 = getValue; - return collection; - } - - internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) - { - var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; - LogEntryHandler> nextHandler = - nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); - return new EnrichmentHandler(nextHandler, this); - } - - private sealed class EnrichmentHandler : LogEntryHandler - { - LogEntryHandler> _nextHandler; - EnrichmentPropertiesCollection _propertyCollection; - public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) - { - _nextHandler = nextHandler; - _propertyCollection = propertyCollection; - } - - public override void HandleLogEntry(ref LogEntry logEntry) - { - EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); - enrichmentProperties.NestedProperties = logEntry.State; - enrichmentProperties.Prop0 = new KeyValuePair(_propertyCollection.Name0, _propertyCollection.GetValue0()); - enrichmentProperties.Formatter = logEntry.Formatter; - var newLogEntry = new LogEntry>(logEntry.LogLevel, category: null, logEntry.EventId, enrichmentProperties, logEntry.Exception, EnrichmentPropertyValues.Format); - _nextHandler.HandleLogEntry(ref newLogEntry); - } - - public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); - } - - private sealed class EnrichmentLogMetadata : ILogMetadata> - { - private ILogMetadata _innerMetadata { get; } - - public EnrichmentLogMetadata(ILogMetadata innerMetadata) - { - _innerMetadata = innerMetadata; - } - - public LogLevel LogLevel => _innerMetadata.LogLevel; - public EventId EventId => _innerMetadata.EventId; - public string OriginalFormat => _innerMetadata.OriginalFormat; - public int PropertyCount => _innerMetadata.PropertyCount; - public void AppendFormattedMessage(in EnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); - public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) - { - var formatter = _innerMetadata.GetMessageFormatter(customFormatters); - return (e, w) => formatter(e.NestedProperties, w); - } - public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) - { - FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); - return new FormatPropertyListAction>((ref EnrichmentPropertyValues s, ref BufferWriter w) => formatter(ref s.NestedProperties, ref w)); - } - public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); - public Func, Exception?, string> GetStringMessageFormatter() - { - var formatter = _innerMetadata.GetStringMessageFormatter(); - return (e, ex) => formatter(e.NestedProperties, ex); - } - } - } - - internal class EnrichmentPropertiesCollection : EnrichmentPropertiesCollection - { - internal string Name0; - internal Func GetValue0; - internal string Name1; - internal Func GetValue1; - - internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) - { - var collection = new UnboundedEnrichmentPropertiesCollection(); - collection.Name0 = Name0; - collection.GetValue0 = GetValue0; - collection.Name1 = Name1; - collection.GetValue1 = GetValue1; - collection.OverflowProperties.Add((propertyName, () => getValue())); - return collection; - } - - internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) - { - var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; - LogEntryHandler> nextHandler = - nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); - return new EnrichmentHandler(nextHandler, this); - } - - private sealed class EnrichmentHandler : LogEntryHandler - { - LogEntryHandler> _nextHandler; - EnrichmentPropertiesCollection _propertyCollection; - - public EnrichmentHandler(LogEntryHandler> nextHandler, EnrichmentPropertiesCollection propertyCollection) - { - _nextHandler = nextHandler; - _propertyCollection = propertyCollection; - } - - public override void HandleLogEntry(ref LogEntry logEntry) - { - EnrichmentPropertyValues enrichmentProperties = new EnrichmentPropertyValues(); - enrichmentProperties.NestedProperties = logEntry.State; - enrichmentProperties.Prop0 = new KeyValuePair(_propertyCollection.Name0, _propertyCollection.GetValue0()); - enrichmentProperties.Prop1 = new KeyValuePair(_propertyCollection.Name1, _propertyCollection.GetValue1()); - enrichmentProperties.Formatter = logEntry.Formatter; - var newLogEntry = new LogEntry>(logEntry.LogLevel, category: null, logEntry.EventId, enrichmentProperties, logEntry.Exception, EnrichmentPropertyValues.Format); - _nextHandler.HandleLogEntry(ref newLogEntry); - } - - public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); - } - - private sealed class EnrichmentLogMetadata : ILogMetadata> - { - private ILogMetadata _innerMetadata { get; } - - public EnrichmentLogMetadata(ILogMetadata innerMetadata) - { - _innerMetadata = innerMetadata; - } - - public LogLevel LogLevel => _innerMetadata.LogLevel; - public EventId EventId => _innerMetadata.EventId; - public string OriginalFormat => _innerMetadata.OriginalFormat; - public int PropertyCount => _innerMetadata.PropertyCount; - public void AppendFormattedMessage(in EnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); - public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) - { - var formatter = _innerMetadata.GetMessageFormatter(customFormatters); - return (e, w) => formatter(e.NestedProperties, w); - } - public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) - { - FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); - return new FormatPropertyListAction>((ref EnrichmentPropertyValues s, ref BufferWriter w) => formatter(ref s.NestedProperties, ref w)); - } - public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); - public Func, Exception?, string> GetStringMessageFormatter() - { - var formatter = _innerMetadata.GetStringMessageFormatter(); - return (e, ex) => formatter(e.NestedProperties, ex); - } - } - } - - internal class UnboundedEnrichmentPropertiesCollection : EnrichmentPropertiesCollection - { - internal string Name0; - internal Func GetValue0; - internal string Name1; - internal Func GetValue1; - internal List<(string, Func)> OverflowProperties = new List<(string, Func)>(); - - internal override EnrichmentPropertiesCollection AddProperty(string propertyName, Func getValue) - { - OverflowProperties.Add((propertyName, () => getValue())); - return this; - } - - internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) - { - var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; - LogEntryHandler> nextHandler = - nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); - return new EnrichmentHandler(nextHandler, this); - } - - private sealed class EnrichmentHandler : LogEntryHandler - { - LogEntryHandler> _nextHandler; - UnboundedEnrichmentPropertiesCollection _propertyCollection; - public EnrichmentHandler(LogEntryHandler> nextHandler, UnboundedEnrichmentPropertiesCollection propertyCollection) - { - _nextHandler = nextHandler; - _propertyCollection = propertyCollection; - } - - public override void HandleLogEntry(ref LogEntry logEntry) - { - UnboundedEnrichmentPropertyValues enrichmentProperties = new UnboundedEnrichmentPropertyValues(); - enrichmentProperties.NestedProperties = logEntry.State; - enrichmentProperties.Prop0 = new KeyValuePair(_propertyCollection.Name0, _propertyCollection.GetValue0()); - enrichmentProperties.Prop1 = new KeyValuePair(_propertyCollection.Name1, _propertyCollection.GetValue1()); - int overflowProps = _propertyCollection.OverflowProperties.Count; - enrichmentProperties.ExtraValues = ArrayPool>.Shared.Rent(overflowProps); - for (int i = 0; i < overflowProps; i++) - { - var property = _propertyCollection.OverflowProperties[i]; - enrichmentProperties.ExtraValues[i] = new KeyValuePair(property.Item1, property.Item2()); - } - enrichmentProperties.Formatter = logEntry.Formatter; - var newLogEntry = new LogEntry>(logEntry.LogLevel, category: null, logEntry.EventId, enrichmentProperties, logEntry.Exception, UnboundedEnrichmentPropertyValues.Format); - _nextHandler.HandleLogEntry(ref newLogEntry); - } - - public override bool IsEnabled(LogLevel level) => _nextHandler.IsEnabled(level); - } - - private sealed class EnrichmentLogMetadata : ILogMetadata> - { - private ILogMetadata _innerMetadata { get; } - - public EnrichmentLogMetadata(ILogMetadata innerMetadata) - { - _innerMetadata = innerMetadata; - } - - public LogLevel LogLevel => _innerMetadata.LogLevel; - public EventId EventId => _innerMetadata.EventId; - public string OriginalFormat => _innerMetadata.OriginalFormat; - public int PropertyCount => _innerMetadata.PropertyCount; - public void AppendFormattedMessage(in UnboundedEnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); - public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) - { - var formatter = _innerMetadata.GetMessageFormatter(customFormatters); - return (e, w) => formatter(e.NestedProperties, w); - } - public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) - { - FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); - return new FormatPropertyListAction>((ref UnboundedEnrichmentPropertyValues s, ref BufferWriter w) => formatter(ref s.NestedProperties, ref w)); - } - public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); - public Func, Exception?, string> GetStringMessageFormatter() - { - var formatter = _innerMetadata.GetStringMessageFormatter(); - return (e, ex) => formatter(e.NestedProperties, ex); - } - } - } - - internal struct EnrichmentPropertyValues : IReadOnlyList> - { - internal TEnrichmentProperties NestedProperties; - internal KeyValuePair Prop0; - internal Func Formatter; - - public int Count - { - get - { - var nested = NestedProperties as IReadOnlyList>; - return (nested?.Count ?? 0) + 1; - } - } - - public KeyValuePair this[int index] - { - get - { - var nested = NestedProperties as IReadOnlyList>; - if (index == 0) - { - return Prop0; - } - else if (nested != null) - { - return nested[index - 1]; - } - else - { - throw new IndexOutOfRangeException(nameof(index)); - } - } - } - - public override string ToString() => NestedProperties.ToString(); - - public static string Format(EnrichmentPropertyValues state, Exception exception) - { - return state.Formatter(state.NestedProperties, exception); - } - - public IEnumerator> GetEnumerator() - { - yield return Prop0; - - var nested = NestedProperties as IReadOnlyList>; - if (nested != null) - { - foreach (var item in nested) - { - yield return item; - } - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - internal struct EnrichmentPropertyValues : IReadOnlyList> - { - internal TEnrichmentProperties NestedProperties; - internal KeyValuePair Prop0; - internal KeyValuePair Prop1; - internal Func Formatter; - - public int Count - { - get - { - var nested = NestedProperties as IReadOnlyList>; - return (nested?.Count ?? 0) + 2; - } - } - - public KeyValuePair this[int index] - { - get - { - var nested = NestedProperties as IReadOnlyList>; - if (index == 0) - { - return Prop0; - } - else if (index == 1) - { - return Prop1; - } - else if (nested != null) - { - return nested[index - 1]; - } - else - { - throw new IndexOutOfRangeException(nameof(index)); - } - } - } - - public override string ToString() => NestedProperties.ToString(); - - public static string Format(EnrichmentPropertyValues state, Exception exception) - { - return state.Formatter(state.NestedProperties, exception); - } - - public IEnumerator> GetEnumerator() - { - yield return Prop0; - yield return Prop1; - - var nested = NestedProperties as IReadOnlyList>; - if (nested != null) - { - for (var i = 0; i < nested.Count; i++) - { - yield return nested[i]; - } - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - internal struct UnboundedEnrichmentPropertyValues : IReadOnlyList> - { - internal TEnrichmentProperties NestedProperties; - internal KeyValuePair Prop0; - internal KeyValuePair Prop1; - internal KeyValuePair[] ExtraValues; - internal Func Formatter; - - public int Count - { - get - { - var nested = NestedProperties as IReadOnlyList>; - return (nested?.Count ?? 0) + 2 + ExtraValues.Length; - } - } - - public KeyValuePair this[int index] - { - get - { - var nested = NestedProperties as IReadOnlyList>; - if (index == 0) - { - return Prop0; - } - else if (index == 1) - { - return Prop1; - } - else - { - var i = index - 2; - if (i < ExtraValues.Length) - { - return ExtraValues[i]; - } - else if (nested != null) - { - return nested[i - ExtraValues.Length]; - } - else - { - throw new IndexOutOfRangeException(nameof(index)); - } - } - } - } - - public override string ToString() => NestedProperties.ToString(); - - public static string Format(UnboundedEnrichmentPropertyValues state, Exception exception) - { - return state.Formatter(state.NestedProperties, exception); - } - - public IEnumerator> GetEnumerator() - { - yield return Prop0; - yield return Prop1; - for (var i = 0; i < ExtraValues.Length; i++) - { - yield return ExtraValues[i]; - } - - var nested = NestedProperties as IReadOnlyList>; - if (nested != null) - { - for (var i = 0; i < nested.Count; i++) - { - yield return nested[i]; - } - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs index 97dc332d227e14..0720121f177019 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using ConsoleApp31.Prototype; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Abstractions; From a069f8a83ba0a69ab7ca3019e909b75fe4e71ebf Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 26 May 2023 13:51:20 +0800 Subject: [PATCH 13/26] Remove old log entry --- .../src/LogEntryNew.cs | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index ada4117687a72c..079e52c842f11c 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -113,43 +113,4 @@ public abstract class PropertyCustomFormatter public virtual void AppendFormatted(int index, string value, IBufferWriter buffer) => AppendFormatted(index, value, buffer); public abstract void AppendFormatted(int index, T value, IBufferWriter buffer); } - -// public readonly ref struct LogEntry -// { -//#if NET8_0_OR_GREATER -// private readonly ref TState _state; -// private readonly ref TEnrichmentProperties _enrichmentProperties; -//#else -// // TODO: Explore making intrinsic. Or convert to regular fields. -// private readonly ByReference _state; -// private readonly ByReference _enrichmentProperties; -//#endif - -// public LogEntry(LogLevel level, EventId eventId, ref TState state, ref TEnrichmentProperties enrichmentProperties, Exception? exception, Func? formatter) -// { -// LogLevel = level; -// EventId = eventId; -//#if NET8_0_OR_GREATER -// _state = ref state; -// _enrichmentProperties = ref enrichmentProperties; -//#else -// _state = new ByReference(ref state); -// _enrichmentProperties = new ByReference(ref enrichmentProperties); -//#endif -// Exception = exception; -// Formatter = formatter; -// } - -//#if NET8_0_OR_GREATER -// public ref TState State => ref _state; -// public ref TEnrichmentProperties EnrichmentProperties => ref _enrichmentProperties; -//#else -// public ref TState State => ref _state.Value; -// public ref TEnrichmentProperties EnrichmentProperties => ref _enrichmentProperties.Value; -//#endif -// public LogLevel LogLevel { get; } -// public EventId EventId { get; } -// public Exception? Exception { get; } -// public Func? Formatter { get; } -// } } From c3fd52b6679ccd6627b9e1eb12014b5a69a5febf Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 28 May 2023 13:09:54 +0800 Subject: [PATCH 14/26] Redecation example and tests --- .../Common/Redaction/RedactionProcessor.cs | 494 ++++++++++++++++++ .../tests/Common/Redaction/RedactionTests.cs | 101 ++++ 2 files changed, 595 insertions(+) create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs new file mode 100644 index 00000000000000..7c5b409e54ce8f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs @@ -0,0 +1,494 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET8_0_OR_GREATER + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.Extensions.Logging.Tests.Redaction +{ + public static class RedactionExtensions + { + public static ILoggingBuilder AddRedactionProcessor(this ILoggingBuilder builder) + { + builder.AddProcessor((serviceProvider, processor) => new RedactionProcessor(processor, serviceProvider.GetService())); + return builder; + } + } + + internal interface IRedactorProvider + { + IRedactor GetRedactor(DataClass dataClass); + } + + internal interface IRedactor + { + string Redact(ReadOnlySpan source); + int Redact(ReadOnlySpan source, Span destination); + int GetRedactedLength(ReadOnlySpan source); + } + + internal enum DataClass + { + Unknown, + EUPI + } + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true)] + internal abstract class DataClassificationAttribute : Attribute + { + public string Notes { get; set; } = string.Empty; + public DataClass DataClass { get; } + + protected DataClassificationAttribute(DataClass dataClass) + { + DataClass = dataClass; + } + } + + internal class RedactionProcessor : ILogEntryProcessor + { + private readonly ILogEntryProcessor _nextProcessor; + private readonly IRedactorProvider? _redactorProvider; + + public RedactionProcessor(ILogEntryProcessor nextProcessor, IRedactorProvider? provider) + { + _nextProcessor = nextProcessor; + _redactorProvider = provider; + } + + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicCheckRequired) + { + if (metadata != null && _redactorProvider != null) + { + PropertyRedaction[] redactions = GetPolicyRedactions(metadata); + if (redactions.Length > 0) + { + RedactedLogMetadata redactedMetadata = new RedactedLogMetadata(metadata, redactions); + return new RedactionHandler(redactedMetadata, + _nextProcessor.GetLogEntryHandler>(redactedMetadata, out enabled, out dynamicCheckRequired)); + } + } + return _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicCheckRequired); + } + + PropertyRedaction[] GetPolicyRedactions(ILogMetadata metadata) + { + List redactions = new List(); + for (int i = 0; i < metadata.PropertyCount; i++) + { + LogPropertyMetadata propMetadata = metadata.GetPropertyMetadata(i); + if (propMetadata.Attributes == null) + { + continue; + } + DataClassificationAttribute? dataClassAttr = propMetadata.Attributes.OfType().FirstOrDefault(); + if (dataClassAttr != null) + { + redactions.Add(new PropertyRedaction(i, _redactorProvider!.GetRedactor(dataClassAttr.DataClass))); + } + } + return redactions.ToArray(); + } + + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + { + return _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + } + + public bool IsEnabled(LogLevel level) => _nextProcessor.IsEnabled(level); + + class RedactionHandler : LogEntryHandler + { + LogEntryHandler> _nextHandler; + RedactedLogMetadata _metadata; + + public RedactionHandler(RedactedLogMetadata metadata, LogEntryHandler> nextHandler) + { + _metadata = metadata; + _nextHandler = nextHandler; + } + public override void HandleLogEntry(ref LogEntry logEntry) + { + RedactedValues redactedValuesState = new RedactedValues(_metadata, logEntry.State); + LogEntry> copy = new LogEntry>(logEntry.LogLevel, category: null!, logEntry.EventId, redactedValuesState, logEntry.Exception, RedactedValues.Callback); + _nextHandler.HandleLogEntry(ref copy); + } + + public override bool IsEnabled(LogLevel level) + { + return _nextHandler.IsEnabled(level); + } + } + } + + internal struct PropertyRedaction + { + public PropertyRedaction(int index, IRedactor redactor) + { + Index = index; + Redactor = redactor; + } + public int Index; + public IRedactor Redactor; + } + + + internal class RedactedPropertyFormatter : PropertyCustomFormatter + { + const int MaxStackAllocChars = 256; + IRedactor _redactor; + + public RedactedPropertyFormatter(IRedactor redactor) + { + _redactor = redactor; + } + + public override void AppendFormatted(int index, string value, IBufferWriter buffer) + { + int len = _redactor.GetRedactedLength(value); + if (len != 0) + { + Span redactedBuffer = buffer.GetSpan(len); + _redactor.Redact(value, redactedBuffer); + buffer.Advance(len); + } + } + + public override void AppendFormatted(int index, T value, IBufferWriter buffer) + { + if (value == null) + { + return; + } + if (value is ISpanFormattable) + { + Span unredactedBuffer = stackalloc char[MaxStackAllocChars]; + if (((ISpanFormattable)value).TryFormat(unredactedBuffer, out int charsWritten2, null, null)) + { + unredactedBuffer = unredactedBuffer.Slice(0, charsWritten2); + int len2 = _redactor.GetRedactedLength(unredactedBuffer); + if (len2 != 0) + { + Span redactedBuffer2 = buffer.GetSpan(len2); + _redactor.Redact(unredactedBuffer, redactedBuffer2); + buffer.Advance(len2); + } + return; + } + } + string? unredactedValue = value.ToString(); + int len = _redactor.GetRedactedLength(unredactedValue); + if (len != 0) + { + Span redactedBuffer = buffer.GetSpan(len); + _redactor.Redact(unredactedValue, redactedBuffer); + buffer.Advance(len); + } + } + } + + internal class ChainedRedactedPropertyFormatter : PropertyCustomFormatter + { + const int MaxStackAllocChars = 256; + IRedactor _redactor; + PropertyCustomFormatter _nextFormatter; + + public ChainedRedactedPropertyFormatter(IRedactor redactor, PropertyCustomFormatter nextFormatter) + { + _redactor = redactor; + _nextFormatter = nextFormatter; + } + + public override void AppendFormatted(int index, string value, IBufferWriter buffer) + { + int len = _redactor.GetRedactedLength(value); + if (len <= MaxStackAllocChars) + { + Span redactedBuffer = stackalloc char[len]; + _redactor.Redact(value, redactedBuffer); + _nextFormatter.AppendFormatted(index, redactedBuffer, buffer); + } + else + { + string redactedValue = _redactor.Redact(value); + _nextFormatter.AppendFormatted(index, redactedValue, buffer); + } + } + + public override void AppendFormatted(int index, T value, IBufferWriter buffer) + { + if (value == null) + { + return; + } + else if (value is ISpanFormattable) + { + Span unredactedBuffer = stackalloc char[MaxStackAllocChars]; + if (((ISpanFormattable)value).TryFormat(unredactedBuffer, out int charsWritten2, null, null)) + { + unredactedBuffer = unredactedBuffer.Slice(0, charsWritten2); + int len2 = _redactor.GetRedactedLength(unredactedBuffer); + if (len2 <= MaxStackAllocChars) + { + Span redactedBuffer2 = stackalloc char[len2]; + _redactor.Redact(unredactedBuffer, redactedBuffer2); + _nextFormatter.AppendFormatted(index, redactedBuffer2, buffer); + } + else + { + string redactedValue = _redactor.Redact(unredactedBuffer); + _nextFormatter.AppendFormatted(index, redactedValue, buffer); + } + return; + } + } + string? unredactedValue = value.ToString(); + int len = _redactor.GetRedactedLength(unredactedValue); + if (len <= MaxStackAllocChars) + { + Span redactedBuffer = stackalloc char[len]; + _redactor.Redact(unredactedValue, redactedBuffer); + _nextFormatter.AppendFormatted(index, redactedBuffer, buffer); + } + else + { + string redactedValue = _redactor.Redact(unredactedValue); + _nextFormatter.AppendFormatted(index, redactedValue, buffer); + } + } + } + + internal class RedactedLogMetadata : ILogMetadata> + { + ILogMetadata _originalMetadata; + PropertyRedaction[] _redactions; + Action>? _defaultFormatter; + public RedactedLogMetadata(ILogMetadata metadata, PropertyRedaction[] redactions) + { + _originalMetadata = metadata; + _redactions = redactions; + + } + public ILogMetadata OriginalMetadata => _originalMetadata; + + public LogLevel LogLevel => _originalMetadata.LogLevel; + + public EventId EventId => _originalMetadata.EventId; + + public string OriginalFormat => _originalMetadata.OriginalFormat; + + public int PropertyCount => _originalMetadata.PropertyCount; + + public void AppendFormattedMessage(in RedactedValues state, IBufferWriter buffer) + { + if (_defaultFormatter == null) + { + RedactedPropertyFormatter[] propertyRedactors = new RedactedPropertyFormatter[PropertyCount]; + foreach (PropertyRedaction redaction in _redactions) + { + propertyRedactors[redaction.Index] = new RedactedPropertyFormatter(redaction.Redactor); + } + // this could be overwritten by another thread in a race but it doesn't matter + // as any copy of this delegate will have the same functionality + _defaultFormatter = _originalMetadata.GetMessageFormatter(propertyRedactors); + } + _defaultFormatter(state.OriginalState, buffer); + } + + public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) + { + PropertyCustomFormatter[] wrappedFormatters = new PropertyCustomFormatter[customFormatters.Length]; + Array.Copy(customFormatters, wrappedFormatters, customFormatters.Length); + foreach (PropertyRedaction redaction in _redactions) + { + PropertyCustomFormatter nextFormatter = wrappedFormatters[redaction.Index]; + wrappedFormatters[redaction.Index] = nextFormatter == null ? + new RedactedPropertyFormatter(redaction.Redactor) : + new ChainedRedactedPropertyFormatter(redaction.Redactor, nextFormatter); + } + Action> innerFormatter = _originalMetadata.GetMessageFormatter(wrappedFormatters); + return (state, buffer) => innerFormatter(state.OriginalState, buffer); + } + + public LogPropertyMetadata GetPropertyMetadata(int index) => _originalMetadata.GetPropertyMetadata(index); + + class Slot { public ArrayBufferWriter? Buffer; } + static ThreadLocal t_slot = new ThreadLocal(); + + public string FormatMessage(in RedactedValues state) + { + Slot? tstate = t_slot.Value; + if (tstate == null) + { + tstate = new Slot(); + t_slot.Value = tstate; + } + ArrayBufferWriter arrayBuffer = tstate.Buffer ?? new ArrayBufferWriter(); + tstate.Buffer = null; + AppendFormattedMessage(state, arrayBuffer); + string ret = new string(arrayBuffer.WrittenSpan); + arrayBuffer.Clear(); + tstate.Buffer = arrayBuffer; + return ret; + } + + public Func, Exception?, string> GetStringMessageFormatter() => RedactedValues.Callback; + + class RedactedPropertyFormatterFactory : IPropertyFormatterFactory + { + const int MaxStackAllocChars = 256; + RedactedLogMetadata _metadata; + IPropertyFormatterFactory _wrappedFormatterFactory; + + public RedactedPropertyFormatterFactory(RedactedLogMetadata metadata, IPropertyFormatterFactory wrappedFormatterFactory) + { + _metadata = metadata; + _wrappedFormatterFactory = wrappedFormatterFactory; + } + + public FormatPropertyAction GetPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata) + { + PropertyRedaction? redaction = _metadata.GetRedactionForIndex(propertyIndex); + if (!redaction.HasValue) + { + return _wrappedFormatterFactory.GetPropertyFormatter(propertyIndex, _metadata.GetPropertyMetadata(propertyIndex)); + } + else + { + return GetRedactedPropertyFormatter(propertyIndex, metadata, redaction.Value.Redactor); + } + } + + public FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata) + { + PropertyRedaction? redaction = _metadata.GetRedactionForIndex(propertyIndex); + if (!redaction.HasValue) + { + return _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyMetadata(propertyIndex)); + } + else + { + return GetRedactedSpanPropertyFormatter(propertyIndex, metadata, redaction.Value.Redactor); + } + } + + private FormatSpanPropertyAction GetRedactedSpanPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata, IRedactor redactor) + { + FormatSpanPropertyAction wrappedFormatter = _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyMetadata(propertyIndex)); + return FormatRedactedProperty; + + void FormatRedactedProperty(scoped ReadOnlySpan value, ref BufferWriter writer) + { + int len = redactor.GetRedactedLength(value); + if (len <= MaxStackAllocChars) + { + Span redactedBuffer = stackalloc char[len]; + redactor.Redact(value, redactedBuffer); + wrappedFormatter(redactedBuffer, ref writer); + } + else + { + string redactedValue = redactor.Redact(value); + wrappedFormatter(redactedValue, ref writer); + } + } + } + + private FormatPropertyAction GetRedactedPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata, IRedactor redactor) + { + FormatSpanPropertyAction wrappedFormatter = _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyMetadata(propertyIndex)); + return FormatRedactedProperty; + + void FormatRedactedProperty(PropType value, ref BufferWriter writer) + { + if (value == null) + { + return; + } + else if (value is ISpanFormattable) + { + Span unredactedBuffer = stackalloc char[MaxStackAllocChars]; + if (((ISpanFormattable)value).TryFormat(unredactedBuffer, out int charsWritten2, null, null)) + { + unredactedBuffer = unredactedBuffer.Slice(0, charsWritten2); + int len2 = redactor!.GetRedactedLength(unredactedBuffer); + if (len2 <= MaxStackAllocChars) + { + Span redactedBuffer2 = stackalloc char[len2]; + redactor!.Redact(unredactedBuffer, redactedBuffer2); + wrappedFormatter(redactedBuffer2, ref writer); + } + else + { + string redactedValue = redactor.Redact(unredactedBuffer); + wrappedFormatter(redactedValue, ref writer); + } + return; + } + } + string? unredactedValue = value.ToString(); + int len = redactor.GetRedactedLength(unredactedValue); + if (len <= MaxStackAllocChars) + { + Span redactedBuffer = stackalloc char[len]; + redactor.Redact(unredactedValue, redactedBuffer); + wrappedFormatter(redactedBuffer, ref writer); + } + else + { + string redactedValue = redactor.Redact(unredactedValue); + wrappedFormatter(redactedValue, ref writer); + } + } + } + } + + public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + { + FormatPropertyListAction wrappedFormatPropertyList = _originalMetadata.GetPropertyListFormatter(new RedactedPropertyFormatterFactory(this, propertyFormatterFactory)); + return FormatPropertyList; + + void FormatPropertyList(in RedactedValues state, ref BufferWriter writer) + { + wrappedFormatPropertyList(in state.OriginalState, ref writer); + } + } + + private PropertyRedaction? GetRedactionForIndex(int propIndex) + { + foreach (PropertyRedaction redaction in _redactions) + { + if (redaction.Index == propIndex) + { + return redaction; + } + } + return null; + } + } + + internal struct RedactedValues + { + public RedactedValues(RedactedLogMetadata metadata, in T originalState) + { + Metadata = metadata; + OriginalState = originalState; + } + + public RedactedLogMetadata Metadata; + public T OriginalState; + + public override string ToString() => Metadata.FormatMessage(this); + + public static readonly Func, Exception?, string> Callback = (state, e) => state.ToString(); + } +} + +#endif diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs new file mode 100644 index 00000000000000..8b54278a67eabe --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET8_0_OR_GREATER + +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Test; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.Extensions.Logging.Tests.Redaction +{ + public class RedactionTests + { + [Fact] + public void RedactLoggerMessage() + { + var sink = new TestSink(); + var provider = new TestLoggerProvider(sink, isEnabled: true); + ILoggerFactory factory = LoggerFactory.Create(b => + { + b.AddProvider(provider); + b.Services.AddSingleton(); + b.AddRedactionProcessor(); + }); + + ILogger foo = factory.CreateLogger("Foo"); + LogMessages.LogName(foo, "Frank", 76); + LogMessages.LogNameRedacted(foo, "Frank", 76); + + Assert.Equal(2, sink.Writes.Count); + Assert.Equal("User Frank has now 76 status", sink.Writes.ElementAt(0).Message); + Assert.Equal("User [Redacted - EUPI] has now 76 status", sink.Writes.ElementAt(1).Message); + } + + private sealed class TestRedactorProvider : IRedactorProvider + { + public IRedactor GetRedactor(DataClass dataClass) => new TestRedactor(dataClass); + } + + private sealed class TestRedactor : IRedactor + { + private readonly string _redactedText; + + public TestRedactor(DataClass dataClass) + { + _redactedText = $"[Redacted - {dataClass}]"; + } + + public int GetRedactedLength(ReadOnlySpan source) => _redactedText.Length; + public string Redact(ReadOnlySpan source) => _redactedText; + public int Redact(ReadOnlySpan source, Span destination) + { + _redactedText.AsSpan().CopyTo(destination); + return _redactedText.Length; + } + } + } + + internal class EUPIAttribute : DataClassificationAttribute + { + public EUPIAttribute() : base(DataClass.EUPI) + { + } + } + + public static partial class LogMessages + { + //[LoggerMessage(1, LogLevel.Information, "User {username} has now {status} status")] + public static void LogName(this ILogger logger, string username, int status) + { + // manually writing the code the source generator is proposed to create + __LogNameCallback(logger, username, status, null); + } + + private static readonly Action __LogNameCallback = + LoggerMessage.Define( + LogLevel.Information, + new EventId(1, nameof(LogName)), + "User {username} has now {status} status"); + + + //[LoggerMessage(2, LogLevel.Information, "User {username} has now {status} status")] + public static void LogNameRedacted(this ILogger logger, [EUPI] string username, int status) + { + // manually writing the code the source generator is proposed to create + __LogNameRedactedCallback(logger, username, status, null); + } + + private static readonly Action __LogNameRedactedCallback = + LoggerMessage.Define( + LogLevel.Information, + new EventId(1, nameof(LogNameRedacted)), + "User {username} has now {status} status", + new LogDefineOptions() { ParameterAttributes = new Attribute[]?[] { new Attribute[] { new EUPIAttribute() }, null } }); + } +} + +#endif From 39ad281fedc170c0cf0d1acedcb6b6ab6291a78e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 29 May 2023 07:58:57 +0800 Subject: [PATCH 15/26] API clean up --- ...crosoft.Extensions.Logging.Abstractions.cs | 19 +- .../src/LogDefineOptions.cs | 5 +- .../src/LogEntryNew.cs | 16 +- .../src/LogValuesFormatter.cs | 23 +- .../src/LoggerMessage.cs | 381 +----------------- .../src/EnrichmentExtensions.cs | 6 +- .../Common/Redaction/RedactionProcessor.cs | 27 +- .../tests/Common/Redaction/RedactionTests.cs | 2 +- 8 files changed, 53 insertions(+), 426 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index 2de1bdea4c72e3..facda3d3ac92c0 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -86,8 +86,8 @@ public partial interface ILogMetadata int PropertyCount { get; } void AppendFormattedMessage(in TState state, System.Buffers.IBufferWriter buffer); System.Action> GetMessageFormatter(Microsoft.Extensions.Logging.PropertyCustomFormatter[] customFormatters); + Microsoft.Extensions.Logging.LogPropertyInfo GetPropertyInfo(int index); Microsoft.Extensions.Logging.FormatPropertyListAction GetPropertyListFormatter(Microsoft.Extensions.Logging.IPropertyFormatterFactory propertyFormatterFactory); - Microsoft.Extensions.Logging.LogPropertyMetadata GetPropertyMetadata(int index); System.Func GetStringMessageFormatter(); } public partial interface IProcessorFactory @@ -96,8 +96,8 @@ public partial interface IProcessorFactory } public partial interface IPropertyFormatterFactory { - Microsoft.Extensions.Logging.FormatPropertyAction GetPropertyFormatter(int propertyIndex, Microsoft.Extensions.Logging.LogPropertyMetadata metadata); - Microsoft.Extensions.Logging.FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, Microsoft.Extensions.Logging.LogPropertyMetadata metadata); + Microsoft.Extensions.Logging.FormatPropertyAction GetPropertyFormatter(int propertyIndex, Microsoft.Extensions.Logging.LogPropertyInfo metadata); + Microsoft.Extensions.Logging.FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, Microsoft.Extensions.Logging.LogPropertyInfo metadata); } public partial interface ISupportExternalScope { @@ -106,7 +106,7 @@ public partial interface ISupportExternalScope public partial class LogDefineOptions { public LogDefineOptions() { } - public System.Attribute[]?[]? ParameterAttributes { get { throw null; } set { } } + public object[]?[]? ParameterMetadata { get { throw null; } set { } } public bool SkipEnabledCheck { get { throw null; } set { } } } public abstract partial class LogEntryHandler @@ -180,7 +180,6 @@ public static partial class LoggerMessage public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } - public static Microsoft.Extensions.Logging.LoggerMessage.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options = null) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString) { throw null; } @@ -189,7 +188,6 @@ public static partial class LoggerMessage public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString) { throw null; } public static System.Action Define(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, string formatString, Microsoft.Extensions.Logging.LogDefineOptions? options) { throw null; } - public delegate void Action(T0 v0, T1 v1, T2 v2, T3 v3, T4 v4, T5 v5, T6 v6, T7 v7, T8 v8, T9 v9, T10 v10, T11 v11, T12 v12, T13 v13, T14 v14, T15 v15, T16 v16, T17 v17, T18 v18, T19 v19, T20 v20, T21 v21); public delegate void Log(Microsoft.Extensions.Logging.ILogger logger, ref TState state, System.Exception? exception); } [System.AttributeUsageAttribute(System.AttributeTargets.Method)] @@ -210,7 +208,7 @@ public Logger(Microsoft.Extensions.Logging.ILoggerFactory factory) { } Microsoft.Extensions.Logging.ScopePipeline Microsoft.Extensions.Logging.ILogEntryPipelineFactory.GetScopePipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState) { throw null; } System.IDisposable Microsoft.Extensions.Logging.ILogger.BeginScope(TState state) { throw null; } bool Microsoft.Extensions.Logging.ILogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) { throw null; } - void Microsoft.Extensions.Logging.ILogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func formatter) { } + void Microsoft.Extensions.Logging.ILogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func formatter) { } } public enum LogLevel { @@ -222,13 +220,12 @@ public enum LogLevel Critical = 5, None = 6, } - public partial struct LogPropertyMetadata + public partial struct LogPropertyInfo { private object _dummy; private int _dummyPrimitive; - public LogPropertyMetadata(string name, string? formatSpecifier, System.Attribute[]? attributes) { throw null; } - public readonly System.Attribute[]? Attributes { get { throw null; } } - public readonly string? FormatSpecifier { get { throw null; } } + public LogPropertyInfo(string name, object[]? metadata) { throw null; } + public readonly object[]? Metadata { get { throw null; } } public readonly string Name { get { throw null; } } } public partial class Pipeline diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogDefineOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogDefineOptions.cs index 6537051cbf8b85..b4b99ef57ccfa2 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogDefineOptions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogDefineOptions.cs @@ -15,6 +15,9 @@ public class LogDefineOptions /// public bool SkipEnabledCheck { get; set; } - public Attribute[]?[]? ParameterAttributes { get; set; } + /// + /// Gets or sets a collection of parameter metadata. Items in the collection match parameters to the log message by position. + /// + public object[]?[]? ParameterMetadata { get; set; } } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index 079e52c842f11c..76cced40522055 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -69,17 +69,15 @@ public interface ILoggerStateWithMetadata public ILogMetadata Metadata { get; } } - public struct LogPropertyMetadata + public struct LogPropertyInfo { - public LogPropertyMetadata(string name, string? formatSpecifier, Attribute[]? attributes) + public LogPropertyInfo(string name, object[]? metadata) { Name = name; - FormatSpecifier = formatSpecifier; - Attributes = attributes; + Metadata = metadata; } public string Name { get; } - public string? FormatSpecifier { get; } - public Attribute[]? Attributes { get; } + public object[]? Metadata { get; } } public interface ILogMetadata @@ -88,7 +86,7 @@ public interface ILogMetadata EventId EventId { get; } string OriginalFormat { get; } int PropertyCount { get; } - LogPropertyMetadata GetPropertyMetadata(int index); + LogPropertyInfo GetPropertyInfo(int index); void AppendFormattedMessage(in TState state, IBufferWriter buffer); Action> GetMessageFormatter(PropertyCustomFormatter[] customFormatters); FormatPropertyListAction GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory); @@ -101,8 +99,8 @@ public interface ILogMetadata public interface IPropertyFormatterFactory { - FormatPropertyAction GetPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata); - FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata); + FormatPropertyAction GetPropertyFormatter(int propertyIndex, LogPropertyInfo metadata); + FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, LogPropertyInfo metadata); } public abstract class PropertyCustomFormatter diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs index e771ceedd98bc8..4120eebd5cded9 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.Logging { internal class LogValuesMetadata : LogValuesFormatter { - public LogValuesMetadata(string format, LogLevel level, EventId eventId, Attribute[]?[]? attributes) : base(format, attributes) + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata) : base(format, metadata) { LogLevel = level; EventId = eventId; @@ -32,21 +32,21 @@ internal class LogValuesFormatter { private const string NullValue = "(null)"; private static readonly char[] FormatDelimiters = { ',', ':' }; - private readonly LogPropertyMetadata[]? _metadata; + private readonly LogPropertyInfo[]? _metadata; private readonly InternalCompositeFormat _format; // NOTE: If this assembly ever builds for netcoreapp, the below code should change to: // - Be annotated as [SkipLocalsInit] to avoid zero'ing the stackalloc'd char span // - Format _valueNames.Count directly into a span - public LogValuesFormatter(string format, Attribute[]?[]? attributes = null) + public LogValuesFormatter(string format, object[]?[]? metadata = null) { ThrowHelper.ThrowIfNull(format); OriginalFormat = format; var vsb = new ValueStringBuilder(stackalloc char[256]); - List metadata = new List(); + List propertyMetadata = new List(); int scanIndex = 0; int endIndex = format.Length; @@ -74,21 +74,16 @@ public LogValuesFormatter(string format, Attribute[]?[]? attributes = null) int colonIndex = format.IndexOf(':', openBraceIndex, closeBraceIndex - openBraceIndex); vsb.Append(format.AsSpan(scanIndex, openBraceIndex - scanIndex + 1)); - vsb.Append(metadata.Count.ToString()); + vsb.Append(propertyMetadata.Count.ToString()); string propName = format.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1); vsb.Append(format.AsSpan(formatDelimiterIndex, closeBraceIndex - formatDelimiterIndex + 1)); - string? propFormat = null; - if (colonIndex != -1) - { - propFormat = format.Substring(colonIndex + 1, closeBraceIndex - colonIndex - 1); - } - Attribute[]? propAttributes = attributes != null && attributes.Length >= metadata.Count ? attributes[metadata.Count] : null; - metadata.Add(new LogPropertyMetadata(propName, propFormat, propAttributes)); + object[]? propMetadata = metadata != null && metadata.Length >= propertyMetadata.Count ? metadata[propertyMetadata.Count] : null; + propertyMetadata.Add(new LogPropertyInfo(propName, propMetadata)); scanIndex = closeBraceIndex + 1; } } - _metadata = metadata.ToArray(); + _metadata = propertyMetadata.ToArray(); _format = InternalCompositeFormat.Parse(vsb.ToString()); } @@ -97,7 +92,7 @@ public LogValuesFormatter(string format, Attribute[]?[]? attributes = null) public int PropertyCount => _metadata != null ? _metadata.Length : 0; public string GetValueName(int index) => _metadata![index].Name; - public LogPropertyMetadata GetPropertyMetadata(int index) => _metadata![index]; + public LogPropertyInfo GetPropertyInfo(int index) => _metadata![index]; private static int FindBraceIndex(string format, char brace, int startIndex, int endIndex) { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs index 0bc2f3beeb4c85..b18ea26d1fe4a4 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs @@ -283,7 +283,7 @@ void LogSlowPath(ILogger logger, ref TState state, Exception? exception) /// A delegate which when invoked creates a log message. public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options) { - LogValuesMetadata metadata = LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterAttributes); + LogValuesMetadata metadata = LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterMetadata); LogEntryPipeline>? pipeline = null; bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); return Log; @@ -333,102 +333,6 @@ void LogSlowPath(ILogger logger, T1 arg1, T2 arg2, Exception? exception) } } - public delegate void Action(T0 v0, T1 v1, T2 v2, T3 v3, T4 v4, T5 v5, T6 v6, T7 v7, T8 v8, T9 v9, T10 v10, T11 v11, T12 v12, T13 v13, T14 v14, T15 v15, T16 v16, T17 v17, T18 v18, T19 v19, T20 v20, T21 v21); - - public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options = null) - { - LogValuesMetadata metadata = - LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterAttributes); - LogEntryPipeline>? pipeline = null; - bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); - return Log; - - void Log(ILogger logger, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16, T17 arg17, T18 arg18, T19 arg19, Exception? exception) - { - LogEntryPipeline>? pipelineSnapshot = pipeline; - if (pipelineSnapshot != null && pipelineSnapshot.UserState == logger && pipelineSnapshot.IsUpToDate) - { - if (!pipelineSnapshot.IsEnabled || - (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) - return; - LogValues state = new LogValues(metadata); - state._value0 = arg0; - state._value1 = arg1; - state._value2 = arg2; - state._value3 = arg3; - state._value4 = arg4; - state._value5 = arg5; - state._value6 = arg6; - state._value7 = arg7; - state._value8 = arg8; - state._value9 = arg9; - state._value10 = arg10; - state._value11 = arg11; - state._value12 = arg12; - state._value13 = arg13; - state._value14 = arg14; - state._value15 = arg15; - state._value16 = arg16; - state._value17 = arg17; - state._value18 = arg18; - state._value19 = arg19; - LogEntry> entry = - new LogEntry>(logLevel, category: null!, eventId, state, exception, null!); - pipelineSnapshot.HandleLogEntry(ref entry); - } - else - { - LogSlowPath(logger, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, exception); - } - } - - void LogSlowPath(ILogger logger, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16, T17 arg17, T18 arg18, T19 arg19, Exception? exception) - { - LogEntryPipeline>? pipelineSnapshot = null; - LogValues state = new LogValues(metadata); - state._value0 = arg0; - state._value1 = arg1; - state._value2 = arg2; - state._value3 = arg3; - state._value4 = arg4; - state._value5 = arg5; - state._value6 = arg6; - state._value7 = arg7; - state._value8 = arg8; - state._value9 = arg9; - state._value10 = arg10; - state._value11 = arg11; - state._value12 = arg12; - state._value13 = arg13; - state._value14 = arg14; - state._value15 = arg15; - state._value16 = arg16; - state._value17 = arg17; - state._value18 = arg18; - state._value19 = arg19; - LogEntry> entry = - new LogEntry>(logLevel, category: null!, eventId, state, exception, null!); - if (logger is ILogEntryPipelineFactory) - { - pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetLoggingPipeline(metadata, logger); - pipeline = pipelineSnapshot; - } - if (pipelineSnapshot != null) - { - if (!pipelineSnapshot.IsEnabled || - (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) - return; - pipelineSnapshot.HandleLogEntry(ref entry); - } - else - { - if (needFullEnabledCheck && logger.IsEnabled(logLevel)) - return; - logger.Log(entry.LogLevel, entry.EventId, entry.State, entry.Exception, LogValues.Callback); - } - } - } - /// /// Creates a delegate which can be invoked for logging a message. /// @@ -735,7 +639,7 @@ IEnumerator IEnumerable.GetEnumerator() internal class LogValuesMetadata : LogValuesMetadata, ILogMetadata> { - public LogValuesMetadata(string format, LogLevel level, EventId eventId, Attribute[]?[]? attributes = null) : base(format, level, eventId, attributes) { } + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) { } public void AppendFormattedMessage(in LogValues state, IBufferWriter buffer) { @@ -761,8 +665,8 @@ public void AppendFormattedMessage(in LogValues state, IBufferWriter> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) { - FormatPropertyAction formatter0 = propertyFormatterFactory.GetPropertyFormatter(0, GetPropertyMetadata(0)); - FormatPropertyAction formatter1 = propertyFormatterFactory.GetPropertyFormatter(1, GetPropertyMetadata(1)); + FormatPropertyAction formatter0 = propertyFormatterFactory.GetPropertyFormatter(0, GetPropertyInfo(0)); + FormatPropertyAction formatter1 = propertyFormatterFactory.GetPropertyFormatter(1, GetPropertyInfo(1)); return FormatPropertyList; void FormatPropertyList(in LogValues tstate, ref BufferWriter writer) @@ -772,7 +676,6 @@ void FormatPropertyList(in LogValues tstate, ref BufferWriter writ } } - public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customPropertyFormatters) => (state, buffer) => AppendFormattedMessage(state, buffer, customPropertyFormatters); @@ -823,8 +726,6 @@ private static void AppendCustomFormattedProperty(int index, T value, ref Buf } public Func, Exception?, string> GetStringMessageFormatter() => LogValues.Callback; - - } internal readonly struct LogValues : IReadOnlyList> @@ -879,284 +780,14 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } - public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, Attribute[]?[]? parameterAttributes = null) + public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, object[]?[]? parameterMetadata = null) { - var metadata = new LogValuesMetadata(formatString, level, eventId, parameterAttributes); + var metadata = new LogValuesMetadata(formatString, level, eventId, parameterMetadata); ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 2, metadata.PropertyCount); return metadata; } } - internal class LogValuesMetadata : - LogValuesMetadata, ILogMetadata> - { - public LogValuesMetadata(string format, LogLevel level, EventId eventId, Attribute[]?[]? attributes = null) : base(format, level, eventId, attributes) { } - - public void AppendFormattedMessage(in LogValues state, IBufferWriter buffer) - { - BufferWriter writer = new BufferWriter(buffer); - foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in CompositeFormat._segments) - { - int index = segment.ArgIndex; - switch (index) - { - case 0: - AppendFormattedPropertyValue(state._value0, ref writer, segment.Alignment, segment.Format); - break; - case 1: - AppendFormattedPropertyValue(state._value1, ref writer, segment.Alignment, segment.Format); - break; - default: - writer.Write(segment.Literal.AsSpan()); - break; - } - } - writer.Flush(); - } - - public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customPropertyFormatters) => - (state, buffer) => AppendFormattedMessage(state, buffer, customPropertyFormatters); - - private void AppendFormattedMessage(in LogValues state, IBufferWriter buffer, PropertyCustomFormatter[] customFormatters) - { - BufferWriter writer = new BufferWriter(buffer); - foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in CompositeFormat._segments) - { - int index = segment.ArgIndex; - switch (index) - { - case 0: - AppendCustomFormattedProperty(index, state._value0, ref writer, segment.Alignment, segment.Format, customFormatters[index]); - break; - case 1: - AppendCustomFormattedProperty(index, state._value1, ref writer, segment.Alignment, segment.Format, customFormatters[index]); - break; - default: - writer.Write(segment.Literal.AsSpan()); - break; - } - } - writer.Flush(); - } - - public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) - { - FormatPropertyAction formatter0 = propertyFormatterFactory.GetPropertyFormatter(0, GetPropertyMetadata(0)); - FormatPropertyAction formatter1 = propertyFormatterFactory.GetPropertyFormatter(1, GetPropertyMetadata(1)); - FormatPropertyAction formatter2 = propertyFormatterFactory.GetPropertyFormatter(2, GetPropertyMetadata(2)); - FormatPropertyAction formatter3 = propertyFormatterFactory.GetPropertyFormatter(3, GetPropertyMetadata(3)); - FormatPropertyAction formatter4 = propertyFormatterFactory.GetPropertyFormatter(4, GetPropertyMetadata(4)); - FormatPropertyAction formatter5 = propertyFormatterFactory.GetPropertyFormatter(5, GetPropertyMetadata(5)); - FormatPropertyAction formatter6 = propertyFormatterFactory.GetPropertyFormatter(6, GetPropertyMetadata(6)); - FormatPropertyAction formatter7 = propertyFormatterFactory.GetPropertyFormatter(7, GetPropertyMetadata(7)); - FormatPropertyAction formatter8 = propertyFormatterFactory.GetPropertyFormatter(8, GetPropertyMetadata(8)); - FormatPropertyAction formatter9 = propertyFormatterFactory.GetPropertyFormatter(9, GetPropertyMetadata(9)); - FormatPropertyAction formatter10 = propertyFormatterFactory.GetPropertyFormatter(10, GetPropertyMetadata(10)); - FormatPropertyAction formatter11 = propertyFormatterFactory.GetPropertyFormatter(11, GetPropertyMetadata(11)); - FormatPropertyAction formatter12 = propertyFormatterFactory.GetPropertyFormatter(12, GetPropertyMetadata(12)); - FormatPropertyAction formatter13 = propertyFormatterFactory.GetPropertyFormatter(13, GetPropertyMetadata(13)); - FormatPropertyAction formatter14 = propertyFormatterFactory.GetPropertyFormatter(14, GetPropertyMetadata(14)); - FormatPropertyAction formatter15 = propertyFormatterFactory.GetPropertyFormatter(15, GetPropertyMetadata(15)); - FormatPropertyAction formatter16 = propertyFormatterFactory.GetPropertyFormatter(16, GetPropertyMetadata(16)); - FormatPropertyAction formatter17 = propertyFormatterFactory.GetPropertyFormatter(17, GetPropertyMetadata(17)); - FormatPropertyAction formatter18 = propertyFormatterFactory.GetPropertyFormatter(18, GetPropertyMetadata(18)); - FormatPropertyAction formatter19 = propertyFormatterFactory.GetPropertyFormatter(19, GetPropertyMetadata(19)); - return FormatPropertyList; - - void FormatPropertyList(in LogValues tstate, ref BufferWriter writer) - { - formatter0(tstate._value0, ref writer); - formatter1(tstate._value1, ref writer); - formatter2(tstate._value2, ref writer); - formatter3(tstate._value3, ref writer); - formatter4(tstate._value4, ref writer); - formatter5(tstate._value5, ref writer); - formatter6(tstate._value6, ref writer); - formatter7(tstate._value7, ref writer); - formatter8(tstate._value8, ref writer); - formatter9(tstate._value9, ref writer); - formatter10(tstate._value10, ref writer); - formatter11(tstate._value11, ref writer); - formatter12(tstate._value12, ref writer); - formatter13(tstate._value13, ref writer); - formatter14(tstate._value14, ref writer); - formatter15(tstate._value15, ref writer); - formatter16(tstate._value16, ref writer); - formatter17(tstate._value17, ref writer); - formatter18(tstate._value18, ref writer); - formatter19(tstate._value19, ref writer); - } - } - - private static void AppendCustomFormattedProperty(int index, T value, ref BufferWriter writer, int alignment, string? format, PropertyCustomFormatter? formatter) - { - if (formatter == null) - { - AppendFormattedPropertyValue(value, ref writer, alignment, format); - } - else - { - writer.Flush(); - if (value is string strVal) - { - formatter.AppendFormatted(index, strVal, writer.Writer); - } - else if (value is int intVal) - { - formatter.AppendFormatted(index, intVal, writer.Writer); - } - else - { - formatter.AppendFormatted(index, value, writer.Writer); - } - } - } - - public Func, Exception?, string> GetStringMessageFormatter() => - LogValues.Callback; - } - - internal struct LogValues : IReadOnlyList> - { - public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); - - private readonly LogValuesFormatter _formatter; - internal T0 _value0; - internal T1 _value1; - internal T2 _value2; - internal T3 _value3; - internal T4 _value4; - internal T5 _value5; - internal T6 _value6; - internal T7 _value7; - internal T8 _value8; - internal T9 _value9; - internal T10 _value10; - internal T11 _value11; - internal T12 _value12; - internal T13 _value13; - internal T14 _value14; - internal T15 _value15; - internal T16 _value16; - internal T17 _value17; - internal T18 _value18; - internal T19 _value19; - -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public LogValues(LogValuesFormatter formatter) -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - { - _formatter = formatter; - } - - public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8, T9 value9, T10 value10, - T11 value11, T12 value12, T13 value13, T14 value14, T15 value15, T16 value16, T17 value17, T18 value18, T19 value19) - { - _formatter = formatter; - _value0 = value0; - _value1 = value1; - _value2 = value2; - _value3 = value3; - _value4 = value4; - _value5 = value5; - _value6 = value6; - _value7 = value7; - _value8 = value8; - _value9 = value9; - _value10 = value10; - _value11 = value11; - _value12 = value12; - _value13 = value13; - _value14 = value14; - _value15 = value15; - _value16 = value16; - _value17 = value17; - _value18 = value18; - _value19 = value19; - } - - public ILogMetadata>? Metadata => - _formatter as LogValuesMetadata; - - public KeyValuePair this[int index] - { - get - { - switch (index) - { - case 0: - return new KeyValuePair(_formatter.GetValueName(0), _value0); - case 1: - return new KeyValuePair(_formatter.GetValueName(1), _value1); - case 2: - return new KeyValuePair(_formatter.GetValueName(2), _value2); - case 3: - return new KeyValuePair(_formatter.GetValueName(3), _value3); - case 4: - return new KeyValuePair(_formatter.GetValueName(4), _value4); - case 5: - return new KeyValuePair(_formatter.GetValueName(5), _value5); - case 6: - return new KeyValuePair(_formatter.GetValueName(6), _value6); - case 7: - return new KeyValuePair(_formatter.GetValueName(7), _value7); - case 8: - return new KeyValuePair(_formatter.GetValueName(8), _value8); - case 9: - return new KeyValuePair(_formatter.GetValueName(9), _value9); - case 10: - return new KeyValuePair(_formatter.GetValueName(10), _value10); - case 11: - return new KeyValuePair(_formatter.GetValueName(11), _value11); - case 12: - return new KeyValuePair(_formatter.GetValueName(12), _value12); - case 13: - return new KeyValuePair(_formatter.GetValueName(13), _value13); - case 14: - return new KeyValuePair(_formatter.GetValueName(14), _value14); - case 15: - return new KeyValuePair(_formatter.GetValueName(15), _value15); - case 16: - return new KeyValuePair(_formatter.GetValueName(16), _value16); - case 17: - return new KeyValuePair(_formatter.GetValueName(17), _value17); - case 18: - return new KeyValuePair(_formatter.GetValueName(18), _value18); - case 19: - return new KeyValuePair(_formatter.GetValueName(19), _value19); - case 20: - return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); - default: - throw new IndexOutOfRangeException(nameof(index)); - } - } - } - - public int Count => 21; - - public IEnumerator> GetEnumerator() - { - for (int i = 0; i < Count; ++i) - { - yield return this[i]; - } - } - - public override string ToString() => throw new NotImplementedException(); - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, Attribute[]?[]? parameterAttributes = null) - { - var metadata = new LogValuesMetadata(formatString, level, eventId, parameterAttributes); - ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 20, metadata.PropertyCount); - return metadata; - } - } - private readonly struct LogValues : IReadOnlyList> { public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); diff --git a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs index 7b0f468227fb19..0b8577787c52df 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs @@ -159,7 +159,7 @@ public FormatPropertyListAction> GetPropert FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); return new FormatPropertyListAction>((in EnrichmentPropertyValues s, ref BufferWriter w) => formatter(in s.NestedProperties, ref w)); } - public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); + public LogPropertyInfo GetPropertyInfo(int index) => _innerMetadata.GetPropertyInfo(index); public Func, Exception?, string> GetStringMessageFormatter() { var formatter = _innerMetadata.GetStringMessageFormatter(); @@ -248,7 +248,7 @@ public FormatPropertyListAction> GetPro FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); return new FormatPropertyListAction>((in EnrichmentPropertyValues s, ref BufferWriter w) => formatter(in s.NestedProperties, ref w)); } - public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); + public LogPropertyInfo GetPropertyInfo(int index) => _innerMetadata.GetPropertyInfo(index); public Func, Exception?, string> GetStringMessageFormatter() { var formatter = _innerMetadata.GetStringMessageFormatter(); @@ -346,7 +346,7 @@ public FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); return new FormatPropertyListAction>((in UnboundedEnrichmentPropertyValues s, ref BufferWriter w) => formatter(in s.NestedProperties, ref w)); } - public LogPropertyMetadata GetPropertyMetadata(int index) => _innerMetadata.GetPropertyMetadata(index); + public LogPropertyInfo GetPropertyInfo(int index) => _innerMetadata.GetPropertyInfo(index); public Func, Exception?, string> GetStringMessageFormatter() { var formatter = _innerMetadata.GetStringMessageFormatter(); diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs index 7c5b409e54ce8f..c3a11c7a21b846 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// Prototype redaction processor uses APIs not available on .NET Framework. +// The prototype redaction could probably be updated to support .NET Framework if required. + #if NET8_0_OR_GREATER using System; @@ -83,12 +86,12 @@ PropertyRedaction[] GetPolicyRedactions(ILogMetadata metadata) List redactions = new List(); for (int i = 0; i < metadata.PropertyCount; i++) { - LogPropertyMetadata propMetadata = metadata.GetPropertyMetadata(i); - if (propMetadata.Attributes == null) + LogPropertyInfo propMetadata = metadata.GetPropertyInfo(i); + if (propMetadata.Metadata == null) { continue; } - DataClassificationAttribute? dataClassAttr = propMetadata.Attributes.OfType().FirstOrDefault(); + DataClassificationAttribute? dataClassAttr = propMetadata.Metadata.OfType().FirstOrDefault(); if (dataClassAttr != null) { redactions.Add(new PropertyRedaction(i, _redactorProvider!.GetRedactor(dataClassAttr.DataClass))); @@ -317,7 +320,7 @@ public Action, IBufferWriter> GetMessageFormatter(Proper return (state, buffer) => innerFormatter(state.OriginalState, buffer); } - public LogPropertyMetadata GetPropertyMetadata(int index) => _originalMetadata.GetPropertyMetadata(index); + public LogPropertyInfo GetPropertyInfo(int index) => _originalMetadata.GetPropertyInfo(index); class Slot { public ArrayBufferWriter? Buffer; } static ThreadLocal t_slot = new ThreadLocal(); @@ -353,12 +356,12 @@ public RedactedPropertyFormatterFactory(RedactedLogMetadata metadata, IProper _wrappedFormatterFactory = wrappedFormatterFactory; } - public FormatPropertyAction GetPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata) + public FormatPropertyAction GetPropertyFormatter(int propertyIndex, LogPropertyInfo metadata) { PropertyRedaction? redaction = _metadata.GetRedactionForIndex(propertyIndex); if (!redaction.HasValue) { - return _wrappedFormatterFactory.GetPropertyFormatter(propertyIndex, _metadata.GetPropertyMetadata(propertyIndex)); + return _wrappedFormatterFactory.GetPropertyFormatter(propertyIndex, _metadata.GetPropertyInfo(propertyIndex)); } else { @@ -366,12 +369,12 @@ public FormatPropertyAction GetPropertyFormatter(int propert } } - public FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata) + public FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, LogPropertyInfo metadata) { PropertyRedaction? redaction = _metadata.GetRedactionForIndex(propertyIndex); if (!redaction.HasValue) { - return _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyMetadata(propertyIndex)); + return _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyInfo(propertyIndex)); } else { @@ -379,9 +382,9 @@ public FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, LogP } } - private FormatSpanPropertyAction GetRedactedSpanPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata, IRedactor redactor) + private FormatSpanPropertyAction GetRedactedSpanPropertyFormatter(int propertyIndex, LogPropertyInfo metadata, IRedactor redactor) { - FormatSpanPropertyAction wrappedFormatter = _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyMetadata(propertyIndex)); + FormatSpanPropertyAction wrappedFormatter = _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyInfo(propertyIndex)); return FormatRedactedProperty; void FormatRedactedProperty(scoped ReadOnlySpan value, ref BufferWriter writer) @@ -401,9 +404,9 @@ void FormatRedactedProperty(scoped ReadOnlySpan value, ref BufferWriter GetRedactedPropertyFormatter(int propertyIndex, LogPropertyMetadata metadata, IRedactor redactor) + private FormatPropertyAction GetRedactedPropertyFormatter(int propertyIndex, LogPropertyInfo metadata, IRedactor redactor) { - FormatSpanPropertyAction wrappedFormatter = _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyMetadata(propertyIndex)); + FormatSpanPropertyAction wrappedFormatter = _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyInfo(propertyIndex)); return FormatRedactedProperty; void FormatRedactedProperty(PropType value, ref BufferWriter writer) diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs index 8b54278a67eabe..c5cd2a739a7900 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs @@ -94,7 +94,7 @@ public static void LogNameRedacted(this ILogger logger, [EUPI] string username, LogLevel.Information, new EventId(1, nameof(LogNameRedacted)), "User {username} has now {status} status", - new LogDefineOptions() { ParameterAttributes = new Attribute[]?[] { new Attribute[] { new EUPIAttribute() }, null } }); + new LogDefineOptions() { ParameterMetadata = new Attribute[]?[] { new Attribute[] { new EUPIAttribute() }, null } }); } } From 4671d54ca06ba29d3ec9f5d49b976b2da6dd9632 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 29 May 2023 09:05:23 +0800 Subject: [PATCH 16/26] Clean up --- .../src/LogEntryPipeline.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs index 814f07be8ac50b..c8efd208a2c9c7 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs @@ -28,22 +28,6 @@ public Pipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequi public bool IsUpToDate { get; set; } } - //public class ScopePipeline - //{ - // public ScopePipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) - // { - // UserState = userState; - // IsEnabled = isEnabled; - // IsDynamicLevelCheckRequired = isDynamicLevelCheckRequired; - // IsUpToDate = true; - // } - - // public object? UserState { get; } - // public bool IsEnabled { get; } - // public bool IsDynamicLevelCheckRequired { get; } - // public bool IsUpToDate { get; set; } - //} - public class LogEntryPipeline : Pipeline { public LogEntryPipeline(LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : From 2e6bf33ad51754223a373cf99cc92982a660ed57 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 29 May 2023 18:32:33 +0800 Subject: [PATCH 17/26] Get values from redacted state --- .../Common/Redaction/RedactionProcessor.cs | 52 +++++++++++++++++-- .../tests/Common/Redaction/RedactionTests.cs | 41 ++++++++++++++- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs index c3a11c7a21b846..af8e0f811ae875 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs @@ -8,6 +8,7 @@ using System; using System.Buffers; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -270,15 +271,28 @@ public override void AppendFormatted(int index, T value, IBufferWriter internal class RedactedLogMetadata : ILogMetadata> { - ILogMetadata _originalMetadata; - PropertyRedaction[] _redactions; - Action>? _defaultFormatter; + private readonly ILogMetadata _originalMetadata; + private readonly PropertyRedaction[] _redactions; + private Action>? _defaultFormatter; + public RedactedLogMetadata(ILogMetadata metadata, PropertyRedaction[] redactions) { _originalMetadata = metadata; _redactions = redactions; + } + public IRedactor? GetPropertyRedactor(int index) + { + for (var i = 0; i < _redactions.Length; i++) + { + if (_redactions[i].Index == index) + { + return _redactions[i].Redactor; + } + } + return null; } + public ILogMetadata OriginalMetadata => _originalMetadata; public LogLevel LogLevel => _originalMetadata.LogLevel; @@ -477,7 +491,7 @@ void FormatPropertyList(in RedactedValues state, ref BufferWriter write } } - internal struct RedactedValues + internal struct RedactedValues : IReadOnlyList> { public RedactedValues(RedactedLogMetadata metadata, in T originalState) { @@ -488,8 +502,38 @@ public RedactedValues(RedactedLogMetadata metadata, in T originalState) public RedactedLogMetadata Metadata; public T OriginalState; + private IReadOnlyList> _originalStateValues; + public IReadOnlyList> OriginalStateValues => _originalStateValues ??= OriginalState as IReadOnlyList>; + public override string ToString() => Metadata.FormatMessage(this); + public IEnumerator> GetEnumerator() + { + for (var i = 0; i < Count; i++) + { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => OriginalStateValues != null ? OriginalStateValues.Count : 0; + + public KeyValuePair this[int index] + { + get + { + var originalValue = OriginalStateValues[index]; + var redactor = Metadata.GetPropertyRedactor(index); + if (redactor == null) + { + return originalValue; + } + string? unredactedValue = originalValue.Value?.ToString(); + return new KeyValuePair(originalValue.Key, redactor.Redact(unredactedValue)); + } + } + public static readonly Func, Exception?, string> Callback = (state, e) => state.ToString(); } } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs index c5cd2a739a7900..6700cdf4cb1049 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs @@ -4,6 +4,7 @@ #if NET8_0_OR_GREATER using System; +using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Test; @@ -15,7 +16,7 @@ namespace Microsoft.Extensions.Logging.Tests.Redaction public class RedactionTests { [Fact] - public void RedactLoggerMessage() + public void Redact_Message() { var sink = new TestSink(); var provider = new TestLoggerProvider(sink, isEnabled: true); @@ -35,6 +36,44 @@ public void RedactLoggerMessage() Assert.Equal("User [Redacted - EUPI] has now 76 status", sink.Writes.ElementAt(1).Message); } + [Fact] + public void Redact_StateValues() + { + var sink = new TestSink(); + var provider = new TestLoggerProvider(sink, isEnabled: true); + ILoggerFactory factory = LoggerFactory.Create(b => + { + b.AddProvider(provider); + b.Services.AddSingleton(); + b.AddRedactionProcessor(); + }); + + ILogger foo = factory.CreateLogger("Foo"); + LogMessages.LogNameRedacted(foo, "Frank", 76); + + Assert.Equal(1, sink.Writes.Count); + var write = sink.Writes.ElementAt(0); + Assert.Equal("User [Redacted - EUPI] has now 76 status", write.Message); + + var values = Assert.IsAssignableFrom>>(write.State); + Assert.Collection(values, + kvp => + { + Assert.Equal("username", kvp.Key); + Assert.Equal("[Redacted - EUPI]", kvp.Value); + }, + kvp => + { + Assert.Equal("status", kvp.Key); + Assert.Equal(76, kvp.Value); + }, + kvp => + { + Assert.Equal("{OriginalFormat}", kvp.Key); + Assert.Equal("User {username} has now {status} status", kvp.Value); + }); + } + private sealed class TestRedactorProvider : IRedactorProvider { public IRedactor GetRedactor(DataClass dataClass) => new TestRedactor(dataClass); From 9fcaa89819756c9f00c3b5e061b51448c9714813 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 30 May 2023 12:13:07 +0800 Subject: [PATCH 18/26] Clean up redaction properties --- .../Common/Redaction/RedactionProcessor.cs | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs index af8e0f811ae875..cbb08a8eb69417 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs @@ -491,7 +491,7 @@ void FormatPropertyList(in RedactedValues state, ref BufferWriter write } } - internal struct RedactedValues : IReadOnlyList> + internal readonly struct RedactedValues : IReadOnlyList> { public RedactedValues(RedactedLogMetadata metadata, in T originalState) { @@ -499,39 +499,58 @@ public RedactedValues(RedactedLogMetadata metadata, in T originalState) OriginalState = originalState; } - public RedactedLogMetadata Metadata; - public T OriginalState; - - private IReadOnlyList> _originalStateValues; - public IReadOnlyList> OriginalStateValues => _originalStateValues ??= OriginalState as IReadOnlyList>; + public readonly RedactedLogMetadata Metadata; + public readonly T OriginalState; public override string ToString() => Metadata.FormatMessage(this); public IEnumerator> GetEnumerator() { - for (var i = 0; i < Count; i++) + var nested = OriginalState as IReadOnlyList>; + if (nested == null) + { + yield break; + } + for (var i = 0; i < nested.Count; i++) { - yield return this[i]; + yield return GetRedactedValue(i, nested[i]); } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public int Count => OriginalStateValues != null ? OriginalStateValues.Count : 0; + public int Count + { + get + { + var nested = OriginalState as IReadOnlyList>; + return nested?.Count ?? 0; + } + } public KeyValuePair this[int index] { get { - var originalValue = OriginalStateValues[index]; - var redactor = Metadata.GetPropertyRedactor(index); - if (redactor == null) + var nested = OriginalState as IReadOnlyList>; + if (nested == null) { - return originalValue; + throw new IndexOutOfRangeException(nameof(index)); } - string? unredactedValue = originalValue.Value?.ToString(); - return new KeyValuePair(originalValue.Key, redactor.Redact(unredactedValue)); + var originalValue = nested[index]; + return GetRedactedValue(index, originalValue); + } + } + + private readonly KeyValuePair GetRedactedValue(int index, KeyValuePair originalValue) + { + var redactor = Metadata.GetPropertyRedactor(index); + if (redactor == null) + { + return originalValue; } + string? unredactedValue = originalValue.Value?.ToString(); + return new KeyValuePair(originalValue.Key, redactor.Redact(unredactedValue)); } public static readonly Func, Exception?, string> Callback = (state, e) => state.ToString(); From 4a6dab59bae3f8b3a4481a5a18a08d2883e3bbc4 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 30 May 2023 12:35:42 +0800 Subject: [PATCH 19/26] Remove dynamic check on scope pipeline --- .../ref/Microsoft.Extensions.Logging.Abstractions.cs | 5 ++--- .../src/LogEntryNew.cs | 2 +- .../src/LogEntryPipeline.cs | 5 ++--- .../Microsoft.Extensions.Logging/src/DispatchProcessor.cs | 3 +-- .../src/EnrichmentExtensions.cs | 4 ++-- src/libraries/Microsoft.Extensions.Logging/src/Logger.cs | 6 +++--- .../tests/Common/EnrichmentTests.cs | 4 ++-- .../tests/Common/ProcessorTests.cs | 4 ++-- .../tests/Common/Redaction/RedactionProcessor.cs | 4 ++-- 9 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index facda3d3ac92c0..547d8d60598d66 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -49,7 +49,7 @@ public partial interface ILogEntryPipelineFactory public partial interface ILogEntryProcessor { Microsoft.Extensions.Logging.LogEntryHandler GetLogEntryHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); - Microsoft.Extensions.Logging.ScopeHandler GetScopeHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull; + Microsoft.Extensions.Logging.ScopeHandler GetScopeHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled) where TState : notnull; bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel); } public partial interface ILogEntryProcessorFactory @@ -265,9 +265,8 @@ protected ScopeHandler() { } } public partial class ScopePipeline : Microsoft.Extensions.Logging.Pipeline where TState : notnull { - public ScopePipeline(Microsoft.Extensions.Logging.ScopeHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base (default(object), default(bool), default(bool)) { } + public ScopePipeline(Microsoft.Extensions.Logging.ScopeHandler handler, object? userState, bool isEnabled) : base (default(object), default(bool), default(bool)) { } public System.IDisposable? HandleScope(ref TState scope) { throw null; } - public bool IsEnabledDynamic(Microsoft.Extensions.Logging.LogLevel level) { throw null; } } } namespace Microsoft.Extensions.Logging.Abstractions diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index 76cced40522055..7262ce90093bf7 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -29,7 +29,7 @@ public ProcessorContext(ILogEntryProcessor processor, CancellationToken cancella public interface ILogEntryProcessor { LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); - ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull; + ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull; bool IsEnabled(LogLevel logLevel); } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs index c8efd208a2c9c7..aace472b72ee85 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs @@ -44,15 +44,14 @@ public LogEntryPipeline(LogEntryHandler handler, object? userState, bool public class ScopePipeline : Pipeline where TState : notnull { - public ScopePipeline(ScopeHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : - base(userState, isEnabled, isDynamicLevelCheckRequired) + public ScopePipeline(ScopeHandler handler, object? userState, bool isEnabled) : + base(userState, isEnabled, isDynamicLevelCheckRequired: false) { _firstHandler = handler; } private readonly ScopeHandler _firstHandler; - public bool IsEnabledDynamic(LogLevel level) => _firstHandler.IsEnabled(level); public IDisposable? HandleScope(ref TState scope) => _firstHandler.HandleBeginScope(ref scope); } } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs index 03005ce8c11443..e592e28c6a7e60 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs @@ -51,10 +51,9 @@ public LogEntryHandler GetLogEntryHandler(ILogMetadata? return new DynamicDispatchToLoggers(this, metadata?.GetStringMessageFormatter()); } - public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicCheckRequired) where TState : notnull + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull { enabled = true; - dynamicCheckRequired = false; return new DynamicDispatchScopeToLoggers(this); } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs index 0b8577787c52df..94f71a92bc9f9a 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs @@ -66,9 +66,9 @@ public LogEntryHandler GetLogEntryHandler(ILogMetadata? return _propCollection.GetLogEntryHandler(_nextProcessor, metadata, out enabled, out dynamicEnabledCheckRequired); } - public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull { - return _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return _nextProcessor.GetScopeHandler(metadata, out enabled); } public bool IsEnabled(LogLevel logLevel) diff --git a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs index f8716bdba8cccd..5f57eb357b84ff 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs @@ -197,8 +197,8 @@ public VersionedLoggerState(LoggerInformation[] loggers, ILogEntryProcessor proc { if (!Pipelines.TryGetValue(key, out pipeline)) { - ScopeHandler handler = Processor.GetScopeHandler(metadata, out bool enabled, out bool dynamicCheckRequired); - pipeline = new ScopePipeline(handler, userState, enabled, dynamicCheckRequired); + ScopeHandler handler = Processor.GetScopeHandler(metadata, out bool enabled); + pipeline = new ScopePipeline(handler, userState, enabled); // in a multi-threaded race it is possible to create new pipelines after the versioned state is already disposed // if this happens the pipeline is immediately marked as being not up-to-date. pipeline.IsUpToDate = _isUpToDate; @@ -229,7 +229,7 @@ public LogEntryHandler GetLogEntryHandler(ILogMetadata? throw new NotImplementedException(); } - public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull { throw new NotImplementedException(); } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs index 0720121f177019..e9816d632f9b0f 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs @@ -116,9 +116,9 @@ public LogEntryHandler GetLogEntryHandler(ILogMetadata? return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); } - public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull { - var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled); return new TestScopeHandler(nextHandler, _handleLogEntryCallback); } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs index 20b4901ed45938..b1bd9f042db1be 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs @@ -101,9 +101,9 @@ public LogEntryHandler GetLogEntryHandler(ILogMetadata? return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); } - public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull { - var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled); return new TestScopeHandler(nextHandler, _handleLogEntryCallback); } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs index cbb08a8eb69417..4ec37d914cc2b6 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs @@ -101,9 +101,9 @@ PropertyRedaction[] GetPolicyRedactions(ILogMetadata metadata) return redactions.ToArray(); } - public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) where TState : notnull + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull { - return _nextProcessor.GetScopeHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return _nextProcessor.GetScopeHandler(metadata, out enabled); } public bool IsEnabled(LogLevel level) => _nextProcessor.IsEnabled(level); From 4ccdd2267c5d24d26522ed0ece29bd645b5a7ab7 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 30 May 2023 13:16:03 +0800 Subject: [PATCH 20/26] Remove ILoggerStateWithMetadata --- ...crosoft.Extensions.Logging.Abstractions.cs | 4 -- .../src/LogEntryNew.cs | 6 --- .../src/DispatchProcessor.cs | 40 ------------------- .../src/Logger.cs | 14 +------ 4 files changed, 2 insertions(+), 62 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index 547d8d60598d66..357af5e8600dc4 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -71,10 +71,6 @@ public partial interface ILoggerProvider : System.IDisposable { Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName); } - public partial interface ILoggerStateWithMetadata - { - Microsoft.Extensions.Logging.ILogMetadata Metadata { get; } - } public partial interface ILogger : Microsoft.Extensions.Logging.ILogger { } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index 7262ce90093bf7..1199146df5800e 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -63,12 +63,6 @@ public abstract class ScopeHandler where TState : notnull public abstract IDisposable? HandleBeginScope(ref TState state); } - //TODO: Not sure if we need to keep this? - public interface ILoggerStateWithMetadata - { - public ILogMetadata Metadata { get; } - } - public struct LogPropertyInfo { public LogPropertyInfo(string name, object[]? metadata) diff --git a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs index e592e28c6a7e60..d6d024e176bd1a 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs @@ -288,46 +288,6 @@ private static void ThrowLoggingError(List exceptions) message: "An error occurred while writing to logger(s).", innerExceptions: exceptions); } - - - //Processors should also handle scopes - //ScopeLogger[]? loggers = ScopeLoggers; - //ScopeLogger[]? loggers = null; - - //if (loggers == null) - //{ - // return NullScope.Instance; - //} - - //if (loggers.Length == 1) - //{ - // return loggers[0].CreateScope(state); - //} - - //var scope = new Scope(loggers.Length); - //List? exceptions = null; - //for (int i = 0; i < loggers.Length; i++) - //{ - // ref readonly ScopeLogger scopeLogger = ref loggers[i]; - - // try - // { - // scope.SetDisposable(i, scopeLogger.CreateScope(state)); - // } - // catch (Exception ex) - // { - // exceptions ??= new List(); - // exceptions.Add(ex); - // } - //} - - //if (exceptions != null && exceptions.Count > 0) - //{ - // ThrowLoggingError(exceptions); - //} - - //return scope; - public override bool IsEnabled(LogLevel level) => _processor.IsEnabled(level); } } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs index 5f57eb357b84ff..74d4af9e958dcd 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs @@ -33,12 +33,7 @@ public Logger(LoggerFactory loggerFactory) public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - ILogMetadata? metadata = null; - if (state is ILoggerStateWithMetadata) - { - metadata = ((ILoggerStateWithMetadata)state).Metadata; - } - LogEntryPipeline pipeline = GetLoggingPipeline(metadata, this)!; + LogEntryPipeline pipeline = GetLoggingPipeline(metadata: null, this)!; if (!pipeline.IsEnabled || (pipeline.IsDynamicLevelCheckRequired && !pipeline.IsEnabledDynamic(logLevel))) { return; @@ -59,12 +54,7 @@ public bool IsEnabled(LogLevel level) public IDisposable? BeginScope(TState state) where TState : notnull { - ILogMetadata? metadata = null; - if (state is ILoggerStateWithMetadata) - { - metadata = ((ILoggerStateWithMetadata)state).Metadata; - } - ScopePipeline? pipeline = GetScopePipeline(metadata, this); + ScopePipeline? pipeline = GetScopePipeline(metadata: null, this); if (pipeline is null || !pipeline.IsEnabled) { return null; From 5b6951e0b82b285cf0166ffb6e2ed065b6a51fb9 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 31 May 2023 22:07:20 +0800 Subject: [PATCH 21/26] Enrichment feedback. Fix registering multiple processors --- .../src/EnrichmentExtensions.cs | 165 ++++++++++++------ .../src/LoggingBuilderExtensions.cs | 4 +- .../tests/Common/EnrichmentTests.cs | 131 +++++++------- .../tests/Common/ProcessorTests.cs | 75 -------- .../tests/Common/TestLogEntryHandler.cs | 33 ++++ .../tests/Common/TestLogEntryProcessor.cs | 33 ++++ .../tests/Common/TestScopeHandler.cs | 31 ++++ 7 files changed, 278 insertions(+), 194 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLogEntryHandler.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLogEntryProcessor.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/TestScopeHandler.cs diff --git a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs index 94f71a92bc9f9a..efa1d135a3137a 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs @@ -108,7 +108,7 @@ internal override EnrichmentPropertiesCollection AddProperty(string propertyN internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; + var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata, Name0) : null; LogEntryHandler> nextHandler = nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); return new EnrichmentHandler(nextHandler, this); @@ -137,17 +137,19 @@ public override void HandleLogEntry(ref LogEntry logEntry) private sealed class EnrichmentLogMetadata : ILogMetadata> { - private ILogMetadata _innerMetadata { get; } + private readonly ILogMetadata _innerMetadata; + private readonly string _name0; - public EnrichmentLogMetadata(ILogMetadata innerMetadata) + public EnrichmentLogMetadata(ILogMetadata innerMetadata, string name0) { _innerMetadata = innerMetadata; + _name0 = name0; } public LogLevel LogLevel => _innerMetadata.LogLevel; public EventId EventId => _innerMetadata.EventId; public string OriginalFormat => _innerMetadata.OriginalFormat; - public int PropertyCount => _innerMetadata.PropertyCount; + public int PropertyCount => _innerMetadata.PropertyCount + 1; public void AppendFormattedMessage(in EnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) { @@ -159,7 +161,22 @@ public FormatPropertyListAction> GetPropert FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); return new FormatPropertyListAction>((in EnrichmentPropertyValues s, ref BufferWriter w) => formatter(in s.NestedProperties, ref w)); } - public LogPropertyInfo GetPropertyInfo(int index) => _innerMetadata.GetPropertyInfo(index); + public LogPropertyInfo GetPropertyInfo(int index) + { + if (index < _innerMetadata.PropertyCount) + { + return _innerMetadata.GetPropertyInfo(index); + } + var i = index - _innerMetadata.PropertyCount; + if (i == 0) + { + return new LogPropertyInfo(_name0, null); + } + else + { + throw new IndexOutOfRangeException(nameof(index)); + } + } public Func, Exception?, string> GetStringMessageFormatter() { var formatter = _innerMetadata.GetStringMessageFormatter(); @@ -193,7 +210,7 @@ internal override EnrichmentPropertiesCollection AddProperty(string propertyN internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; + var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata, Name0, Name1) : null; LogEntryHandler> nextHandler = nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); return new EnrichmentHandler(nextHandler, this); @@ -226,17 +243,21 @@ public override void HandleLogEntry(ref LogEntry logEntry) private sealed class EnrichmentLogMetadata : ILogMetadata> { - private ILogMetadata _innerMetadata { get; } + private readonly ILogMetadata _innerMetadata; + private readonly string _name0; + private readonly string _name1; - public EnrichmentLogMetadata(ILogMetadata innerMetadata) + public EnrichmentLogMetadata(ILogMetadata innerMetadata, string name0, string name1) { _innerMetadata = innerMetadata; + _name0 = name0; + _name1 = name1; } public LogLevel LogLevel => _innerMetadata.LogLevel; public EventId EventId => _innerMetadata.EventId; public string OriginalFormat => _innerMetadata.OriginalFormat; - public int PropertyCount => _innerMetadata.PropertyCount; + public int PropertyCount => _innerMetadata.PropertyCount + 2; public void AppendFormattedMessage(in EnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) { @@ -248,7 +269,26 @@ public FormatPropertyListAction> GetPro FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); return new FormatPropertyListAction>((in EnrichmentPropertyValues s, ref BufferWriter w) => formatter(in s.NestedProperties, ref w)); } - public LogPropertyInfo GetPropertyInfo(int index) => _innerMetadata.GetPropertyInfo(index); + public LogPropertyInfo GetPropertyInfo(int index) + { + if (index < _innerMetadata.PropertyCount) + { + return _innerMetadata.GetPropertyInfo(index); + } + var i = index - _innerMetadata.PropertyCount; + if (i == 0) + { + return new LogPropertyInfo(_name0, null); + } + else if (i == 1) + { + return new LogPropertyInfo(_name1, null); + } + else + { + throw new IndexOutOfRangeException(nameof(index)); + } + } public Func, Exception?, string> GetStringMessageFormatter() { var formatter = _innerMetadata.GetStringMessageFormatter(); @@ -282,7 +322,7 @@ internal override EnrichmentPropertiesCollection AddProperty(string propertyN internal override LogEntryHandler GetLogEntryHandler(ILogEntryProcessor nextProcessor, ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata) : null; + var enrichmentMetadata = metadata != null ? new EnrichmentLogMetadata(metadata, Name0, Name1, OverflowProperties) : null; LogEntryHandler> nextHandler = nextProcessor.GetLogEntryHandler>(enrichmentMetadata, out enabled, out dynamicEnabledCheckRequired); return new EnrichmentHandler(nextHandler, this); @@ -325,16 +365,22 @@ public override void HandleLogEntry(ref LogEntry logEntry) private sealed class EnrichmentLogMetadata : ILogMetadata> { private ILogMetadata _innerMetadata { get; } + private readonly string _name0; + private readonly string _name1; + private readonly List<(string, Func)> _overflowProperties; - public EnrichmentLogMetadata(ILogMetadata innerMetadata) + public EnrichmentLogMetadata(ILogMetadata innerMetadata, string name0, string name1, List<(string, Func)> overflowProperties) { _innerMetadata = innerMetadata; + _name0 = name0; + _name1 = name1; + _overflowProperties = overflowProperties; } public LogLevel LogLevel => _innerMetadata.LogLevel; public EventId EventId => _innerMetadata.EventId; public string OriginalFormat => _innerMetadata.OriginalFormat; - public int PropertyCount => _innerMetadata.PropertyCount; + public int PropertyCount => _innerMetadata.PropertyCount + 2 + _overflowProperties.Count; public void AppendFormattedMessage(in UnboundedEnrichmentPropertyValues state, IBufferWriter buffer) => _innerMetadata.AppendFormattedMessage(state.NestedProperties, buffer); public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) { @@ -346,7 +392,26 @@ public FormatPropertyListAction formatter = _innerMetadata.GetPropertyListFormatter(propertyFormatterFactory); return new FormatPropertyListAction>((in UnboundedEnrichmentPropertyValues s, ref BufferWriter w) => formatter(in s.NestedProperties, ref w)); } - public LogPropertyInfo GetPropertyInfo(int index) => _innerMetadata.GetPropertyInfo(index); + public LogPropertyInfo GetPropertyInfo(int index) + { + if (index < _innerMetadata.PropertyCount) + { + return _innerMetadata.GetPropertyInfo(index); + } + var i = index - _innerMetadata.PropertyCount; + if (i == 0) + { + return new LogPropertyInfo(_name0, null); + } + else if (i == 1) + { + return new LogPropertyInfo(_name1, null); + } + else + { + return new LogPropertyInfo(_overflowProperties[i - 2].Item1, null); + } + } public Func, Exception?, string> GetStringMessageFormatter() { var formatter = _innerMetadata.GetStringMessageFormatter(); @@ -382,13 +447,15 @@ public int Count get { var nested = NestedProperties as IReadOnlyList>; - if (index == 0) + if (nested != null && index < nested.Count) { - return Prop0; + return nested[index]; } - else if (nested != null) + + var i = index - nested?.Count ?? 0; + if (i == 0) { - return nested[index - 1]; + return Prop0; } else { @@ -406,8 +473,6 @@ public static string Format(EnrichmentPropertyValues public IEnumerator> GetEnumerator() { - yield return Prop0; - var nested = NestedProperties as IReadOnlyList>; if (nested != null) { @@ -416,6 +481,8 @@ public static string Format(EnrichmentPropertyValues yield return item; } } + + yield return Prop0; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -450,17 +517,19 @@ public int Count get { var nested = NestedProperties as IReadOnlyList>; - if (index == 0) + if (nested != null && index < nested.Count) { - return Prop0; + return nested[index]; } - else if (index == 1) + + var i = index - nested?.Count ?? 0; + if (i == 0) { - return Prop1; + return Prop0; } - else if (nested != null) + else if (i == 1) { - return nested[index - 1]; + return Prop1; } else { @@ -478,9 +547,6 @@ public static string Format(EnrichmentPropertyValues> GetEnumerator() { - yield return Prop0; - yield return Prop1; - var nested = NestedProperties as IReadOnlyList>; if (nested != null) { @@ -489,6 +555,9 @@ public static string Format(EnrichmentPropertyValues GetEnumerator(); @@ -525,29 +594,23 @@ public int Count get { var nested = NestedProperties as IReadOnlyList>; - if (index == 0) + if (nested != null && index < nested.Count) + { + return nested[index]; + } + + var i = index - nested?.Count ?? 0; + if (i == 0) { return Prop0; } - else if (index == 1) + else if (i == 1) { return Prop1; } else { - var i = index - 2; - if (i < ExtraValues.Length) - { - return ExtraValues[i]; - } - else if (nested != null) - { - return nested[i - ExtraValues.Length]; - } - else - { - throw new IndexOutOfRangeException(nameof(index)); - } + return ExtraValues[i - 2]; } } } @@ -561,13 +624,6 @@ public static string Format(UnboundedEnrichmentPropertyValues> GetEnumerator() { - yield return Prop0; - yield return Prop1; - for (var i = 0; i < ExtraValues.Length; i++) - { - yield return ExtraValues[i]; - } - var nested = NestedProperties as IReadOnlyList>; if (nested != null) { @@ -576,6 +632,13 @@ public static string Format(UnboundedEnrichmentPropertyValues GetEnumerator(); diff --git a/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs index 42c8820222e747..85ea5c78e188aa 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs @@ -63,13 +63,13 @@ public static ILoggingBuilder Configure(this ILoggingBuilder builder, Action(this ILoggingBuilder builder, Func getProcessor) where T : ILogEntryProcessor { - builder.Services.TryAddSingleton(new ProcessorFactory(getProcessor)); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton>(sp => new ProcessorFactory(getProcessor))); return builder; } public static ILoggingBuilder AddProcessor(this ILoggingBuilder builder, Func getProcessor) where T : ILogEntryProcessor { - builder.Services.TryAddSingleton(sp => new ProcessorFactory((next) => getProcessor(sp, next))); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton>(sp => new ProcessorFactory((next) => getProcessor(sp, next)))); return builder; } } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs index e9816d632f9b0f..f604218b0ed120 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs @@ -6,7 +6,6 @@ using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Microsoft.Extensions.Logging.Test @@ -14,14 +13,12 @@ namespace Microsoft.Extensions.Logging.Test public class EnrichmentTests { [Fact] - public void LogInformation_InvokesProcessor() + public void LogInformation_PropertyAddedToState() { // Arrange var sink = new TestSink(); var provider = new TestLoggerProvider(sink, isEnabled: true); - List logMessages = new List(); - var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(builder => { @@ -44,11 +41,6 @@ public void LogInformation_InvokesProcessor() Assert.Null(write.Exception); Assert.Collection((IReadOnlyList>)write.State, - p => - { - Assert.Equal("prop1", p.Key); - Assert.Equal("Value!", p.Value); - }, p => { Assert.Equal("Name", p.Key); @@ -58,24 +50,41 @@ public void LogInformation_InvokesProcessor() { Assert.Equal("{OriginalFormat}", p.Key); Assert.Equal("Hello {Name}", p.Value); + }, + p => + { + Assert.Equal("prop1", p.Key); + Assert.Equal("Value!", p.Value); }); } - [Fact] - public void DefinedLog_InvokesProcessor() + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(10)] + public void LogInformation_PropertyCount_PropertyAddedToState(int enrichPropertyCount) { // Arrange var sink = new TestSink(); var provider = new TestLoggerProvider(sink, isEnabled: true); - List logMessages = new List(); + var beforeMetadatas = new List(); + var afterMetadatas = new List(); var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(builder => { builder.SetMinimumLevel(LogLevel.Trace); builder.AddProvider(provider); - builder.AddProcessor((serviceProvider, processor) => new TestLogEntryProcessor(processor, m => logMessages.Add(m))); + + for (var i = 0; i < enrichPropertyCount; i++) + { + var capturedIndex = i; + builder.Enrich($"prop{capturedIndex}", () => $"value{capturedIndex}"); + } + builder.AddProcessor((serviceProvider, processor) => new TestLogMetadataLogEntryProcessor(processor, m => afterMetadatas.Add(m))); }); var loggerFactory = serviceCollection.BuildServiceProvider().GetRequiredService(); var logger = loggerFactory.CreateLogger("Test"); @@ -89,89 +98,79 @@ public void DefinedLog_InvokesProcessor() definedLog(logger, "John Doe", 10, null); // Assert - Assert.Collection(logMessages, m => Assert.Equal("Hello John Doe. You are 10 years old.", m)); - Assert.Equal(1, sink.Writes.Count()); Assert.True(sink.Writes.TryTake(out var write)); Assert.Equal(LogLevel.Information, write.LogLevel); Assert.Equal("Hello John Doe. You are 10 years old.", write.State.ToString()); Assert.Equal(1, write.EventId); Assert.Null(write.Exception); - } - private sealed class TestLogEntryProcessor : ILogEntryProcessor - { - private readonly ILogEntryProcessor _nextProcessor; - private readonly Action _handleLogEntryCallback; - - public TestLogEntryProcessor(ILogEntryProcessor nextProcessor, Action handleLogEntryCallback) + var values = Assert.IsAssignableFrom>>(write.State); + AssertPropertyAtIndex(values, 0, "Name", "John Doe"); + AssertPropertyAtIndex(values, 1, "Age", 10); + AssertPropertyAtIndex(values, 2, "{OriginalFormat}", "Hello {Name}. You are {Age} years old."); + for (var i = 0; i < enrichPropertyCount; i++) { - _nextProcessor = nextProcessor; - _handleLogEntryCallback = handleLogEntryCallback; + AssertPropertyAtIndex(values, i + 3, $"prop{i}", $"value{i}"); } - public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) - { - var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); - return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); - } + var after = Assert.Single(afterMetadatas); + Assert.Equal(enrichPropertyCount + 2, after.PropertyCount); + Assert.Equal(enrichPropertyCount + 2, after.PropertyInfos.Length); - public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull + void AssertPropertyAtIndex(IReadOnlyList> values, int index, string key, object value) { - var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled); - return new TestScopeHandler(nextHandler, _handleLogEntryCallback); - } + var kvp = values[index]; + Assert.Equal(key, kvp.Key); + Assert.Equal(value, kvp.Value); - public bool IsEnabled(LogLevel logLevel) => _nextProcessor.IsEnabled(logLevel); + kvp = values.ElementAt(index); + Assert.Equal(key, kvp.Key); + Assert.Equal(value, kvp.Value); + } } - private sealed class TestLogEntryHandler : LogEntryHandler + internal class LogMetadataInfo { - private readonly LogEntryHandler _nextHandler; - private readonly Action _handleLogEntryCallback; - - public TestLogEntryHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) - { - _nextHandler = nextHandler; - _handleLogEntryCallback = handleLogEntryCallback; - } - - public override void HandleLogEntry(ref LogEntry logEntry) - { - var message = logEntry.Formatter(logEntry.State, logEntry.Exception); - _handleLogEntryCallback(message); - - _nextHandler.HandleLogEntry(ref logEntry); - } - - public override bool IsEnabled(LogLevel level) - { - return _nextHandler.IsEnabled(level); - } + public int PropertyCount { get; set; } + public LogPropertyInfo[] PropertyInfos { get; set; } } - private sealed class TestScopeHandler : ScopeHandler + internal sealed class TestLogMetadataLogEntryProcessor : ILogEntryProcessor { - private readonly ScopeHandler _nextHandler; - private readonly Action _handleLogEntryCallback; + private readonly ILogEntryProcessor _nextProcessor; + private readonly Action _handleLogEntryCallback; - public TestScopeHandler(ScopeHandler nextHandler, Action handleLogEntryCallback) + public TestLogMetadataLogEntryProcessor(ILogEntryProcessor nextProcessor, Action handleLogEntryCallback) { - _nextHandler = nextHandler; + _nextProcessor = nextProcessor; _handleLogEntryCallback = handleLogEntryCallback; } - public override IDisposable? HandleBeginScope(ref TState state) + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) { - _handleLogEntryCallback(state.ToString()); + var propertyInfos = new List(); + for (var i = 0; i < metadata.PropertyCount; i++) + { + propertyInfos.Add(metadata.GetPropertyInfo(i)); + } + _handleLogEntryCallback(new LogMetadataInfo + { + PropertyCount = metadata.PropertyCount, + PropertyInfos = propertyInfos.ToArray() + }); - return _nextHandler.HandleBeginScope(ref state); + var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return new TestLogEntryHandler(nextHandler, null); } - public override bool IsEnabled(LogLevel level) + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull { - return _nextHandler.IsEnabled(level); + var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled); + return new TestScopeHandler(nextHandler, null); } + + public bool IsEnabled(LogLevel logLevel) => _nextProcessor.IsEnabled(logLevel); } } } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs index b1bd9f042db1be..0143e0b640b01a 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ProcessorTests.cs @@ -83,80 +83,5 @@ public void DefinedLog_InvokesProcessor() Assert.Equal(1, write.EventId); Assert.Null(write.Exception); } - - private sealed class TestLogEntryProcessor : ILogEntryProcessor - { - private readonly ILogEntryProcessor _nextProcessor; - private readonly Action _handleLogEntryCallback; - - public TestLogEntryProcessor(ILogEntryProcessor nextProcessor, Action handleLogEntryCallback) - { - _nextProcessor = nextProcessor; - _handleLogEntryCallback = handleLogEntryCallback; - } - - public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) - { - var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); - return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); - } - - public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull - { - var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled); - return new TestScopeHandler(nextHandler, _handleLogEntryCallback); - } - - public bool IsEnabled(LogLevel logLevel) => _nextProcessor.IsEnabled(logLevel); - } - - private sealed class TestLogEntryHandler : LogEntryHandler - { - private readonly LogEntryHandler _nextHandler; - private readonly Action _handleLogEntryCallback; - - public TestLogEntryHandler(LogEntryHandler nextHandler, Action handleLogEntryCallback) - { - _nextHandler = nextHandler; - _handleLogEntryCallback = handleLogEntryCallback; - } - - public override void HandleLogEntry(ref LogEntry logEntry) - { - var message = logEntry.Formatter(logEntry.State, logEntry.Exception); - _handleLogEntryCallback(message); - - _nextHandler.HandleLogEntry(ref logEntry); - } - - public override bool IsEnabled(LogLevel level) - { - return _nextHandler.IsEnabled(level); - } - } - - private sealed class TestScopeHandler : ScopeHandler - { - private readonly ScopeHandler _nextHandler; - private readonly Action _handleLogEntryCallback; - - public TestScopeHandler(ScopeHandler nextHandler, Action handleLogEntryCallback) - { - _nextHandler = nextHandler; - _handleLogEntryCallback = handleLogEntryCallback; - } - - public override IDisposable? HandleBeginScope(ref TState state) - { - _handleLogEntryCallback(state.ToString()); - - return _nextHandler.HandleBeginScope(ref state); - } - - public override bool IsEnabled(LogLevel level) - { - return _nextHandler.IsEnabled(level); - } - } } } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLogEntryHandler.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLogEntryHandler.cs new file mode 100644 index 00000000000000..050bd7c1269dbf --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLogEntryHandler.cs @@ -0,0 +1,33 @@ +// 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.Logging.Abstractions; + +namespace Microsoft.Extensions.Logging.Test +{ + internal sealed class TestLogEntryHandler : LogEntryHandler + { + private readonly LogEntryHandler _nextHandler; + private readonly Action? _handleLogEntryCallback; + + public TestLogEntryHandler(LogEntryHandler nextHandler, Action? handleLogEntryCallback) + { + _nextHandler = nextHandler; + _handleLogEntryCallback = handleLogEntryCallback; + } + + public override void HandleLogEntry(ref LogEntry logEntry) + { + var message = logEntry.Formatter(logEntry.State, logEntry.Exception); + _handleLogEntryCallback?.Invoke(message); + + _nextHandler.HandleLogEntry(ref logEntry); + } + + public override bool IsEnabled(LogLevel level) + { + return _nextHandler.IsEnabled(level); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLogEntryProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLogEntryProcessor.cs new file mode 100644 index 00000000000000..a104b650123129 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLogEntryProcessor.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Logging.Test +{ + internal sealed class TestLogEntryProcessor : ILogEntryProcessor + { + private readonly ILogEntryProcessor _nextProcessor; + private readonly Action _handleLogEntryCallback; + + public TestLogEntryProcessor(ILogEntryProcessor nextProcessor, Action handleLogEntryCallback) + { + _nextProcessor = nextProcessor; + _handleLogEntryCallback = handleLogEntryCallback; + } + + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + var nextHandler = _nextProcessor.GetLogEntryHandler(metadata, out enabled, out dynamicEnabledCheckRequired); + return new TestLogEntryHandler(nextHandler, _handleLogEntryCallback); + } + + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull + { + var nextHandler = _nextProcessor.GetScopeHandler(metadata, out enabled); + return new TestScopeHandler(nextHandler, _handleLogEntryCallback); + } + + public bool IsEnabled(LogLevel logLevel) => _nextProcessor.IsEnabled(logLevel); + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestScopeHandler.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestScopeHandler.cs new file mode 100644 index 00000000000000..8e4cc5fcd79574 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestScopeHandler.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Logging.Test +{ + internal sealed class TestScopeHandler : ScopeHandler + { + private readonly ScopeHandler _nextHandler; + private readonly Action _handleLogEntryCallback; + + public TestScopeHandler(ScopeHandler nextHandler, Action handleLogEntryCallback) + { + _nextHandler = nextHandler; + _handleLogEntryCallback = handleLogEntryCallback; + } + + public override IDisposable? HandleBeginScope(ref TState state) + { + _handleLogEntryCallback(state.ToString()); + + return _nextHandler.HandleBeginScope(ref state); + } + + public override bool IsEnabled(LogLevel level) + { + return _nextHandler.IsEnabled(level); + } + } +} From 337bf2197f6791f85b30244169d6d7276853a448 Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Mon, 5 Jun 2023 06:09:48 -0700 Subject: [PATCH 22/26] Remove Pipeline types from public API --- ...crosoft.Extensions.Logging.Abstractions.cs | 29 +-- .../src/LogEntryPipeline.cs | 52 ++++-- .../src/LoggerMessage.cs | 107 ++++------- .../src/LoggerT.cs | 96 ++++------ .../src/Logger.cs | 173 +++++++++++------- .../src/LoggerFactory.cs | 4 +- .../src/PipelineManager.cs | 49 ----- .../tests/Common/LoggerMessageTest.cs | 46 +++++ .../tests/Common/TestLoggerWithProcessor.cs | 95 ++++++++++ 9 files changed, 351 insertions(+), 300 deletions(-) delete mode 100644 src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLoggerWithProcessor.cs diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index 357af5e8600dc4..7727585c9b49e0 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -41,11 +41,6 @@ public partial interface IExternalScopeProvider void ForEachScope(System.Action callback, TState state); System.IDisposable Push(object? state); } - public partial interface ILogEntryPipelineFactory - { - Microsoft.Extensions.Logging.LogEntryPipeline? GetLoggingPipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState); - Microsoft.Extensions.Logging.ScopePipeline? GetScopePipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState) where TState : notnull; - } public partial interface ILogEntryProcessor { Microsoft.Extensions.Logging.LogEntryHandler GetLogEntryHandler(Microsoft.Extensions.Logging.ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired); @@ -111,12 +106,6 @@ protected LogEntryHandler() { } public abstract void HandleLogEntry(ref Microsoft.Extensions.Logging.Abstractions.LogEntry logEntry); public abstract bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level); } - public partial class LogEntryPipeline : Microsoft.Extensions.Logging.Pipeline - { - public LogEntryPipeline(Microsoft.Extensions.Logging.LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : base (default(object), default(bool), default(bool)) { } - public void HandleLogEntry(ref Microsoft.Extensions.Logging.Abstractions.LogEntry logEntry) { } - public bool IsEnabledDynamic(Microsoft.Extensions.Logging.LogLevel level) { throw null; } - } public static partial class LoggerExtensions { public static System.IDisposable? BeginScope(this Microsoft.Extensions.Logging.ILogger logger, string messageFormat, params object?[] args) { throw null; } @@ -197,11 +186,10 @@ public LoggerMessageAttribute(int eventId, Microsoft.Extensions.Logging.LogLevel public string Message { get { throw null; } set { } } public bool SkipEnabledCheck { get { throw null; } set { } } } - public partial class Logger : Microsoft.Extensions.Logging.ILogEntryPipelineFactory, Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.ILogger + public partial class Logger : Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.ILogEntryProcessorFactory { public Logger(Microsoft.Extensions.Logging.ILoggerFactory factory) { } - Microsoft.Extensions.Logging.LogEntryPipeline Microsoft.Extensions.Logging.ILogEntryPipelineFactory.GetLoggingPipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState) { throw null; } - Microsoft.Extensions.Logging.ScopePipeline Microsoft.Extensions.Logging.ILogEntryPipelineFactory.GetScopePipeline(Microsoft.Extensions.Logging.ILogMetadata? metadata, object? userState) { throw null; } + public ProcessorContext GetProcessor() { throw null; } System.IDisposable Microsoft.Extensions.Logging.ILogger.BeginScope(TState state) { throw null; } bool Microsoft.Extensions.Logging.ILogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) { throw null; } void Microsoft.Extensions.Logging.ILogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func formatter) { } @@ -224,14 +212,6 @@ public partial struct LogPropertyInfo public readonly object[]? Metadata { get { throw null; } } public readonly string Name { get { throw null; } } } - public partial class Pipeline - { - public Pipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) { } - public bool IsDynamicLevelCheckRequired { get { throw null; } } - public bool IsEnabled { get { throw null; } } - public bool IsUpToDate { get { throw null; } set { } } - public object? UserState { get { throw null; } } - } public readonly partial struct ProcessorContext { private readonly object _dummy; @@ -259,11 +239,6 @@ protected ScopeHandler() { } public abstract System.IDisposable? HandleBeginScope(ref TState state); public abstract bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level); } - public partial class ScopePipeline : Microsoft.Extensions.Logging.Pipeline where TState : notnull - { - public ScopePipeline(Microsoft.Extensions.Logging.ScopeHandler handler, object? userState, bool isEnabled) : base (default(object), default(bool), default(bool)) { } - public System.IDisposable? HandleScope(ref TState scope) { throw null; } - } } namespace Microsoft.Extensions.Logging.Abstractions { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs index aace472b72ee85..e2ada3aae45688 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryPipeline.cs @@ -2,36 +2,33 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Threading; using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Logging { - public interface ILogEntryPipelineFactory + //TODO: Pipeline isn't the best name for this, but leaving it as-is to make the refactor clearer. + //Really it is a single entry in a cache that maps ILogger -> Handler + internal class Pipeline { - public LogEntryPipeline? GetLoggingPipeline(ILogMetadata? metadata, object? userState); - public ScopePipeline? GetScopePipeline(ILogMetadata? metadata, object? userState) where TState : notnull; - } - - public class Pipeline - { - public Pipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) + public Pipeline(object? userState, bool isEnabled, bool isDynamicLevelCheckRequired, CancellationToken cancelToken) { UserState = userState; IsEnabled = isEnabled; IsDynamicLevelCheckRequired = isDynamicLevelCheckRequired; - IsUpToDate = true; + CancelToken = cancelToken; } public object? UserState { get; } public bool IsEnabled { get; } public bool IsDynamicLevelCheckRequired { get; } - public bool IsUpToDate { get; set; } + public CancellationToken CancelToken { get; } } - public class LogEntryPipeline : Pipeline + internal class LogEntryPipeline : Pipeline { - public LogEntryPipeline(LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired) : - base(userState, isEnabled, isDynamicLevelCheckRequired) + public LogEntryPipeline(LogEntryHandler handler, object? userState, bool isEnabled, bool isDynamicLevelCheckRequired, CancellationToken cancelToken) : + base(userState, isEnabled, isDynamicLevelCheckRequired, cancelToken) { _firstHandler = handler; } @@ -42,10 +39,10 @@ public LogEntryPipeline(LogEntryHandler handler, object? userState, bool public void HandleLogEntry(ref LogEntry logEntry) => _firstHandler.HandleLogEntry(ref logEntry); } - public class ScopePipeline : Pipeline where TState : notnull + internal class ScopePipeline : Pipeline where TState : notnull { - public ScopePipeline(ScopeHandler handler, object? userState, bool isEnabled) : - base(userState, isEnabled, isDynamicLevelCheckRequired: false) + public ScopePipeline(ScopeHandler handler, object? userState, bool isEnabled, CancellationToken cancelToken) : + base(userState, isEnabled, isDynamicLevelCheckRequired: false, cancelToken) { _firstHandler = handler; } @@ -54,4 +51,27 @@ public ScopePipeline(ScopeHandler handler, object? userState, bool isEna public IDisposable? HandleScope(ref TState scope) => _firstHandler.HandleBeginScope(ref scope); } + + + internal class InvokeLoggerLogHandler : LogEntryHandler + { + private ILogger _logger; + public InvokeLoggerLogHandler(ILogger logger) + { + _logger = logger; + } + public override void HandleLogEntry(ref LogEntry logEntry) => _logger.Log(logEntry.LogLevel, logEntry.EventId, logEntry.State, logEntry.Exception, logEntry.Formatter); + public override bool IsEnabled(LogLevel level) => _logger.IsEnabled(level); + } + + internal class InvokeLoggerScopeHandler : ScopeHandler where TState : notnull + { + private ILogger _logger; + public InvokeLoggerScopeHandler(ILogger logger) + { + _logger = logger; + } + public override IDisposable? HandleBeginScope(ref TState state) => _logger.BeginScope(state); + public override bool IsEnabled(LogLevel level) => true; + } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs index b18ea26d1fe4a4..622b878c95b102 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Collections; using System.Collections.Generic; +using System.Threading; using Microsoft.Extensions.Logging.Abstractions; using static Microsoft.Extensions.Logging.LoggerMessage; @@ -219,43 +220,38 @@ public static Log Define(ILogMetadata metadata, LogDefin void Log(ILogger logger, ref TState state, Exception? exception) { - LogEntryPipeline? pipelineSnapshot = pipeline; - if (pipelineSnapshot != null && pipelineSnapshot.UserState == logger && pipelineSnapshot.IsUpToDate) - { - if (!pipelineSnapshot.IsEnabled || - (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(metadata.LogLevel))) - return; - LogEntry entry = new LogEntry(metadata.LogLevel, category: null!, metadata.EventId, state, exception, null!); - pipelineSnapshot.HandleLogEntry(ref entry); - } - else - { - LogSlowPath(logger, ref state, exception); - } + LogEntry entry = new LogEntry(metadata.LogLevel, category: null!, metadata.EventId, state, exception, null!); + LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); } + } - void LogSlowPath(ILogger logger, ref TState state, Exception? exception) + private static void LogCore(ref LogEntryPipeline? cachedPipeline, ILogger logger, ILogMetadata? metadata, ref LogEntry entry, bool needFullEnabledCheck) + { + if (cachedPipeline == null || cachedPipeline.UserState != logger || cachedPipeline.CancelToken.IsCancellationRequested) { - LogEntryPipeline? pipelineSnapshot = null; - LogEntry entry = new LogEntry(metadata.LogLevel, category: null!, metadata.EventId, state, exception, null!); - if (logger is ILogEntryPipelineFactory) - { - pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetLoggingPipeline(metadata, logger); - pipeline = pipelineSnapshot; - } - if (pipelineSnapshot != null) - { - if (!pipelineSnapshot.IsEnabled || - (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(metadata.LogLevel))) - return; - pipelineSnapshot.HandleLogEntry(ref entry); - } - else - { - if (needFullEnabledCheck && logger.IsEnabled(metadata.LogLevel)) - return; - logger.Log(entry.LogLevel, entry.EventId, entry.State, entry.Exception, metadata.GetStringMessageFormatter()); - } + cachedPipeline = GetLogEntryPipeline(metadata, logger); + } + + if (!cachedPipeline.IsEnabled || + (cachedPipeline.IsDynamicLevelCheckRequired && needFullEnabledCheck && !cachedPipeline.IsEnabledDynamic(entry.LogLevel))) + { + return; + } + + cachedPipeline.HandleLogEntry(ref entry); + } + + private static LogEntryPipeline GetLogEntryPipeline(ILogMetadata? metadata, ILogger logger) + { + if (logger is ILogEntryProcessorFactory) + { + ProcessorContext context = ((ILogEntryProcessorFactory)logger).GetProcessor(); + LogEntryHandler handler = context.Processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicEnableCheckRequired); + return new LogEntryPipeline(handler, logger, enabled, dynamicEnableCheckRequired, context.CancellationToken); + } + else + { + return new LogEntryPipeline(new InvokeLoggerLogHandler(logger), logger, true, true, CancellationToken.None); } } @@ -290,46 +286,9 @@ void LogSlowPath(ILogger logger, ref TState state, Exception? exception) void Log(ILogger logger, T1 arg1, T2 arg2, Exception? exception) { - LogEntryPipeline>? pipelineSnapshot = pipeline; - if (pipelineSnapshot != null && pipelineSnapshot.UserState == logger && pipelineSnapshot.IsUpToDate) - { - if (!pipelineSnapshot.IsEnabled || - (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) - return; - LogValues state = new LogValues(metadata, arg1, arg2); - LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, LogValues.Callback); - pipelineSnapshot.HandleLogEntry(ref entry); - } - else - { - LogSlowPath(logger, arg1, arg2, exception); - } - } - - void LogSlowPath(ILogger logger, T1 arg1, T2 arg2, Exception? exception) - { - LogEntryPipeline>? pipelineSnapshot = null; - if (logger is ILogEntryPipelineFactory) - { - pipelineSnapshot = ((ILogEntryPipelineFactory)logger).GetLoggingPipeline(metadata, logger); - pipeline = pipelineSnapshot; - } - if (pipelineSnapshot != null) - { - if (!pipelineSnapshot.IsEnabled || - (pipelineSnapshot.IsDynamicLevelCheckRequired && needFullEnabledCheck && !pipelineSnapshot.IsEnabledDynamic(logLevel))) - return; - LogValues state = new LogValues(metadata, arg1, arg2); - LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, LogValues.Callback); - pipelineSnapshot.HandleLogEntry(ref entry); - } - else - { - if (needFullEnabledCheck && !logger.IsEnabled(logLevel)) - return; - LogValues state = new LogValues(metadata, arg1, arg2); - logger.Log(logLevel, eventId, state, exception, LogValues.Callback); - } + LogValues state = new LogValues(metadata, arg1, arg2); + LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, LogValues.Callback); + LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs index 5f2711dfe6e52a..10bdce589bc68f 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Threading; using Microsoft.Extensions.Internal; namespace Microsoft.Extensions.Logging @@ -12,10 +13,8 @@ namespace Microsoft.Extensions.Logging /// provided . /// /// The type. - public class Logger : ILogger, ILogEntryPipelineFactory + public class Logger : ILogger, ILogEntryProcessorFactory { - //private Dictionary _metadataPipelines = new Dictionary(); // metadata -> LogEntryPipeline - //private Dictionary _noMetadataPipelines = new Dictionary(); private readonly ILogger _logger; /// @@ -29,6 +28,38 @@ public Logger(ILoggerFactory factory) _logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T), includeGenericParameters: false, nestedTypeDelimiter: '.')); } + private class Processor : ILogEntryProcessor + { + private ILogger _logger; + public Processor(ILogger logger) + { + _logger = logger; + } + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + enabled = true; + dynamicEnabledCheckRequired = true; + return new InvokeLoggerLogHandler(_logger); + } + + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull + { + enabled = true; + return new InvokeLoggerScopeHandler(_logger); + } + + public bool IsEnabled(LogLevel logLevel) => _logger.IsEnabled(logLevel); + } + + public ProcessorContext GetProcessor() + { + if(_logger is ILogEntryProcessorFactory factory) + { + return factory.GetProcessor(); + } + return new ProcessorContext(new Processor(_logger), CancellationToken.None); + } + /// IDisposable? ILogger.BeginScope(TState state) { @@ -46,65 +77,6 @@ void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Excep { _logger.Log(logLevel, eventId, state, exception, formatter); } - - ScopePipeline? ILogEntryPipelineFactory.GetScopePipeline(ILogMetadata? metadata, object? userState) - { - if (_logger is ILogEntryPipelineFactory factory) - { - return factory.GetScopePipeline(metadata, userState); - } - else - { - return null; - } - } - - LogEntryPipeline? ILogEntryPipelineFactory.GetLoggingPipeline(ILogMetadata? metadata, object? userState) - { - if (_logger is ILogEntryPipelineFactory factory) - { - return factory.GetLoggingPipeline(metadata, userState); - } - else - { - return null; - } - - /* - if (_logger is not ILogEntryProcessor) - return null; - object pipeline; - if (metadata != null) - { - lock (_metadataPipelines) - { - if (!_metadataPipelines.TryGetValue(metadata, out pipeline)) - { - pipeline = BuildPipeline(metadata); - _metadataPipelines[metadata] = pipeline; - } - } - } - else - { - lock (_noMetadataPipelines) - { - if (!_noMetadataPipelines.TryGetValue(typeof(TState), out pipeline)) - { - pipeline = BuildPipeline(null); - _noMetadataPipelines[typeof(TState)] = pipeline; - } - } - } - return (LogEntryPipeline)pipeline; - */ - } - - /* - private LogEntryPipeline BuildPipeline(ILogMetadata? metadata) - { - return new LogEntryPipeline(((ILogEntryProcessor)_logger).GetLogEntryHandler(metadata), this, true, false); - }*/ } } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs index 74d4af9e958dcd..d3bdc36c35a9f9 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Logging { - internal sealed class Logger : ILogger, ILogEntryPipelineFactory + internal sealed class Logger : ILogger, ILogEntryProcessorFactory { private readonly LoggerFactory _loggerFactory; @@ -21,45 +22,23 @@ public Logger(LoggerFactory loggerFactory) public Action ProcessorInvalidated => () => _loggerFactory.OnProcessorInvalidated(this); - public LogEntryPipeline? GetLoggingPipeline(ILogMetadata? metadata, object? userState) - { - return VersionedState.GetLoggingPipeline(metadata, userState); - } - - public ScopePipeline? GetScopePipeline(ILogMetadata? metadata, object? userState) where TState : notnull - { - return VersionedState.GetScopePipeline(metadata, userState); - } - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - LogEntryPipeline pipeline = GetLoggingPipeline(metadata: null, this)!; - if (!pipeline.IsEnabled || (pipeline.IsDynamicLevelCheckRequired && !pipeline.IsEnabledDynamic(logLevel))) + LogEntryHandler handler = VersionedState.GetLogEntryHandler(null, out bool enabled, out bool dynamicCheckRequired); + if(!enabled || (dynamicCheckRequired && !handler.IsEnabled(logLevel))) { return; } LogEntry logEntry = new LogEntry(logLevel, category: null!, eventId, state, exception, formatter); - pipeline.HandleLogEntry(ref logEntry); + handler.HandleLogEntry(ref logEntry); } - public bool IsEnabled(LogLevel level) - { - ILogEntryProcessor processor = VersionedState.Processor; - if (processor != null) - { - return processor.IsEnabled(level); - } - return false; - } + public bool IsEnabled(LogLevel level) => VersionedState.IsEnabled(level); public IDisposable? BeginScope(TState state) where TState : notnull { - ScopePipeline? pipeline = GetScopePipeline(metadata: null, this); - if (pipeline is null || !pipeline.IsEnabled) - { - return null; - } - return pipeline.HandleScope(ref state); + ScopeHandler handler = VersionedState.GetScopeHandler(null, out bool _); + return handler.HandleBeginScope(ref state); } internal static void ThrowLoggingError(List exceptions) @@ -67,6 +46,8 @@ internal static void ThrowLoggingError(List exceptions) throw new AggregateException( message: "An error occurred while writing to logger(s).", innerExceptions: exceptions); } + + public ProcessorContext GetProcessor() => new ProcessorContext(VersionedState, VersionedState.CancelToken); } internal sealed class Scope : IDisposable @@ -122,8 +103,44 @@ public void Dispose() } } - internal sealed class VersionedLoggerState + internal sealed class VersionedLoggerState : ILogEntryProcessor, IDisposable { + internal readonly struct HandlerKey + { + public HandlerKey(bool isLoggingHandler, object typeOrMetadata) + { + IsLoggingPipeline = isLoggingHandler; + TypeOrMetadata = typeOrMetadata; + } + + public bool IsLoggingPipeline { get; } + public object TypeOrMetadata { get; } + } + + internal readonly struct LogEntryHandlerState + { + public LogEntryHandlerState(LogEntryHandler handler, bool enabled, bool dynamicEnableCheckRequired) + { + Handler = handler; + IsEnabled = enabled; + IsDynamicEnableCheckRequired = dynamicEnableCheckRequired; + } + public LogEntryHandler Handler { get; } + public bool IsEnabled { get; } + public bool IsDynamicEnableCheckRequired { get; } + } + + internal readonly struct ScopeHandlerState where TState : notnull + { + public ScopeHandlerState(ScopeHandler handler, bool enabled) + { + Handler = handler; + IsEnabled = enabled; + } + public ScopeHandler Handler { get; } + public bool IsEnabled { get; } + } + public static readonly VersionedLoggerState Default = new VersionedLoggerState(); public VersionedLoggerState() @@ -138,75 +155,85 @@ public VersionedLoggerState(LoggerInformation[] loggers, ILogEntryProcessor proc Loggers = loggers; Processor = processor; FilterOptions = filterOptions; - _isUpToDate = true; + _cancelSource = new CancellationTokenSource(); } - private bool _isUpToDate; + private CancellationTokenSource _cancelSource = new CancellationTokenSource(); + public CancellationToken CancelToken => _cancelSource.Token; public LoggerInformation[] Loggers { get; } - public ILogEntryProcessor Processor { get; } public LoggerFilterOptions FilterOptions { get; } - public Dictionary Pipelines { get; } = new Dictionary(); + private ILogEntryProcessor Processor { get; } + + private Dictionary Handlers { get; } = new Dictionary(); - public LogEntryPipeline? GetLoggingPipeline(ILogMetadata? metadata, object? userState) + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnableCheckRequired) { // The default versioned state should never be used to create pipelines, it is a shared singleton // that exists just to satisfy nullability checks Debug.Assert(this != Default); - Pipeline? pipeline; - PipelineKey key = new PipelineKey(isLoggingPipeline: true, (metadata == null) ? typeof(TState) : metadata, terminalProcessor: null, userState: userState); - lock (Pipelines) + object? handlerObject; + LogEntryHandlerState handlerState; + HandlerKey key = new HandlerKey(isLoggingHandler: true, (metadata == null) ? typeof(TState) : metadata); + lock (Handlers) { - if (!Pipelines.TryGetValue(key, out pipeline)) + if (!Handlers.TryGetValue(key, out handlerObject)) { - LogEntryHandler handler = Processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicCheckRequired); - pipeline = new LogEntryPipeline(handler, userState, enabled, dynamicCheckRequired); - // in a multi-threaded race it is possible to create new pipelines after the versioned state is already disposed - // if this happens the pipeline is immediately marked as being not up-to-date. - pipeline.IsUpToDate = _isUpToDate; - Pipelines[key] = pipeline; + LogEntryHandler handler = Processor.GetLogEntryHandler(metadata, out enabled, out dynamicEnableCheckRequired); + handlerState = new LogEntryHandlerState(handler, enabled, dynamicEnableCheckRequired); + Handlers[key] = handlerState; + } + else + { + handlerState = (LogEntryHandlerState)handlerObject; } } - return (LogEntryPipeline?)pipeline; + enabled = handlerState.IsEnabled; + dynamicEnableCheckRequired = handlerState.IsDynamicEnableCheckRequired; + return handlerState.Handler; } - public ScopePipeline? GetScopePipeline(ILogMetadata? metadata, object? userState) where TState : notnull + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull { // The default versioned state should never be used to create pipelines, it is a shared singleton // that exists just to satisfy nullability checks Debug.Assert(this != Default); - if (!FilterOptions.CaptureScopes) - { - return null; - } - - Pipeline? pipeline; - PipelineKey key = new PipelineKey(isLoggingPipeline: false, (metadata == null) ? typeof(TState) : metadata, terminalProcessor: null, userState: userState); - lock (Pipelines) + object? handlerObject; + ScopeHandlerState handlerState; + HandlerKey key = new HandlerKey(isLoggingHandler: true, (metadata == null) ? typeof(TState) : metadata); + lock (Handlers) { - if (!Pipelines.TryGetValue(key, out pipeline)) + if (!Handlers.TryGetValue(key, out handlerObject)) { - ScopeHandler handler = Processor.GetScopeHandler(metadata, out bool enabled); - pipeline = new ScopePipeline(handler, userState, enabled); - // in a multi-threaded race it is possible to create new pipelines after the versioned state is already disposed - // if this happens the pipeline is immediately marked as being not up-to-date. - pipeline.IsUpToDate = _isUpToDate; - Pipelines[key] = pipeline; + if (FilterOptions.CaptureScopes) + { + ScopeHandler handler = Processor.GetScopeHandler(metadata, out enabled); + handlerState = new ScopeHandlerState(handler, enabled); + } + else + { + handlerState = new ScopeHandlerState(new NullScopeHandler(), false); + } + Handlers[key] = handlerState; + } + else + { + handlerState = (ScopeHandlerState)handlerObject; } } - return (ScopePipeline?)pipeline; + enabled = handlerState.IsEnabled; + return handlerState.Handler; } - public void MarkNotUpToDate() + public bool IsEnabled(LogLevel logLevel) => Processor.IsEnabled(logLevel); + public void Dispose() { - lock (Pipelines) + // the default versioned state is never disposed + if (this != Default) { - _isUpToDate = false; - foreach (Pipeline pipeline in Pipelines.Values) - { - pipeline.IsUpToDate = false; - } + _cancelSource.Cancel(); + _cancelSource.Dispose(); } } @@ -229,5 +256,11 @@ public bool IsEnabled(LogLevel logLevel) return false; } } + + private sealed class NullScopeHandler : ScopeHandler where TState : notnull + { + public override IDisposable? HandleBeginScope(ref TState state) => null; + public override bool IsEnabled(LogLevel level) => false; + } } } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs index 72c69cde9ede57..2d414e70429cc8 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs @@ -178,11 +178,11 @@ private void UpdateLogger(Logger logger, LoggerInformation[] loggerInfos) { Debug.Assert(Monitor.IsEntered(_sync)); VersionedLoggerState oldState = logger.VersionedState; - // Even though we set new versioned state before disposing the old one keep in mind that + // Even though we set new versioned state before disposing the old one, keep in mind that // it is still possible for a concurrent operation to capture the versioned state before // it was replaced and then use it after it was disposed. logger.VersionedState = new VersionedLoggerState(loggerInfos, GetProcessor(loggerInfos), _filterOptions); - oldState.MarkNotUpToDate(); + oldState.Dispose(); } private ILogEntryProcessor GetProcessor(LoggerInformation[] loggerInfos) diff --git a/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs b/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs deleted file mode 100644 index 355e4d5bdc9319..00000000000000 --- a/src/libraries/Microsoft.Extensions.Logging/src/PipelineManager.cs +++ /dev/null @@ -1,49 +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.Generic; - -namespace Microsoft.Extensions.Logging -{ - internal sealed class PipelineManager - { - private readonly Dictionary _pipelines = new Dictionary(); - - public LogEntryPipeline? GetLoggingPipeline(ILogEntryProcessor processor, ILogMetadata? metadata, object? userState) - { - object? pipeline; - PipelineKey key = new PipelineKey(isLoggingPipeline: true, (metadata == null) ? typeof(TState) : metadata, processor, userState); - lock (_pipelines) - { - if (!_pipelines.TryGetValue(key, out pipeline)) - { - pipeline = BuildPipeline(processor, metadata, userState); - _pipelines[key] = pipeline; - } - } - return (LogEntryPipeline?)pipeline; - } - - private static LogEntryPipeline BuildPipeline(ILogEntryProcessor processor, ILogMetadata? metadata, object? userState) - { - LogEntryHandler handler = processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicCheckRequired); - return new LogEntryPipeline(handler, userState, enabled, dynamicCheckRequired); - } - } - - internal readonly struct PipelineKey - { - public PipelineKey(bool isLoggingPipeline, object typeOrMetadata, ILogEntryProcessor? terminalProcessor, object? userState) - { - IsLoggingPipeline = isLoggingPipeline; - TypeOrMetadata = typeOrMetadata; - TerminalProcessor = terminalProcessor; - UserState = userState; - } - - public bool IsLoggingPipeline { get; } - public object TypeOrMetadata { get; } - public ILogEntryProcessor? TerminalProcessor { get; } - public object? UserState { get; } - } -} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/LoggerMessageTest.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/LoggerMessageTest.cs index a47ccc154fc6c2..f09bc5f9242b22 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/LoggerMessageTest.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/LoggerMessageTest.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Logging.Tests; using Xunit; using Xunit.Sdk; @@ -194,6 +195,51 @@ public void LogMessages(Delegate messageDelegate, int argumentCount) Assert.Equal(2, testLogger.IsEnabledCallCount); } + + // LoggerMessage.Define should preferentially log via the ILogEntryProcessor APIs when ILogger + // implements ILogEntryProcessorFactory + // + // TODO: this test fails everywhere except two parameters because the other LoggerMessage.Define() APIs + // haven't been switched over to use processor API yet. I think most of the work is implementing the + // concrete metadata for 0-6 arg variations, potentially refactoring so they can share more. + // TODO: Test that LoggerMessage.Define(metadata) invokes processor APIs when present + // TODO: Test that Logger invokes processor APIs on the wrapped ILogger if present + // TODO: Test that ILogger.Log() extension methods invoke processor APIs on the ILogger if present + // TODO: Test that Logger invokes the ILogger sinks with processor APIs if present + // TODO: Test all the paths also use the processor APIs when present for scopes. + [Theory] + [MemberData(nameof(LogMessagesData))] + public void LogMessages_UsesLoggerProcessorWhenAvailable(Delegate messageDelegate, int argumentCount) + { + // Arrange + var loggerProcessor = new RecordingProcessor(); + var testLogger = new TestLoggerWithProcessor(loggerProcessor); + var exception = new Exception("TestException"); + var parameterNames = Enumerable.Range(0, argumentCount).Select(i => "P" + i).ToArray(); + var parameters = new List(); + parameters.Add(testLogger); + parameters.AddRange(parameterNames); + parameters.Add(exception); + + var expectedFormat = "Log " + string.Join(" ", parameterNames.Select(p => "{" + p + "}")); + var expectedToString = "Log " + string.Join(" ", parameterNames); + var expectedValues = parameterNames.Select(p => new KeyValuePair(p, p)).ToList(); + expectedValues.Add(new KeyValuePair("{OriginalFormat}", expectedFormat)); + + // Act + messageDelegate.DynamicInvoke(parameters.ToArray()); + + // Assert + Assert.Equal(expectedFormat, loggerProcessor.MetadataOriginalFormat); + for(int i = 0; i < parameterNames.Length; i++) + { + Assert.Equal(parameterNames[i], loggerProcessor.MetadataLogProperties[i].Name); + } + var actualLogValues = Assert.IsAssignableFrom>>(loggerProcessor.State); + AssertLogValues(expectedValues, actualLogValues.ToList()); + Assert.Equal(expectedToString, actualLogValues.ToString()); + } + [Theory] [MemberData(nameof(LogMessagesDataSkipEnabledCheck))] public void LogMessagesSkipEnabledCheck(Delegate messageDelegate, int argumentCount) diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLoggerWithProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLoggerWithProcessor.cs new file mode 100644 index 00000000000000..a6321d5f32fea6 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/TestLoggerWithProcessor.cs @@ -0,0 +1,95 @@ +// 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.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Castle.DynamicProxy.Generators.Emitters.SimpleAST; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Microsoft.Extensions.Logging.Tests +{ + internal class TestLoggerWithProcessor : ILogger, ILogEntryProcessorFactory + { + public TestLoggerWithProcessor(ILogEntryProcessor processor) + { + CurrentProcessor = processor; + } + public ILogEntryProcessor CurrentProcessor { get; set; } + + + public ProcessorContext GetProcessor() => new ProcessorContext(CurrentProcessor, CancellationToken.None); + + public IDisposable? BeginScope(TState state) where TState : notnull => + throw new InvalidOperationException("Expected test to use processor APIs"); + + public bool IsEnabled(LogLevel logLevel) => + throw new InvalidOperationException("Expected test to use processor APIs"); + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => + throw new InvalidOperationException("Expected test to use processor APIs"); + } + + class RecordingProcessor : ILogEntryProcessor + { + public string MetadataOriginalFormat { get; set; } + public List MetadataLogProperties { get; set; } + public object State { get; set; } + + public LogEntryHandler GetLogEntryHandler(ILogMetadata? metadata, out bool enabled, out bool dynamicEnabledCheckRequired) + { + RecordMetadata(metadata); + enabled = true; + dynamicEnabledCheckRequired = false; + return new RecordingLogEntryHandler(this); + } + + public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull + { + RecordMetadata(metadata); + enabled = true; + return new RecordingScopeHandler(this); + } + + public bool IsEnabled(LogLevel logLevel) => true; + + private void RecordMetadata(ILogMetadata metadata) + { + MetadataOriginalFormat = metadata.OriginalFormat; + MetadataLogProperties = Enumerable.Range(0, metadata.PropertyCount).Select(metadata.GetPropertyInfo).ToList(); + } + } + + internal class RecordingLogEntryHandler : LogEntryHandler + { + RecordingProcessor _processor; + public RecordingLogEntryHandler(RecordingProcessor processor) + { + _processor = processor; + } + public override void HandleLogEntry(ref LogEntry logEntry) + { + _processor.State = logEntry.State; + } + public override bool IsEnabled(LogLevel level) => true; + } + + internal class RecordingScopeHandler : ScopeHandler + { + RecordingProcessor _processor; + public RecordingScopeHandler(RecordingProcessor processor) + { + _processor = processor; + } + public override IDisposable? HandleBeginScope(ref TState state) + { + _processor.State = state; + return null; + } + public override bool IsEnabled(LogLevel level) => true; + } + +} From 30fc5231947a0c2dada66661a862413a4ca3c665 Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Wed, 14 Jun 2023 01:39:48 -0700 Subject: [PATCH 23/26] Simplifying public API and cutting enrichment - Refactored to remove BufferWriter - Extracted all formatting work outside of ILogMetadata to extension methods. - Added a new CreatePropertyListVisitor as the primitive all high performance serialization can be layered over. - Removed all the virtual override based formatting logic and converged on IPropertyVisitorFactory to produce delegates. - Continued refactoring away from LogValuesFormatter towards ILogMetadata. --- ...crosoft.Extensions.Logging.Abstractions.cs | 50 +-- .../src/BufferWriter.cs | 124 ------ .../src/FormattedLogValues.cs | 75 +++- .../src/FormattingState.cs | 364 ++++++++++++++++++ .../src/LogEntryNew.cs | 31 +- .../src/LogMetadataExtensions.cs | 89 +++++ .../src/LogValuesFormatter.cs | 298 +------------- .../src/LoggerMessage.cs | 106 +---- .../src/MessageFormatHelper.cs | 157 ++++++++ .../src/PooledBufferWriter.cs | 239 ++++++++++++ .../ref/Microsoft.Extensions.Logging.cs | 2 + .../src/DispatchProcessor.cs | 8 +- .../src/EnrichmentExtensions.cs | 3 +- .../tests/Common/EnrichmentTests.cs | 3 +- .../tests/Common/FormattedLogValuesTest.cs | 8 +- .../tests/Common/MessageFormattingTest.cs | 146 +++++++ .../Common/Redaction/RedactionProcessor.cs | 351 +++++------------ 17 files changed, 1207 insertions(+), 847 deletions(-) delete mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferWriter.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattingState.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogMetadataExtensions.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/MessageFormatHelper.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/PooledBufferWriter.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/MessageFormattingTest.cs diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index 7727585c9b49e0..8ec6fc0e6c45ca 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -6,18 +6,6 @@ namespace Microsoft.Extensions.Logging { - public ref partial struct BufferWriter - { - private object _dummy; - private int _dummyPrimitive; - public BufferWriter(System.Buffers.IBufferWriter bufferWriter) { throw null; } - public System.Span CurrentSpan { get { throw null; } } - public System.Buffers.IBufferWriter Writer { get { throw null; } } - public void Advance(int len) { } - public void EnsureSize(int minSize) { } - public void Flush() { } - public void Grow(int minSize) { } - } public readonly partial struct EventId : System.IEquatable { private readonly object _dummy; @@ -33,9 +21,15 @@ public void Grow(int minSize) { } public static bool operator !=(Microsoft.Extensions.Logging.EventId left, Microsoft.Extensions.Logging.EventId right) { throw null; } public override string ToString() { throw null; } } - public delegate void FormatPropertyAction(PropType propertyValue, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); - public delegate void FormatPropertyListAction(in TState state, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); - public delegate void FormatSpanPropertyAction(System.ReadOnlySpan propertyValue, ref Microsoft.Extensions.Logging.BufferWriter bufferWriter); + public delegate void VisitPropertyListAction(ref TState state, ref System.Span spanCookie, ref TCookie cookie); + public delegate void VisitPropertyAction(int propIndex, PropType propValue, ref System.Span spanCookie, ref TCookie cookie); + public delegate void VisitSpanPropertyAction(int propIndex, scoped System.ReadOnlySpan propValue, ref System.Span spanCookie, ref TCookie cookie); + + public interface IPropertyVisitorFactory + { + VisitPropertyAction GetPropertyVisitor(); + VisitSpanPropertyAction GetSpanPropertyVisitor(); + } public partial interface IExternalScopeProvider { void ForEachScope(System.Action callback, TState state); @@ -75,21 +69,13 @@ public partial interface ILogMetadata Microsoft.Extensions.Logging.LogLevel LogLevel { get; } string OriginalFormat { get; } int PropertyCount { get; } - void AppendFormattedMessage(in TState state, System.Buffers.IBufferWriter buffer); - System.Action> GetMessageFormatter(Microsoft.Extensions.Logging.PropertyCustomFormatter[] customFormatters); Microsoft.Extensions.Logging.LogPropertyInfo GetPropertyInfo(int index); - Microsoft.Extensions.Logging.FormatPropertyListAction GetPropertyListFormatter(Microsoft.Extensions.Logging.IPropertyFormatterFactory propertyFormatterFactory); - System.Func GetStringMessageFormatter(); + Microsoft.Extensions.Logging.VisitPropertyListAction CreatePropertyListVisitor(Microsoft.Extensions.Logging.IPropertyVisitorFactory propertyVisitorFactory); } public partial interface IProcessorFactory { Microsoft.Extensions.Logging.ILogEntryProcessor GetProcessor(Microsoft.Extensions.Logging.ILogEntryProcessor nextProcessor); } - public partial interface IPropertyFormatterFactory - { - Microsoft.Extensions.Logging.FormatPropertyAction GetPropertyFormatter(int propertyIndex, Microsoft.Extensions.Logging.LogPropertyInfo metadata); - Microsoft.Extensions.Logging.FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, Microsoft.Extensions.Logging.LogPropertyInfo metadata); - } public partial interface ISupportExternalScope { void SetScopeProvider(Microsoft.Extensions.Logging.IExternalScopeProvider scopeProvider); @@ -225,20 +211,20 @@ public partial class ProcessorFactory : Microsoft.Extensions.Logging.IProcess public ProcessorFactory(System.Func getProcessor) { } public Microsoft.Extensions.Logging.ILogEntryProcessor GetProcessor(Microsoft.Extensions.Logging.ILogEntryProcessor nextProcessor) { throw null; } } - public abstract partial class PropertyCustomFormatter - { - protected PropertyCustomFormatter() { } - public virtual void AppendFormatted(int index, int value, System.Buffers.IBufferWriter buffer) { } - public virtual void AppendFormatted(int index, System.ReadOnlySpan value, System.Buffers.IBufferWriter buffer) { } - public virtual void AppendFormatted(int index, string value, System.Buffers.IBufferWriter buffer) { } - public abstract void AppendFormatted(int index, T value, System.Buffers.IBufferWriter buffer); - } public abstract partial class ScopeHandler where TState : notnull { protected ScopeHandler() { } public abstract System.IDisposable? HandleBeginScope(ref TState state); public abstract bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level); } + + public delegate void FormatLogMessage(ref TState state, System.Buffers.IBufferWriter bufferWriter); + + public static partial class LogMetadataExtensions + { + public static FormatLogMessage CreateMessageFormatter(this ILogMetadata metadata) { throw null; } + public static System.Func CreateStringMessageFormatter(this ILogMetadata metadata) { throw null; } + } } namespace Microsoft.Extensions.Logging.Abstractions { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferWriter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferWriter.cs deleted file mode 100644 index 4e0ebb0f0fd477..00000000000000 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferWriter.cs +++ /dev/null @@ -1,124 +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 System.Buffers; - -namespace Microsoft.Extensions.Logging -{ - public ref struct BufferWriter - { - private Span _currentSpan; - private IBufferWriter _writer; - private int _allocated; - - public BufferWriter(IBufferWriter bufferWriter) - { - _writer = bufferWriter; - _currentSpan = bufferWriter.GetSpan(); - _allocated = _currentSpan.Length; - } - - public IBufferWriter Writer => _writer; - public Span CurrentSpan => _currentSpan; - - public void Advance(int len) - { - _currentSpan = _currentSpan.Slice(len); - } - - public void EnsureSize(int minSize) - { - if (_currentSpan.Length < minSize) - { - Grow(minSize); - } - } - - public void Grow(int minSize) - { - if (_allocated != _currentSpan.Length) - { - Flush(); - } - _currentSpan = _writer.GetSpan(minSize); - _allocated = _currentSpan.Length; - } - - public void Flush() - { - _writer.Advance(_allocated - _currentSpan.Length); - _currentSpan = default; - _allocated = 0; - } - } - - internal static class BufferWriterExtensions - { - public static void Write(ref this BufferWriter writer, ReadOnlySpan value) - { - if (!value.TryCopyTo(writer.CurrentSpan)) - { - writer.Grow(value.Length); - value.CopyTo(writer.CurrentSpan); - } - writer.Advance(value.Length); - } - - public static void Write(ref this BufferWriter writer, ReadOnlySpan value, int alignment) - { - int valueLen = value.Length; - bool leftAlign = false; - if (alignment < 0) - { - leftAlign = true; - alignment = -alignment; - } - int lenNeeded = Math.Max(alignment, valueLen); - int paddingNeeded = lenNeeded - valueLen; - if (writer.CurrentSpan.Length < lenNeeded) - { - writer.Grow(lenNeeded); - } - Span currentSpan = writer.CurrentSpan; - if (leftAlign) - { - currentSpan.Slice(0, paddingNeeded).Fill(' '); - value.CopyTo(currentSpan.Slice(paddingNeeded)); - } - else - { - value.CopyTo(currentSpan.Slice(paddingNeeded)); - currentSpan.Slice(valueLen, paddingNeeded).Fill(' '); - } - writer.Advance(lenNeeded); - } - - /* - public void Append(int value) - { - if (_unusedChars.Length < 20) - { - Grow(20); - } - value.TryFormat(_unusedChars, out int written); - _unusedChars = _unusedChars.Slice(written); - } - - public void Append(T value) - { - if (value is string strVal) - { - Append(strVal); - } - else if(value is int intVal) - { - Append(intVal); - } - else if(value != null) - { - Append(value.ToString()); - } - }*/ - } -} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs index 037ec63bdcd3ee..8ec7402379488a 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs @@ -6,6 +6,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Logging { @@ -19,14 +20,14 @@ namespace Microsoft.Extensions.Logging private const string NullFormat = "[null]"; private static int s_count; - private static readonly ConcurrentDictionary s_formatters = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary s_formatters = new ConcurrentDictionary(); - private readonly LogValuesFormatter? _formatter; + private readonly FormattedLogValuesMetadata? _metadata; private readonly object?[]? _values; private readonly string _originalMessage; // for testing purposes - internal LogValuesFormatter? Formatter => _formatter; + internal FormattedLogValuesMetadata? Metadata => _metadata; public FormattedLogValues(string? format, params object?[]? values) { @@ -34,23 +35,23 @@ public FormattedLogValues(string? format, params object?[]? values) { if (s_count >= MaxCachedFormatters) { - if (!s_formatters.TryGetValue(format, out _formatter)) + if (!s_formatters.TryGetValue(format, out _metadata)) { - _formatter = new LogValuesFormatter(format); + _metadata = new FormattedLogValuesMetadata(format); } } else { - _formatter = s_formatters.GetOrAdd(format, f => + _metadata = s_formatters.GetOrAdd(format, f => { Interlocked.Increment(ref s_count); - return new LogValuesFormatter(f); + return new FormattedLogValuesMetadata(f); }); } } else { - _formatter = null; + _metadata = null; } _originalMessage = format ?? NullFormat; @@ -71,7 +72,7 @@ public FormattedLogValues(string? format, params object?[]? values) return new KeyValuePair ("{OriginalFormat}", _originalMessage); } - return _formatter!.GetValue(_values!, index); + return new KeyValuePair(_metadata!.GetPropertyInfo(index).Name, _values![index]); } } @@ -79,12 +80,12 @@ public int Count { get { - if (_formatter == null) + if (_metadata == null) { return 1; } - return _formatter.PropertyCount + 1; + return _metadata.PropertyCount + 1; } } @@ -96,14 +97,19 @@ public int Count } } + public object?[]? Values => _values; + public override string ToString() { - if (_formatter == null) + if (_metadata == null) { return _originalMessage; } - return _formatter.Format(_values); + // this could be done a little more efficiently by caching CompositeFormat parsed earlier + // creating a FormattingState and directly passing the values. It would avoid allocating + // the delegate and the delegate closure. + return _metadata.Formatter(this, null); } IEnumerator IEnumerable.GetEnumerator() @@ -111,4 +117,47 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } + + internal class FormattedLogValuesMetadata : ILogMetadata + { + private LogPropertyInfo[] _propertyInfo; + private Func? _formatter; + public FormattedLogValuesMetadata(string originalFormat) + { + OriginalFormat = originalFormat; + MessageFormatHelper.Parse(originalFormat, out _propertyInfo); + } + + public LogLevel LogLevel => throw new NotImplementedException(); + public EventId EventId => throw new NotImplementedException(); + public string OriginalFormat { get; private set; } + public int PropertyCount => _propertyInfo != null ? _propertyInfo.Length : 0; + public LogPropertyInfo GetPropertyInfo(int index) => _propertyInfo[index]; + public VisitPropertyListAction CreatePropertyListVisitor(IPropertyVisitorFactory propertyVisitorFactory) + { + VisitPropertyAction visitProperty = propertyVisitorFactory.GetPropertyVisitor(); + return VisitProperties; + + void VisitProperties(ref FormattedLogValues flv, ref Span spanCookie, ref TCookie cookie) + { + object?[]? values = flv.Values; + if(values != null) + { + for(int i = 0; i < values.Length;i++) + { + visitProperty(i, values[i], ref spanCookie, ref cookie); + } + } + } + } + + internal Func Formatter + { + get + { + _formatter ??= this.CreateStringMessageFormatter(); + return _formatter; + } + } + } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattingState.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattingState.cs new file mode 100644 index 00000000000000..80cc339536cf41 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattingState.cs @@ -0,0 +1,364 @@ +// 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.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging +{ + /// + /// This is a state machine that produces a formatted string. It is used as part of a solution for high-performance + /// and low/no allocation ILogger message formatting. + /// It is similar to the DefaultInterpolatedStringHandler with a few design differences: + /// 1. The outputted format matches the pre-existing behavior for ILogger.Log extension methods. This is mostly + /// string.Format() behavior but has special cases for formatting null values and arrays. + /// 2. There is no support for IFormatProvider or ICustomFormatter. + /// 3. The memory backing the formatted output comes from an IBufferWriter rather than a rented array. + /// 4. This type isn't a ref struct. It requires the caller to manage the span parameter by repeatedly passing it in. + /// (This type needs to get passed as a generic parameter and ref structs can't be passed that way today) + /// 5. FormattingState knows the CompositeFormat for the string that is being formatted and tracks the current + /// parameter index internally whereas DefaultInterpolatedStringHandler expects the caller to keep track. + /// 6. Related to (5), the caller only calls to provide the format hole arguments whereas DefaultInterpolatedStringHandler + /// uses calls to provide all the literals as well. + /// + /// + /// Usage: + /// 1. Construct with a CompositeFormat and an IBufferWriter that receives the formatted data + /// 2. Call Init() to get an initial Span. A ref to this span will need to passed into each subsequent call. + /// 3. In order from left to right for each hole in the format string call AppendPropertyUtf16 or AppendSpanPropertyUtf16 + /// to provide the value that fills that hole. + /// 4. Call Finish() to flush + /// + /// In the future I imagine we might want to add Encoding support for UTF8, but for now it is UTF16 only. If a particular logging + /// sink needed high performance UTF8 formatting this code could always be replicated external to M.E.L.Abstractions. + /// + internal struct FormattingState + { + private InternalCompositeFormat _format; + private int _index; + private int _allocated; + private IBufferWriter _writer; + + private const string NullValue = "(null)"; + private const int GuessedLengthPerHole = 11; + private const int MinimumArrayPoolLength = 256; + + public FormattingState(InternalCompositeFormat format, IBufferWriter writer) + { + _format = format; + _writer = writer; + } + + public void Init(out Span span) + { + int estimatedSize = Math.Max(MinimumArrayPoolLength, _format._literalLength + (_format._formattedCount * GuessedLengthPerHole)); + span = _writer.GetSpan(estimatedSize); + _allocated = span.Length; + } + + public void AppendPropertyUtf16(ref Span span, TProp propertyValue) + { + // These range checks wouldn't be needed if we detect argument<->hole mismatches upfront + if (_index == _format._segments.Length) + { + return; + } + string? literal = _format._segments[_index].Literal; + if (literal != null) + { + AppendStringUtf16(ref span, literal); + _index++; + if (_index == _format._segments.Length) + { + return; + } + } + (_, _, int alignment, string? format) = _format._segments[_index]; + _index++; + + if (propertyValue is string stringVal) + { + AppendStringPropertyUtf16(ref span, stringVal, alignment); + } + else + { + AppendNonStringPropertyUtf16(ref span, propertyValue, alignment, format); + } + } + + public void AppendSpanPropertyUtf16(ref Span writeBuffer, scoped ReadOnlySpan propertyValue) + { + // These range checks wouldn't be needed if we detect argument<->hole mismatches upfront + if (_index == _format._segments.Length) + { + return; + } + string? literal = _format._segments[_index].Literal; + if (literal != null) + { + AppendStringUtf16(ref writeBuffer, literal); + _index++; + if (_index == _format._segments.Length) + { + return; + } + } + int alignment = _format._segments[_index].Alignment; + _index++; + if (alignment == 0) + { + AppendCharsUtf16(ref writeBuffer, propertyValue); + } + else + { + AppendAlignedCharSpanUtf16(ref writeBuffer, propertyValue, alignment); + } + } + + public void Finish(ref Span span) + { + if (_index < _format._segments.Length) + { + Debug.Assert(_index == _format._segments.Length - 1); + string? literal = _format._segments[_index].Literal; + Debug.Assert(literal != null); + if (literal != null) + { + AppendStringUtf16(ref span, literal); + _index++; + } + } + Flush(ref span); + } + + private void AppendStringPropertyUtf16(ref Span span, string propertyValue, int alignment) + { + if (alignment == 0) + { + AppendStringUtf16(ref span, propertyValue); + } + else + { + AppendAlignedPropertyUtf16(ref span, propertyValue, alignment, null); + } + } + + private void AppendNonStringPropertyUtf16(ref Span span, TProp propertyValue, int alignment, string? format) + { + if (alignment == 0) + { +#if NET6_0_OR_GREATER + if (propertyValue is ISpanFormattable) + { + EnsureSize(ref span, 64); + if (((ISpanFormattable)propertyValue).TryFormat(MemoryMarshal.Cast(span), out int charsWritten, format, CultureInfo.InvariantCulture)) + { + Advance(ref span, charsWritten * 2); + return; + } + } +#endif + AppendStringUtf16(ref span, FormatPropertyValue(propertyValue, format)); + } + else + { + AppendAlignedPropertyUtf16(ref span, propertyValue, alignment, format); + } + } + + private void AppendAlignedCharSpanUtf16(ref Span writeBuffer, scoped ReadOnlySpan value, int alignment) + { + bool leftAlign = false; + int paddingNeeded; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + paddingNeeded = alignment - value.Length; + if (paddingNeeded <= 0) + { + EnsureSize(ref writeBuffer, 2 * value.Length); + Span charWriteBuffer = MemoryMarshal.Cast(writeBuffer); + value.CopyTo(charWriteBuffer); + Advance(ref writeBuffer, 2 * value.Length); + return; + } + + EnsureSize(ref writeBuffer, 2 * alignment); + Span charWriteBuffer2 = MemoryMarshal.Cast(writeBuffer); + if (leftAlign) + { + value.CopyTo(charWriteBuffer2); + charWriteBuffer2.Slice(value.Length, paddingNeeded).Fill(' '); + } + else + { + charWriteBuffer2.Slice(0, paddingNeeded).Fill(' '); + value.CopyTo(charWriteBuffer2.Slice(paddingNeeded)); + } + Advance(ref writeBuffer, 2 * alignment); + } + + private void AppendAlignedPropertyUtf16(ref Span span, TProp value, int alignment, string? format) + { + bool leftAlign = false; + int paddingNeeded; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } +#if NET6_0_OR_GREATER + if (value is ISpanFormattable) + { + EnsureSize(ref span, 2 * Math.Max(32, alignment)); + Span charSpan = MemoryMarshal.Cast(span); + if (((ISpanFormattable)value).TryFormat(charSpan, out int charsWritten, format, CultureInfo.InvariantCulture)) + { + paddingNeeded = alignment - charsWritten; + if (paddingNeeded <= 0) + { + Advance(ref span, 2 * charsWritten); + return; + } + if (leftAlign) + { + charSpan.Slice(charsWritten, paddingNeeded).Fill(' '); + } + else + { + charSpan.Slice(0, charsWritten).CopyTo(charSpan.Slice(paddingNeeded)); + charSpan.Slice(0, paddingNeeded).Fill(' '); + } + Advance(ref span, 2 * alignment); + return; + } + } +#endif + + string unpadded = FormatPropertyValue(value, format); + paddingNeeded = alignment - unpadded.Length; + EnsureSize(ref span, 2 * Math.Max(unpadded.Length, alignment)); + if (paddingNeeded <= 0) + { + AppendStringUtf16(ref span, unpadded); + return; + } + Span charSpan2 = MemoryMarshal.Cast(span); + if (leftAlign) + { + unpadded.AsSpan().CopyTo(charSpan2); + charSpan2.Slice(unpadded.Length, paddingNeeded).Fill(' '); + } + else + { + charSpan2.Slice(0, paddingNeeded).Fill(' '); + unpadded.AsSpan().CopyTo(charSpan2.Slice(paddingNeeded)); + } + Advance(ref span, 2 * alignment); + } + + private static string FormatPropertyValue(T value, string? format) + { + string? s; + if (value is IFormattable) + { + s = ((IFormattable)value).ToString(format, null); + } + else if (value is not string && value is IEnumerable enumerable) + { + var vsb = new ValueStringBuilder(stackalloc char[256]); + bool first = true; + foreach (object? e in enumerable) + { + if (!first) + { + vsb.Append(", "); + } + + vsb.Append(e != null ? e.ToString() : NullValue); + first = false; + } + return vsb.ToString(); + } + else + { + s = value?.ToString(); + } + if (s == null) + { + return NullValue; + } + else + { + return s; + } + } + + private void AppendStringUtf16(ref Span span, string value) + { + AppendBytes(ref span, MemoryMarshal.AsBytes(value.AsSpan())); + } + + private void AppendCharsUtf16(ref Span writeBuffer, scoped ReadOnlySpan value) + { + Span charWriteBuffer = MemoryMarshal.Cast(writeBuffer); + if (!value.TryCopyTo(charWriteBuffer)) + { + Grow(ref writeBuffer, 2 * value.Length); + charWriteBuffer = MemoryMarshal.Cast(writeBuffer); + value.CopyTo(charWriteBuffer); + } + Advance(ref writeBuffer, 2 * value.Length); + } + + private void AppendBytes(ref Span span, ReadOnlySpan value) + { + if (!value.TryCopyTo(span)) + { + Grow(ref span, value.Length); + value.CopyTo(span); + } + Advance(ref span, value.Length); + } + + private static void Advance(ref Span span, int len) + { + span = span.Slice(len); + } + + private void EnsureSize(ref Span span, int minSize) + { + if (span.Length < minSize) + { + Grow(ref span, minSize); + } + } + + private void Grow(ref Span span, int minSize) + { + if (_allocated != span.Length) + { + Flush(ref span); + } + span = _writer.GetSpan(minSize); + _allocated = span.Length; + } + + private void Flush(ref Span span) + { + _writer.Advance(_allocated - span.Length); + span = default; + _allocated = 0; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index 1199146df5800e..74edb6a7071942 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Buffers; using System.Threading; using Microsoft.Extensions.Logging.Abstractions; @@ -65,13 +64,13 @@ public abstract class ScopeHandler where TState : notnull public struct LogPropertyInfo { - public LogPropertyInfo(string name, object[]? metadata) + public LogPropertyInfo(string name, object[]? metadata = null) { Name = name; Metadata = metadata; } public string Name { get; } - public object[]? Metadata { get; } + public object[]? Metadata { get; internal set; } } public interface ILogMetadata @@ -81,28 +80,16 @@ public interface ILogMetadata string OriginalFormat { get; } int PropertyCount { get; } LogPropertyInfo GetPropertyInfo(int index); - void AppendFormattedMessage(in TState state, IBufferWriter buffer); - Action> GetMessageFormatter(PropertyCustomFormatter[] customFormatters); - FormatPropertyListAction GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory); - Func GetStringMessageFormatter(); + VisitPropertyListAction CreatePropertyListVisitor(IPropertyVisitorFactory propertyVisitorFactory); } - public delegate void FormatPropertyListAction(in TState state, ref BufferWriter bufferWriter); - public delegate void FormatPropertyAction(PropType propertyValue, ref BufferWriter bufferWriter); - public delegate void FormatSpanPropertyAction(scoped ReadOnlySpan propertyValue, ref BufferWriter bufferWriter); + public delegate void VisitPropertyListAction(ref TState state, ref Span spanCookie, ref TCookie cookie); + public delegate void VisitPropertyAction(int propIndex, PropType propValue, ref Span spanCookie, ref TCookie cookie); + public delegate void VisitSpanPropertyAction(int propIndex, scoped ReadOnlySpan propValue, ref Span spanCookie, ref TCookie cookie); - public interface IPropertyFormatterFactory + public interface IPropertyVisitorFactory { - FormatPropertyAction GetPropertyFormatter(int propertyIndex, LogPropertyInfo metadata); - FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, LogPropertyInfo metadata); - } - - public abstract class PropertyCustomFormatter - { - //TODO: we can expand this with overrides for other commonly logged value types - public virtual void AppendFormatted(int index, ReadOnlySpan value, IBufferWriter buffer) => AppendFormatted(index, value.ToString(), buffer); - public virtual void AppendFormatted(int index, int value, IBufferWriter buffer) => AppendFormatted(index, value, buffer); - public virtual void AppendFormatted(int index, string value, IBufferWriter buffer) => AppendFormatted(index, value, buffer); - public abstract void AppendFormatted(int index, T value, IBufferWriter buffer); + VisitPropertyAction GetPropertyVisitor(); + VisitSpanPropertyAction GetSpanPropertyVisitor(); } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogMetadataExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogMetadataExtensions.cs new file mode 100644 index 00000000000000..3a7e8af91fa421 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogMetadataExtensions.cs @@ -0,0 +1,89 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging +{ + + public delegate void FormatLogMessage(ref TState state, IBufferWriter bufferWriter); + + public static class LogMetadataExtensions + { + public static FormatLogMessage CreateMessageFormatter(this ILogMetadata metadata) + { + //TODO: this doesn't check if the names are out of order or mismatched in number + //That never happens for LoggerMessage metadata from source compiled loggers because the generator screens for it + //but it can happen elsewhere + InternalCompositeFormat format = MessageFormatHelper.Parse(metadata.OriginalFormat); + VisitPropertyListAction visitProperties = metadata.CreatePropertyListVisitor(new FormattingPropertyVisitorFactory()); + return FormatMessage; + + void FormatMessage(ref TState state, IBufferWriter bufferWriter) + { + FormattingState formattingState = new FormattingState(format, bufferWriter); + formattingState.Init(out Span span); + visitProperties(ref state, ref span, ref formattingState); + formattingState.Finish(ref span); + } + } + + private class Slot { public PooledByteBufferWriter? Buffer; } + private static ThreadLocal t_slot = new ThreadLocal(); + + public static Func CreateStringMessageFormatter(this ILogMetadata metadata) + { + InternalCompositeFormat format = MessageFormatHelper.Parse(metadata.OriginalFormat); + VisitPropertyListAction visitProperties = metadata.CreatePropertyListVisitor(new FormattingPropertyVisitorFactory()); + return FormatMessage; + + string FormatMessage(TState state, Exception? exception) + { + Slot? tstate = t_slot.Value; + if (tstate == null) + { + tstate = new Slot(); + t_slot.Value = tstate; + } + PooledByteBufferWriter buffer = tstate.Buffer ?? new PooledByteBufferWriter(256); + tstate.Buffer = null; + FormattingState formattingState = new FormattingState(format, buffer); + formattingState.Init(out Span span); + visitProperties(ref state, ref span, ref formattingState); + formattingState.Finish(ref span); + string ret = MemoryMarshal.Cast(buffer.WrittenMemory.Span).ToString(); + buffer.Clear(); + tstate.Buffer = buffer; + return ret; + } + } + } + + internal class FormattingPropertyVisitorFactory : IPropertyVisitorFactory + { + public VisitPropertyAction GetPropertyVisitor() + { + return VisitProperty; + + static void VisitProperty(int propIndex, PropType value, ref Span span, ref FormattingState formattingState) + { + formattingState.AppendPropertyUtf16(ref span, value); + } + } + public VisitSpanPropertyAction GetSpanPropertyVisitor() + { + return VisitSpanProperty; + static void VisitSpanProperty(int propIndex, scoped ReadOnlySpan value, ref Span span, ref FormattingState formattingState) + { + formattingState.AppendSpanPropertyUtf16(ref span, value); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs index 4120eebd5cded9..12b79484141368 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs @@ -9,6 +9,7 @@ using System.Globalization; using System.Runtime.CompilerServices; using System.Text; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Logging { @@ -31,60 +32,22 @@ public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[ internal class LogValuesFormatter { private const string NullValue = "(null)"; - private static readonly char[] FormatDelimiters = { ',', ':' }; private readonly LogPropertyInfo[]? _metadata; private readonly InternalCompositeFormat _format; - // NOTE: If this assembly ever builds for netcoreapp, the below code should change to: - // - Be annotated as [SkipLocalsInit] to avoid zero'ing the stackalloc'd char span - // - Format _valueNames.Count directly into a span - public LogValuesFormatter(string format, object[]?[]? metadata = null) { ThrowHelper.ThrowIfNull(format); OriginalFormat = format; - - var vsb = new ValueStringBuilder(stackalloc char[256]); - List propertyMetadata = new List(); - int scanIndex = 0; - int endIndex = format.Length; - - while (scanIndex < endIndex) + _format = MessageFormatHelper.Parse(format, out _metadata); + if(metadata != null && _metadata != null) { - int openBraceIndex = FindBraceIndex(format, '{', scanIndex, endIndex); - if (scanIndex == 0 && openBraceIndex == endIndex) + for (int i = 0; i < _metadata.Length; i++) { - // No holes found. - _format = InternalCompositeFormat.Parse(format); - return; - } - - int closeBraceIndex = FindBraceIndex(format, '}', openBraceIndex, endIndex); - - if (closeBraceIndex == endIndex) - { - vsb.Append(format.AsSpan(scanIndex, endIndex - scanIndex)); - scanIndex = endIndex; - } - else - { - // Format item syntax : { index[,alignment][ :formatString] }. - int formatDelimiterIndex = FindIndexOfAny(format, FormatDelimiters, openBraceIndex, closeBraceIndex); - int colonIndex = format.IndexOf(':', openBraceIndex, closeBraceIndex - openBraceIndex); - - vsb.Append(format.AsSpan(scanIndex, openBraceIndex - scanIndex + 1)); - vsb.Append(propertyMetadata.Count.ToString()); - string propName = format.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1); - vsb.Append(format.AsSpan(formatDelimiterIndex, closeBraceIndex - formatDelimiterIndex + 1)); - object[]? propMetadata = metadata != null && metadata.Length >= propertyMetadata.Count ? metadata[propertyMetadata.Count] : null; - propertyMetadata.Add(new LogPropertyInfo(propName, propMetadata)); - scanIndex = closeBraceIndex + 1; + _metadata[i].Metadata = metadata[i]; } } - - _metadata = propertyMetadata.ToArray(); - _format = InternalCompositeFormat.Parse(vsb.ToString()); } public string OriginalFormat { get; private set; } @@ -94,87 +57,6 @@ public LogValuesFormatter(string format, object[]?[]? metadata = null) public LogPropertyInfo GetPropertyInfo(int index) => _metadata![index]; - private static int FindBraceIndex(string format, char brace, int startIndex, int endIndex) - { - // Example: {{prefix{{{Argument}}}suffix}}. - int braceIndex = endIndex; - int scanIndex = startIndex; - int braceOccurrenceCount = 0; - - while (scanIndex < endIndex) - { - if (braceOccurrenceCount > 0 && format[scanIndex] != brace) - { - if (braceOccurrenceCount % 2 == 0) - { - // Even number of '{' or '}' found. Proceed search with next occurrence of '{' or '}'. - braceOccurrenceCount = 0; - braceIndex = endIndex; - } - else - { - // An unescaped '{' or '}' found. - break; - } - } - else if (format[scanIndex] == brace) - { - if (brace == '}') - { - if (braceOccurrenceCount == 0) - { - // For '}' pick the first occurrence. - braceIndex = scanIndex; - } - } - else - { - // For '{' pick the last occurrence. - braceIndex = scanIndex; - } - - braceOccurrenceCount++; - } - - scanIndex++; - } - - return braceIndex; - } - - private static int FindIndexOfAny(string format, char[] chars, int startIndex, int endIndex) - { - int findIndex = format.IndexOfAny(chars, startIndex, endIndex - startIndex); - return findIndex == -1 ? endIndex : findIndex; - } - - public string Format(object?[]? values) - { - object?[]? formattedValues = values; - - if (values != null) - { - for (int i = 0; i < values.Length; i++) - { - object formattedValue = FormatArgument(values[i]); - // If the formatted value is changed, we allocate and copy items to a new array to avoid mutating the array passed in to this method - if (!ReferenceEquals(formattedValue, values[i])) - { - formattedValues = new object[values.Length]; - Array.Copy(values, formattedValues, i); - formattedValues[i++] = formattedValue; - for (; i < values.Length; i++) - { - formattedValues[i] = FormatArgument(values[i]); - } - break; - } - } - } - - return string.Format(CultureInfo.InvariantCulture, _format.Format, formattedValues ?? Array.Empty()); - } - // NOTE: This method mutates the items in the array if needed to avoid extra allocations, and should only be used when caller expects this to happen internal string FormatWithOverwrite(object?[]? values) { @@ -223,176 +105,6 @@ internal string Format(TArg0 arg0, TArg1 arg1, TArg2 arg2) string.Format(CultureInfo.InvariantCulture, _format.Format, (object?)arg0String ?? arg0, (object?)arg1String ?? arg1, (object?)arg2String ?? arg2); } - public KeyValuePair GetValue(object?[] values, int index) - { - if (index < 0 || index > PropertyCount) - { - throw new IndexOutOfRangeException(nameof(index)); - } - - if (PropertyCount > index) - { - return new KeyValuePair(_metadata![index].Name, values[index]); - } - - return new KeyValuePair("{OriginalFormat}", OriginalFormat); - } - - public IEnumerable> GetValues(object[] values) - { - var valueArray = new KeyValuePair[values.Length + 1]; - for (int index = 0; index != PropertyCount; ++index) - { - valueArray[index] = new KeyValuePair(_metadata![index].Name, values[index]); - } - - valueArray[valueArray.Length - 1] = new KeyValuePair("{OriginalFormat}", OriginalFormat); - return valueArray; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void AppendFormattedPropertyValue(T value, ref BufferWriter bufferWriter, int alignment, string? format) - { - if (value is string valueString) - { - if (alignment == 0) - { - bufferWriter.Write(valueString.AsSpan()); - } - else - { - AppendFormattedPropertyValueAligned(valueString, ref bufferWriter, alignment, format); - } - } - else - { - AppendFormattedPropertyValueNonString(value, ref bufferWriter, alignment, format); - } - } - - - protected static void AppendFormattedPropertyValueNonString(T value, ref BufferWriter bufferWriter, int alignment, string? format) - { - if (alignment == 0) - { -#if NET8_0_OR_GREATER - if (value is ISpanFormattable) - { - bufferWriter.EnsureSize(32); - if (((ISpanFormattable)value).TryFormat(bufferWriter.CurrentSpan, out int charsWritten, format, null)) - { - bufferWriter.Advance(charsWritten); - return; - } - } -#endif - bufferWriter.Write(FormatPropertyValue(value, format).AsSpan()); - } - else - { - AppendFormattedPropertyValueAligned(value, ref bufferWriter, alignment, format); - } - } - - protected static void AppendFormattedPropertyValueAligned(T value, ref BufferWriter bufferWriter, int alignment, string? format) - { - bool leftAlign = false; - int paddingNeeded; - Span span; - if (alignment < 0) - { - leftAlign = true; - alignment = -alignment; - } -#if NET8_0_OR_GREATER - if (value is ISpanFormattable) - { - bufferWriter.EnsureSize(Math.Max(32, alignment)); - span = bufferWriter.CurrentSpan; - if (((ISpanFormattable)value).TryFormat(span, out int charsWritten, format, CultureInfo.InvariantCulture)) - { - paddingNeeded = alignment - charsWritten; - if (paddingNeeded <= 0) - { - bufferWriter.Advance(charsWritten); - return; - } - if (leftAlign) - { - span.Slice(charsWritten, paddingNeeded).Fill(' '); - } - else - { - span.Slice(0, charsWritten).CopyTo(span.Slice(paddingNeeded)); - span.Slice(0, paddingNeeded).Fill(' '); - } - bufferWriter.Advance(alignment); - return; - } - } -#endif - - string unpadded = FormatPropertyValue(value, format); - paddingNeeded = alignment - unpadded.Length; - bufferWriter.EnsureSize(Math.Max(unpadded.Length, alignment)); - span = bufferWriter.CurrentSpan; - if (paddingNeeded <= 0) - { - bufferWriter.Write(unpadded.AsSpan()); - return; - } - - if (leftAlign) - { - unpadded.AsSpan().CopyTo(span); - span.Slice(unpadded.Length, paddingNeeded).Fill(' '); - } - else - { - span.Slice(0, paddingNeeded).Fill(' '); - unpadded.AsSpan().CopyTo(span.Slice(paddingNeeded)); - } - bufferWriter.Advance(alignment); - - } - - protected static string FormatPropertyValue(T value, string? format) - { - string? s; - if (value is IFormattable) - { - s = ((IFormattable)value).ToString(format, null); - } - else if (value is not string && value is IEnumerable enumerable) - { - var vsb = new ValueStringBuilder(stackalloc char[256]); - bool first = true; - foreach (object? e in enumerable) - { - if (!first) - { - vsb.Append(", "); - } - - vsb.Append(e != null ? e.ToString() : NullValue); - first = false; - } - return vsb.ToString(); - } - else - { - s = value?.ToString(); - } - if (s == null) - { - return NullValue; - } - else - { - return s; - } - } - private static object FormatArgument(object? value) { string? stringValue = null; diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs index 622b878c95b102..dc40af58f671e9 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs @@ -5,6 +5,8 @@ using System.Buffers; using System.Collections; using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; using System.Threading; using Microsoft.Extensions.Logging.Abstractions; using static Microsoft.Extensions.Logging.LoggerMessage; @@ -52,11 +54,11 @@ public static class LoggerMessage /// A delegate which when invoked creates a log scope. public static Func DefineScope(string formatString) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 2); + LogValuesMetadata metadata = LogValues.CreateMetadata(LogLevel.None, default, formatString); return (logger, arg1, arg2) => { - return logger.BeginScope(new LogValues(formatter, arg1, arg2)); + return logger.BeginScope(new LogValues(metadata, arg1, arg2)); }; } @@ -287,7 +289,7 @@ private static LogEntryPipeline GetLogEntryPipeline(ILogMetadata void Log(ILogger logger, T1 arg1, T2 arg2, Exception? exception) { LogValues state = new LogValues(metadata, arg1, arg2); - LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, LogValues.Callback); + LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, metadata.MessageFormatter); LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); } } @@ -598,112 +600,40 @@ IEnumerator IEnumerable.GetEnumerator() internal class LogValuesMetadata : LogValuesMetadata, ILogMetadata> { - public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) { } - - public void AppendFormattedMessage(in LogValues state, IBufferWriter buffer) + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) { - BufferWriter writer = new BufferWriter(buffer); - foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in CompositeFormat._segments) - { - int index = segment.ArgIndex; - switch (index) - { - case 0: - AppendFormattedPropertyValue(state._value0, ref writer, segment.Alignment, segment.Format); - break; - case 1: - AppendFormattedPropertyValue(state._value1, ref writer, segment.Alignment, segment.Format); - break; - default: - writer.Write(segment.Literal.AsSpan()); - break; - } - } - writer.Flush(); + MessageFormatter = LogMetadataExtensions.CreateStringMessageFormatter(this); } - public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + public VisitPropertyListAction, TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory visitorFactory) { - FormatPropertyAction formatter0 = propertyFormatterFactory.GetPropertyFormatter(0, GetPropertyInfo(0)); - FormatPropertyAction formatter1 = propertyFormatterFactory.GetPropertyFormatter(1, GetPropertyInfo(1)); - return FormatPropertyList; + VisitPropertyAction visit0 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit1 = visitorFactory.GetPropertyVisitor(); + return Visit; - void FormatPropertyList(in LogValues tstate, ref BufferWriter writer) + void Visit(ref LogValues value, ref Span spanCookie, ref TCookie cookie) { - formatter0(tstate._value0, ref writer); - formatter1(tstate._value1, ref writer); + visit0(0, value._value0, ref spanCookie, ref cookie); + visit1(1, value._value1, ref spanCookie, ref cookie); } } - public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customPropertyFormatters) => - (state, buffer) => AppendFormattedMessage(state, buffer, customPropertyFormatters); - - private void AppendFormattedMessage(in LogValues state, IBufferWriter buffer, PropertyCustomFormatter[] customFormatters) - { - BufferWriter writer = new BufferWriter(buffer); - foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in CompositeFormat._segments) - { - int index = segment.ArgIndex; - switch (index) - { - case 0: - AppendCustomFormattedProperty(index, state._value0, ref writer, segment.Alignment, segment.Format, customFormatters[index]); - break; - case 1: - AppendCustomFormattedProperty(index, state._value1, ref writer, segment.Alignment, segment.Format, customFormatters[index]); - break; - default: - writer.Write(segment.Literal.AsSpan()); - break; - } - } - writer.Flush(); - } - - private static void AppendCustomFormattedProperty(int index, T value, ref BufferWriter writer, int alignment, string? format, PropertyCustomFormatter? formatter) - { - if (formatter == null) - { - AppendFormattedPropertyValue(value, ref writer, alignment, format); - } - else - { - writer.Flush(); - if (value is string strVal) - { - formatter.AppendFormatted(index, strVal, writer.Writer); - } - else if (value is int intVal) - { - formatter.AppendFormatted(index, intVal, writer.Writer); - } - else - { - formatter.AppendFormatted(index, value, writer.Writer); - } - } - } - - public Func, Exception?, string> GetStringMessageFormatter() => LogValues.Callback; + public Func, Exception?, string> MessageFormatter; } internal readonly struct LogValues : IReadOnlyList> { - public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); - - private readonly LogValuesFormatter _formatter; + private readonly LogValuesMetadata _formatter; internal readonly T0 _value0; internal readonly T1 _value1; - public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1) + public LogValues(LogValuesMetadata formatter, T0 value0, T1 value1) { _formatter = formatter; _value0 = value0; _value1 = value1; } - public ILogMetadata>? Metadata => _formatter as LogValuesMetadata; - public KeyValuePair this[int index] { get @@ -732,7 +662,7 @@ public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1) } } - public override string ToString() => _formatter.Format(_value0, _value1); + public override string ToString() => _formatter.MessageFormatter(this, null); IEnumerator IEnumerable.GetEnumerator() { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/MessageFormatHelper.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/MessageFormatHelper.cs new file mode 100644 index 00000000000000..84fbca98c46d62 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/MessageFormatHelper.cs @@ -0,0 +1,157 @@ +// 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.Text; + +namespace Microsoft.Extensions.Logging +{ + + /// + /// Helper routines for parsing the ILogger message format. This is the same as composite format + /// except holes are referenced by name instead of number. + /// + internal static class MessageFormatHelper + { + private static readonly char[] FormatDelimiters = { ',', ':' }; + + internal static InternalCompositeFormat Parse(string messageFormat, out LogPropertyInfo[] properties) + { + var vsb = new ValueStringBuilder(stackalloc char[256]); + List propertyList = new List(); + int scanIndex = 0; + int endIndex = messageFormat.Length; + + while (scanIndex < endIndex) + { + int openBraceIndex = FindBraceIndex(messageFormat, '{', scanIndex, endIndex); + if (scanIndex == 0 && openBraceIndex == endIndex) + { + // No holes found. + properties = Array.Empty(); + return InternalCompositeFormat.Parse(messageFormat); + } + + int closeBraceIndex = FindBraceIndex(messageFormat, '}', openBraceIndex, endIndex); + + if (closeBraceIndex == endIndex) + { + vsb.Append(messageFormat.AsSpan(scanIndex, endIndex - scanIndex)); + scanIndex = endIndex; + } + else + { + // Format item syntax : { index[,alignment][ :formatString] }. + int formatDelimiterIndex = FindIndexOfAny(messageFormat, FormatDelimiters, openBraceIndex, closeBraceIndex); + int colonIndex = messageFormat.IndexOf(':', openBraceIndex, closeBraceIndex - openBraceIndex); + + vsb.Append(messageFormat.AsSpan(scanIndex, openBraceIndex - scanIndex + 1)); + vsb.Append(propertyList.Count.ToString()); + string propName = messageFormat.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1); + propertyList.Add(new LogPropertyInfo(propName)); + vsb.Append(messageFormat.AsSpan(formatDelimiterIndex, closeBraceIndex - formatDelimiterIndex + 1)); + scanIndex = closeBraceIndex + 1; + } + } + + properties = propertyList.ToArray(); + return InternalCompositeFormat.Parse(vsb.ToString()); + } + + internal static InternalCompositeFormat Parse(string messageFormat) + { + var vsb = new ValueStringBuilder(stackalloc char[256]); + int propertyCount = 0; + int scanIndex = 0; + int endIndex = messageFormat.Length; + + while (scanIndex < endIndex) + { + int openBraceIndex = FindBraceIndex(messageFormat, '{', scanIndex, endIndex); + if (scanIndex == 0 && openBraceIndex == endIndex) + { + // No holes found. + return InternalCompositeFormat.Parse(messageFormat); + } + + int closeBraceIndex = FindBraceIndex(messageFormat, '}', openBraceIndex, endIndex); + + if (closeBraceIndex == endIndex) + { + vsb.Append(messageFormat.AsSpan(scanIndex, endIndex - scanIndex)); + scanIndex = endIndex; + } + else + { + // Format item syntax : { index[,alignment][ :formatString] }. + int formatDelimiterIndex = FindIndexOfAny(messageFormat, FormatDelimiters, openBraceIndex, closeBraceIndex); + int colonIndex = messageFormat.IndexOf(':', openBraceIndex, closeBraceIndex - openBraceIndex); + + vsb.Append(messageFormat.AsSpan(scanIndex, openBraceIndex - scanIndex + 1)); + vsb.Append(propertyCount.ToString()); + propertyCount++; + string propName = messageFormat.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1); + vsb.Append(messageFormat.AsSpan(formatDelimiterIndex, closeBraceIndex - formatDelimiterIndex + 1)); + scanIndex = closeBraceIndex + 1; + } + } + + return InternalCompositeFormat.Parse(vsb.ToString()); + } + + private static int FindBraceIndex(string format, char brace, int startIndex, int endIndex) + { + // Example: {{prefix{{{Argument}}}suffix}}. + int braceIndex = endIndex; + int scanIndex = startIndex; + int braceOccurrenceCount = 0; + + while (scanIndex < endIndex) + { + if (braceOccurrenceCount > 0 && format[scanIndex] != brace) + { + if (braceOccurrenceCount % 2 == 0) + { + // Even number of '{' or '}' found. Proceed search with next occurrence of '{' or '}'. + braceOccurrenceCount = 0; + braceIndex = endIndex; + } + else + { + // An unescaped '{' or '}' found. + break; + } + } + else if (format[scanIndex] == brace) + { + if (brace == '}') + { + if (braceOccurrenceCount == 0) + { + // For '}' pick the first occurrence. + braceIndex = scanIndex; + } + } + else + { + // For '{' pick the last occurrence. + braceIndex = scanIndex; + } + + braceOccurrenceCount++; + } + + scanIndex++; + } + + return braceIndex; + } + + private static int FindIndexOfAny(string format, char[] chars, int startIndex, int endIndex) + { + int findIndex = format.IndexOfAny(chars, startIndex, endIndex - startIndex); + return findIndex == -1 ? endIndex : findIndex; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/PooledBufferWriter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/PooledBufferWriter.cs new file mode 100644 index 00000000000000..82cbb4cd1a4bc9 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/PooledBufferWriter.cs @@ -0,0 +1,239 @@ +// 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.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging +{ + // TODO: I assume we want to share this source better + // Copied from src\libraries\Common\src\System\Text\Json\PooledByteBufferWriter.cs + // Minor edits were applied to make it compile here + + internal sealed class PooledByteBufferWriter : IBufferWriter, IDisposable + { + // This class allows two possible configurations: if rentedBuffer is not null then + // it can be used as an IBufferWriter and holds a buffer that should eventually be + // returned to the shared pool. If rentedBuffer is null, then the instance is in a + // cleared/disposed state and it must re-rent a buffer before it can be used again. + private byte[]? _rentedBuffer; + private int _index; + + private const int MinimumBufferSize = 256; + + // Value copied from Array.MaxLength in System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Array.cs. + public const int MaximumBufferSize = 0X7FFFFFC7; + + private PooledByteBufferWriter() + { +#if NETCOREAPP + // Ensure we are in sync with the Array.MaxLength implementation. + Debug.Assert(MaximumBufferSize == Array.MaxLength); +#endif + } + + public PooledByteBufferWriter(int initialCapacity) : this() + { + Debug.Assert(initialCapacity > 0); + + _rentedBuffer = ArrayPool.Shared.Rent(initialCapacity); + _index = 0; + } + + public ReadOnlyMemory WrittenMemory + { + get + { + Debug.Assert(_rentedBuffer != null); + Debug.Assert(_index <= _rentedBuffer.Length); + return _rentedBuffer.AsMemory(0, _index); + } + } + + public int WrittenCount + { + get + { + Debug.Assert(_rentedBuffer != null); + return _index; + } + } + + public int Capacity + { + get + { + Debug.Assert(_rentedBuffer != null); + return _rentedBuffer.Length; + } + } + + public int FreeCapacity + { + get + { + Debug.Assert(_rentedBuffer != null); + return _rentedBuffer.Length - _index; + } + } + + public void Clear() + { + ClearHelper(); + } + + public void ClearAndReturnBuffers() + { + Debug.Assert(_rentedBuffer != null); + + ClearHelper(); + byte[] toReturn = _rentedBuffer; + _rentedBuffer = null; + ArrayPool.Shared.Return(toReturn); + } + + private void ClearHelper() + { + Debug.Assert(_rentedBuffer != null); + Debug.Assert(_index <= _rentedBuffer.Length); + + _rentedBuffer.AsSpan(0, _index).Clear(); + _index = 0; + } + + // Returns the rented buffer back to the pool + public void Dispose() + { + if (_rentedBuffer == null) + { + return; + } + + ClearHelper(); + byte[] toReturn = _rentedBuffer; + _rentedBuffer = null; + ArrayPool.Shared.Return(toReturn); + } + + public void InitializeEmptyInstance(int initialCapacity) + { + Debug.Assert(initialCapacity > 0); + Debug.Assert(_rentedBuffer is null); + + _rentedBuffer = ArrayPool.Shared.Rent(initialCapacity); + _index = 0; + } + + public static PooledByteBufferWriter CreateEmptyInstanceForCaching() => new PooledByteBufferWriter(); + + public void Advance(int count) + { + Debug.Assert(_rentedBuffer != null); + Debug.Assert(count >= 0); + Debug.Assert(_index <= _rentedBuffer.Length - count); + _index += count; + } + + public Memory GetMemory(int sizeHint = MinimumBufferSize) + { + CheckAndResizeBuffer(sizeHint); + return _rentedBuffer.AsMemory(_index); + } + + public Span GetSpan(int sizeHint = MinimumBufferSize) + { + CheckAndResizeBuffer(sizeHint); + return _rentedBuffer.AsSpan(_index); + } + +#if NETCOREAPP + internal ValueTask WriteToStreamAsync(Stream destination, CancellationToken cancellationToken) + { + return destination.WriteAsync(WrittenMemory, cancellationToken); + } + + internal void WriteToStream(Stream destination) + { + destination.Write(WrittenMemory.Span); + } +#else + internal Task WriteToStreamAsync(Stream destination, CancellationToken cancellationToken) + { + Debug.Assert(_rentedBuffer != null); + return destination.WriteAsync(_rentedBuffer, 0, _index, cancellationToken); + } + + internal void WriteToStream(Stream destination) + { + Debug.Assert(_rentedBuffer != null); + destination.Write(_rentedBuffer, 0, _index); + } +#endif + + private void CheckAndResizeBuffer(int sizeHint) + { + Debug.Assert(_rentedBuffer != null); + Debug.Assert(sizeHint > 0); + + int currentLength = _rentedBuffer.Length; + int availableSpace = currentLength - _index; + + // If we've reached ~1GB written, grow to the maximum buffer + // length to avoid incessant minimal growths causing perf issues. + if (_index >= MaximumBufferSize / 2) + { + sizeHint = Math.Max(sizeHint, MaximumBufferSize - currentLength); + } + + if (sizeHint > availableSpace) + { + int growBy = Math.Max(sizeHint, currentLength); + + int newSize = currentLength + growBy; + + if ((uint)newSize > MaximumBufferSize) + { + newSize = currentLength + sizeHint; + if ((uint)newSize > MaximumBufferSize) + { + ThrowHelper.ThrowOutOfMemoryException_BufferMaximumSizeExceeded((uint)newSize); + } + } + + byte[] oldBuffer = _rentedBuffer; + + _rentedBuffer = ArrayPool.Shared.Rent(newSize); + + Debug.Assert(oldBuffer.Length >= _index); + Debug.Assert(_rentedBuffer.Length >= _index); + + Span oldBufferAsSpan = oldBuffer.AsSpan(0, _index); + oldBufferAsSpan.CopyTo(_rentedBuffer); + oldBufferAsSpan.Clear(); + ArrayPool.Shared.Return(oldBuffer); + } + + Debug.Assert(_rentedBuffer.Length - _index > 0); + Debug.Assert(_rentedBuffer.Length - _index >= sizeHint); + } + } +} + +namespace System +{ + internal static partial class ThrowHelper + { + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowOutOfMemoryException_BufferMaximumSizeExceeded(uint capacity) + { + throw new OutOfMemoryException(SR.Format(/*SR.BufferMaximumSizeExceeded*/ "PLACEHOLDER", capacity)); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs index 47f6b5ff4969cc..0cdab65618a6fa 100644 --- a/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs +++ b/src/libraries/Microsoft.Extensions.Logging/ref/Microsoft.Extensions.Logging.cs @@ -26,10 +26,12 @@ public enum ActivityTrackingOptions Tags = 32, Baggage = 64, } + /* public static partial class EnrichmentExtensions { public static Microsoft.Extensions.Logging.ILoggingBuilder Enrich(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string propertyName, System.Func valueFunc) { throw null; } } + */ public static partial class FilterLoggingBuilderExtensions { public static Microsoft.Extensions.Logging.ILoggingBuilder AddFilter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Func levelFilter) { throw null; } diff --git a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs index d6d024e176bd1a..9edd00a11c0e92 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/DispatchProcessor.cs @@ -48,7 +48,7 @@ public LogEntryHandler GetLogEntryHandler(ILogMetadata? enabled = true; dynamicCheckRequired = true; - return new DynamicDispatchToLoggers(this, metadata?.GetStringMessageFormatter()); + return new DynamicDispatchToLoggers(this); } public ScopeHandler GetScopeHandler(ILogMetadata? metadata, out bool enabled) where TState : notnull @@ -177,17 +177,15 @@ public DispatchViaScopeHandler(ScopeHandler handler) private sealed class DynamicDispatchToLoggers : LogEntryHandler { private DispatchProcessor _processor; - private Func? _formatter; - public DynamicDispatchToLoggers(DispatchProcessor processor, Func? formatter) + public DynamicDispatchToLoggers(DispatchProcessor processor) { _processor = processor; - _formatter = formatter; } public override void HandleLogEntry(ref LogEntry logEntry) { - Func? formatter = logEntry.Formatter ?? _formatter; + Func formatter = logEntry.Formatter; formatter ??= (TState s, Exception? _) => s == null ? "" : s.ToString() ?? ""; LoggerInformation[] loggers = _processor._loggers!; List? exceptions = null; diff --git a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs index efa1d135a3137a..e52885561c9cab 100644 --- a/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging/src/EnrichmentExtensions.cs @@ -10,6 +10,7 @@ namespace Microsoft.Extensions.Logging { + /* public static class EnrichmentExtensions { public static ILoggingBuilder Enrich(this ILoggingBuilder builder, string propertyName, Func valueFunc) @@ -642,5 +643,5 @@ public static string Format(UnboundedEnrichmentPropertyValues GetEnumerator(); - } + }*/ } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs index f604218b0ed120..44f00dfefa753d 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/EnrichmentTests.cs @@ -12,6 +12,7 @@ namespace Microsoft.Extensions.Logging.Test { public class EnrichmentTests { + /* [Fact] public void LogInformation_PropertyAddedToState() { @@ -171,6 +172,6 @@ public ScopeHandler GetScopeHandler(ILogMetadata? metada } public bool IsEnabled(LogLevel logLevel) => _nextProcessor.IsEnabled(logLevel); - } + } */ } } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/FormattedLogValuesTest.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/FormattedLogValuesTest.cs index c20ede7d712b97..428702c187db79 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/FormattedLogValuesTest.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/FormattedLogValuesTest.cs @@ -127,12 +127,12 @@ public void CachedFormattersAreCapped() } // check cached formatter - var formatter = new FormattedLogValues("0{i}", 0).Formatter; - Assert.Same(formatter, new FormattedLogValues("0{i}", 0).Formatter); + var formatter = new FormattedLogValues("0{i}", 0).Metadata; + Assert.Same(formatter, new FormattedLogValues("0{i}", 0).Metadata); // check non-cached formatter - formatter = new FormattedLogValues("test {}", 0).Formatter; - Assert.NotSame(formatter, new FormattedLogValues("test {}", 0).Formatter); + formatter = new FormattedLogValues("test {}", 0).Metadata; + Assert.NotSame(formatter, new FormattedLogValues("test {}", 0).Metadata); } // message format, format arguments, expected message diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/MessageFormattingTest.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/MessageFormattingTest.cs new file mode 100644 index 00000000000000..cb5650bc28a6cb --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/MessageFormattingTest.cs @@ -0,0 +1,146 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Logging.Test +{ + public class MessageFormattingTest + { + [Fact] + public void BasicStringMessageFormatter() + { + //arrange + TwoParamMetadata metadata = new TwoParamMetadata("User {p1} is {p2} years old."); + Func<(string, int), Exception?, string> formatter = metadata.CreateStringMessageFormatter(); + + //act + string result = formatter(("John", 10), null); + + //assert + Assert.Equal("User John is 10 years old.", result); + } + + [Fact] + public void BasicMessageFormatter() + { + //arrange + TwoParamMetadata metadata = new TwoParamMetadata("User {p1} is {p2} years old."); + FormatLogMessage<(string,int)> formatter = metadata.CreateMessageFormatter(); + PooledByteBufferWriter writer = new PooledByteBufferWriter(256); + + //act + (string, int) state = ("John", 10); + formatter(ref state, writer); + + //assert + Assert.Equal("User John is 10 years old.", MemoryMarshal.Cast(writer.WrittenMemory.Span).ToString()); + } + + [Fact] + public void NullSentinel() + { + //arrange + TwoParamMetadata metadata = new TwoParamMetadata("User {p1} is {p2} years old."); + Func<(string, int), Exception?, string> formatter = metadata.CreateStringMessageFormatter(); + + //act + string result = formatter((null, 10), null); + + //assert + Assert.Equal("User (null) is 10 years old.", result); + } + + [Fact] + public void EnumerateArrays() + { + //arrange + TwoParamMetadata metadata = new TwoParamMetadata("Users {p1} are {p2} years old."); + Func<(string[], int), Exception?, string> formatter = metadata.CreateStringMessageFormatter(); + + //act + string result = formatter((new string[] { "Joe", "Jim", "John" }, 10), null); + + //assert + Assert.Equal("Users Joe, Jim, John are 10 years old.", result); + } + + [Fact] + public void MessagesHandleFormatSpecifiers() + { + //arrange + TwoParamMetadata metadata = new TwoParamMetadata("User {p1} is 0x{p2:x} years old."); + Func<(string, int), Exception?, string> formatter = metadata.CreateStringMessageFormatter(); + + //act + string result = formatter(("John", 10), null); + + //assert + Assert.Equal("User John is 0xa years old.", result); + } + + [Theory] + [InlineData("User {p1,-10} is {p2,-10} years old.", "User John is 10 years old.")] + [InlineData("User {p1,10} is {p2,10} years old.", "User John is 10 years old.")] + [InlineData("User {p1,1} is {p2,1} years old.", "User John is 10 years old.")] + [InlineData("User {p1,-1} is {p2,-1} years old.", "User John is 10 years old.")] + [InlineData("User {p1,-5} is {p2,3} years old.", "User John is 10 years old.")] + public void MessagesHandleAlignment(string format, string expected) + { + //arrange + TwoParamMetadata metadata = new TwoParamMetadata(format); + Func<(string, int), Exception?, string> formatter = metadata.CreateStringMessageFormatter(); + + //act + string result = formatter(("John", 10), null); + + //assert + Assert.Equal(expected, result); + } + } + + class TwoParamMetadata : ILogMetadata<(T1, T2)> + { + public TwoParamMetadata(string format) + { + OriginalFormat = format; + } + + public LogLevel LogLevel => LogLevel.None; + + public EventId EventId => 1; + + public string OriginalFormat { get; set; } + + public int PropertyCount => 2; + + public VisitPropertyListAction<(T1, T2), TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory propertyVisitorFactory) + { + VisitPropertyAction visit0 = propertyVisitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit1 = propertyVisitorFactory.GetPropertyVisitor(); + return Visit; + + void Visit(ref (T1, T2) value, ref Span spanCookie, ref TCookie cookie) + { + visit0(0, value.Item1, ref spanCookie, ref cookie); + visit1(1, value.Item2, ref spanCookie, ref cookie); + } + } + public LogPropertyInfo GetPropertyInfo(int index) + { + switch (index) + { + case 0: return new LogPropertyInfo("p1", null); + case 1: return new LogPropertyInfo("p2", null); + } + throw new IndexOutOfRangeException(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs index 4ec37d914cc2b6..8f94c61c677744 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs @@ -143,137 +143,11 @@ public PropertyRedaction(int index, IRedactor redactor) public IRedactor Redactor; } - - internal class RedactedPropertyFormatter : PropertyCustomFormatter - { - const int MaxStackAllocChars = 256; - IRedactor _redactor; - - public RedactedPropertyFormatter(IRedactor redactor) - { - _redactor = redactor; - } - - public override void AppendFormatted(int index, string value, IBufferWriter buffer) - { - int len = _redactor.GetRedactedLength(value); - if (len != 0) - { - Span redactedBuffer = buffer.GetSpan(len); - _redactor.Redact(value, redactedBuffer); - buffer.Advance(len); - } - } - - public override void AppendFormatted(int index, T value, IBufferWriter buffer) - { - if (value == null) - { - return; - } - if (value is ISpanFormattable) - { - Span unredactedBuffer = stackalloc char[MaxStackAllocChars]; - if (((ISpanFormattable)value).TryFormat(unredactedBuffer, out int charsWritten2, null, null)) - { - unredactedBuffer = unredactedBuffer.Slice(0, charsWritten2); - int len2 = _redactor.GetRedactedLength(unredactedBuffer); - if (len2 != 0) - { - Span redactedBuffer2 = buffer.GetSpan(len2); - _redactor.Redact(unredactedBuffer, redactedBuffer2); - buffer.Advance(len2); - } - return; - } - } - string? unredactedValue = value.ToString(); - int len = _redactor.GetRedactedLength(unredactedValue); - if (len != 0) - { - Span redactedBuffer = buffer.GetSpan(len); - _redactor.Redact(unredactedValue, redactedBuffer); - buffer.Advance(len); - } - } - } - - internal class ChainedRedactedPropertyFormatter : PropertyCustomFormatter - { - const int MaxStackAllocChars = 256; - IRedactor _redactor; - PropertyCustomFormatter _nextFormatter; - - public ChainedRedactedPropertyFormatter(IRedactor redactor, PropertyCustomFormatter nextFormatter) - { - _redactor = redactor; - _nextFormatter = nextFormatter; - } - - public override void AppendFormatted(int index, string value, IBufferWriter buffer) - { - int len = _redactor.GetRedactedLength(value); - if (len <= MaxStackAllocChars) - { - Span redactedBuffer = stackalloc char[len]; - _redactor.Redact(value, redactedBuffer); - _nextFormatter.AppendFormatted(index, redactedBuffer, buffer); - } - else - { - string redactedValue = _redactor.Redact(value); - _nextFormatter.AppendFormatted(index, redactedValue, buffer); - } - } - - public override void AppendFormatted(int index, T value, IBufferWriter buffer) - { - if (value == null) - { - return; - } - else if (value is ISpanFormattable) - { - Span unredactedBuffer = stackalloc char[MaxStackAllocChars]; - if (((ISpanFormattable)value).TryFormat(unredactedBuffer, out int charsWritten2, null, null)) - { - unredactedBuffer = unredactedBuffer.Slice(0, charsWritten2); - int len2 = _redactor.GetRedactedLength(unredactedBuffer); - if (len2 <= MaxStackAllocChars) - { - Span redactedBuffer2 = stackalloc char[len2]; - _redactor.Redact(unredactedBuffer, redactedBuffer2); - _nextFormatter.AppendFormatted(index, redactedBuffer2, buffer); - } - else - { - string redactedValue = _redactor.Redact(unredactedBuffer); - _nextFormatter.AppendFormatted(index, redactedValue, buffer); - } - return; - } - } - string? unredactedValue = value.ToString(); - int len = _redactor.GetRedactedLength(unredactedValue); - if (len <= MaxStackAllocChars) - { - Span redactedBuffer = stackalloc char[len]; - _redactor.Redact(unredactedValue, redactedBuffer); - _nextFormatter.AppendFormatted(index, redactedBuffer, buffer); - } - else - { - string redactedValue = _redactor.Redact(unredactedValue); - _nextFormatter.AppendFormatted(index, redactedValue, buffer); - } - } - } - internal class RedactedLogMetadata : ILogMetadata> { private readonly ILogMetadata _originalMetadata; private readonly PropertyRedaction[] _redactions; - private Action>? _defaultFormatter; + private Func, Exception?, string> _formatter; public RedactedLogMetadata(ILogMetadata metadata, PropertyRedaction[] redactions) { @@ -303,195 +177,144 @@ public RedactedLogMetadata(ILogMetadata metadata, PropertyRedaction[] redacti public int PropertyCount => _originalMetadata.PropertyCount; - public void AppendFormattedMessage(in RedactedValues state, IBufferWriter buffer) - { - if (_defaultFormatter == null) - { - RedactedPropertyFormatter[] propertyRedactors = new RedactedPropertyFormatter[PropertyCount]; - foreach (PropertyRedaction redaction in _redactions) - { - propertyRedactors[redaction.Index] = new RedactedPropertyFormatter(redaction.Redactor); - } - // this could be overwritten by another thread in a race but it doesn't matter - // as any copy of this delegate will have the same functionality - _defaultFormatter = _originalMetadata.GetMessageFormatter(propertyRedactors); - } - _defaultFormatter(state.OriginalState, buffer); - } - - public Action, IBufferWriter> GetMessageFormatter(PropertyCustomFormatter[] customFormatters) - { - PropertyCustomFormatter[] wrappedFormatters = new PropertyCustomFormatter[customFormatters.Length]; - Array.Copy(customFormatters, wrappedFormatters, customFormatters.Length); - foreach (PropertyRedaction redaction in _redactions) - { - PropertyCustomFormatter nextFormatter = wrappedFormatters[redaction.Index]; - wrappedFormatters[redaction.Index] = nextFormatter == null ? - new RedactedPropertyFormatter(redaction.Redactor) : - new ChainedRedactedPropertyFormatter(redaction.Redactor, nextFormatter); - } - Action> innerFormatter = _originalMetadata.GetMessageFormatter(wrappedFormatters); - return (state, buffer) => innerFormatter(state.OriginalState, buffer); - } - public LogPropertyInfo GetPropertyInfo(int index) => _originalMetadata.GetPropertyInfo(index); - class Slot { public ArrayBufferWriter? Buffer; } - static ThreadLocal t_slot = new ThreadLocal(); - - public string FormatMessage(in RedactedValues state) + internal string FormatMessage(in RedactedValues state) { - Slot? tstate = t_slot.Value; - if (tstate == null) + if(_formatter == null) { - tstate = new Slot(); - t_slot.Value = tstate; + // multiple threads could race to set this and overwrite one another + // but it doesn't matter. + _formatter = this.CreateStringMessageFormatter(); } - ArrayBufferWriter arrayBuffer = tstate.Buffer ?? new ArrayBufferWriter(); - tstate.Buffer = null; - AppendFormattedMessage(state, arrayBuffer); - string ret = new string(arrayBuffer.WrittenSpan); - arrayBuffer.Clear(); - tstate.Buffer = arrayBuffer; - return ret; + return _formatter(state, null); } - public Func, Exception?, string> GetStringMessageFormatter() => RedactedValues.Callback; - - class RedactedPropertyFormatterFactory : IPropertyFormatterFactory + class RedactedValuePropertyVisitorFactory : IPropertyVisitorFactory { const int MaxStackAllocChars = 256; + RedactedLogMetadata _metadata; - IPropertyFormatterFactory _wrappedFormatterFactory; + IPropertyVisitorFactory _innerFactory; + VisitPropertyAction _stringVisitor; + VisitSpanPropertyAction _spanVisitor; - public RedactedPropertyFormatterFactory(RedactedLogMetadata metadata, IPropertyFormatterFactory wrappedFormatterFactory) + public RedactedValuePropertyVisitorFactory(RedactedLogMetadata metadata, IPropertyVisitorFactory innerFactory) { _metadata = metadata; - _wrappedFormatterFactory = wrappedFormatterFactory; + _innerFactory = innerFactory; + _stringVisitor = _innerFactory.GetPropertyVisitor(); + _spanVisitor = _innerFactory.GetSpanPropertyVisitor(); } - public FormatPropertyAction GetPropertyFormatter(int propertyIndex, LogPropertyInfo metadata) + public VisitPropertyAction GetPropertyVisitor() { - PropertyRedaction? redaction = _metadata.GetRedactionForIndex(propertyIndex); - if (!redaction.HasValue) - { - return _wrappedFormatterFactory.GetPropertyFormatter(propertyIndex, _metadata.GetPropertyInfo(propertyIndex)); - } - else - { - return GetRedactedPropertyFormatter(propertyIndex, metadata, redaction.Value.Redactor); - } - } + VisitPropertyAction unredactedVisit = _innerFactory.GetPropertyVisitor(); + return Visit; - public FormatSpanPropertyAction GetSpanPropertyFormatter(int propertyIndex, LogPropertyInfo metadata) - { - PropertyRedaction? redaction = _metadata.GetRedactionForIndex(propertyIndex); - if (!redaction.HasValue) - { - return _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyInfo(propertyIndex)); - } - else + void Visit(int propIndex, PropType value, ref Span spanCookie, ref TCookie cookie) { - return GetRedactedSpanPropertyFormatter(propertyIndex, metadata, redaction.Value.Redactor); - } - } + IRedactor? redactor = _metadata.GetPropertyRedactor(propIndex); + if(redactor == null) + { + unredactedVisit(propIndex, value, ref spanCookie, ref cookie); + return; + } - private FormatSpanPropertyAction GetRedactedSpanPropertyFormatter(int propertyIndex, LogPropertyInfo metadata, IRedactor redactor) - { - FormatSpanPropertyAction wrappedFormatter = _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyInfo(propertyIndex)); - return FormatRedactedProperty; + Span unredactedBuffer = stackalloc char[MaxStackAllocChars]; + if (TryGetUnredactedBuffer(value, ref unredactedBuffer)) + { + int len2 = redactor.GetRedactedLength(unredactedBuffer); + if (len2 <= MaxStackAllocChars) + { + Span redactedBuffer2 = stackalloc char[len2]; + redactor.Redact(unredactedBuffer, redactedBuffer2); + _spanVisitor(propIndex, redactedBuffer2, ref spanCookie, ref cookie); + } + else + { + string redactedValue = redactor.Redact(unredactedBuffer); + _stringVisitor(propIndex, redactedValue, ref spanCookie, ref cookie); + } + return; + } - void FormatRedactedProperty(scoped ReadOnlySpan value, ref BufferWriter writer) - { - int len = redactor.GetRedactedLength(value); + string? unredactedValue = value.ToString(); + int len = redactor.GetRedactedLength(unredactedValue); if (len <= MaxStackAllocChars) { Span redactedBuffer = stackalloc char[len]; - redactor.Redact(value, redactedBuffer); - wrappedFormatter(redactedBuffer, ref writer); + redactor.Redact(unredactedValue, redactedBuffer); + _spanVisitor(propIndex, redactedBuffer, ref spanCookie, ref cookie); } else { - string redactedValue = redactor.Redact(value); - wrappedFormatter(redactedValue, ref writer); + string redactedValue = redactor.Redact(unredactedValue); + _stringVisitor(propIndex, redactedValue, ref spanCookie, ref cookie); } } } - private FormatPropertyAction GetRedactedPropertyFormatter(int propertyIndex, LogPropertyInfo metadata, IRedactor redactor) + private bool TryGetUnredactedBuffer(TProp value, ref Span unredactedBuffer) { - FormatSpanPropertyAction wrappedFormatter = _wrappedFormatterFactory.GetSpanPropertyFormatter(propertyIndex, _metadata.GetPropertyInfo(propertyIndex)); - return FormatRedactedProperty; - - void FormatRedactedProperty(PropType value, ref BufferWriter writer) + if (value == null) { - if (value == null) + unredactedBuffer = default; + return true; + } + else if (value is ISpanFormattable) + { + if (((ISpanFormattable)value).TryFormat(unredactedBuffer, out int charsWritten2, null, null)) { - return; + unredactedBuffer = unredactedBuffer.Slice(0, charsWritten2); + return true; } - else if (value is ISpanFormattable) + } + return false; + } + + public VisitSpanPropertyAction GetSpanPropertyVisitor() + { + return Visit; + + void Visit(int propIndex, scoped ReadOnlySpan value, ref Span spanCookie, ref TCookie cookie) + { + IRedactor? redactor = _metadata.GetPropertyRedactor(propIndex); + if (redactor == null) { - Span unredactedBuffer = stackalloc char[MaxStackAllocChars]; - if (((ISpanFormattable)value).TryFormat(unredactedBuffer, out int charsWritten2, null, null)) - { - unredactedBuffer = unredactedBuffer.Slice(0, charsWritten2); - int len2 = redactor!.GetRedactedLength(unredactedBuffer); - if (len2 <= MaxStackAllocChars) - { - Span redactedBuffer2 = stackalloc char[len2]; - redactor!.Redact(unredactedBuffer, redactedBuffer2); - wrappedFormatter(redactedBuffer2, ref writer); - } - else - { - string redactedValue = redactor.Redact(unredactedBuffer); - wrappedFormatter(redactedValue, ref writer); - } - return; - } + _spanVisitor(propIndex, value, ref spanCookie, ref cookie); + return; } - string? unredactedValue = value.ToString(); - int len = redactor.GetRedactedLength(unredactedValue); + + int len = redactor.GetRedactedLength(value); if (len <= MaxStackAllocChars) { - Span redactedBuffer = stackalloc char[len]; - redactor.Redact(unredactedValue, redactedBuffer); - wrappedFormatter(redactedBuffer, ref writer); + Span redactedBuffer2 = stackalloc char[len]; + redactor.Redact(value, redactedBuffer2); + _spanVisitor(propIndex, redactedBuffer2, ref spanCookie, ref cookie); } else { - string redactedValue = redactor.Redact(unredactedValue); - wrappedFormatter(redactedValue, ref writer); + string redactedValue = redactor.Redact(value); + _stringVisitor(propIndex, redactedValue, ref spanCookie, ref cookie); } } } } - public FormatPropertyListAction> GetPropertyListFormatter(IPropertyFormatterFactory propertyFormatterFactory) + public VisitPropertyListAction, TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory visitor) { - FormatPropertyListAction wrappedFormatPropertyList = _originalMetadata.GetPropertyListFormatter(new RedactedPropertyFormatterFactory(this, propertyFormatterFactory)); - return FormatPropertyList; + VisitPropertyListAction innerListVisitor = _originalMetadata.CreatePropertyListVisitor(new RedactedValuePropertyVisitorFactory(this, visitor)); + return VisitPropertyList; - void FormatPropertyList(in RedactedValues state, ref BufferWriter writer) + void VisitPropertyList(ref RedactedValues tState, ref Span spanCookie, ref TCookie cookie) { - wrappedFormatPropertyList(in state.OriginalState, ref writer); + innerListVisitor(ref tState.OriginalState, ref spanCookie, ref cookie); } } - - private PropertyRedaction? GetRedactionForIndex(int propIndex) - { - foreach (PropertyRedaction redaction in _redactions) - { - if (redaction.Index == propIndex) - { - return redaction; - } - } - return null; - } } - internal readonly struct RedactedValues : IReadOnlyList> + internal struct RedactedValues : IReadOnlyList> { public RedactedValues(RedactedLogMetadata metadata, in T originalState) { @@ -500,7 +323,7 @@ public RedactedValues(RedactedLogMetadata metadata, in T originalState) } public readonly RedactedLogMetadata Metadata; - public readonly T OriginalState; + public T OriginalState; public override string ToString() => Metadata.FormatMessage(this); From 85c3d44d3df2d8223c50e7906ebd346f6ebdbf0a Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Wed, 14 Jun 2023 03:03:26 -0700 Subject: [PATCH 24/26] Remove LogValuesFormatter - Replace all usage of the formatter with ILogMetadata --- .../src/LogValuesFormatter.cs | 144 ----- .../src/LogValuesMetadata.cs | 213 +++++++ .../src/LoggerMessage.cs | 525 ++++++++---------- 3 files changed, 445 insertions(+), 437 deletions(-) delete mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesMetadata.cs diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs deleted file mode 100644 index 12b79484141368..00000000000000 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs +++ /dev/null @@ -1,144 +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 System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Runtime.CompilerServices; -using System.Text; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.Extensions.Logging -{ - internal class LogValuesMetadata : LogValuesFormatter - { - public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata) : base(format, metadata) - { - LogLevel = level; - EventId = eventId; - } - - public string FinalFormat => CompositeFormat.Format; - public LogLevel LogLevel { get; } - public EventId EventId { get; } - } - - /// - /// Formatter to convert the named format items like {NamedformatItem} to format. - /// - internal class LogValuesFormatter - { - private const string NullValue = "(null)"; - private readonly LogPropertyInfo[]? _metadata; - private readonly InternalCompositeFormat _format; - - public LogValuesFormatter(string format, object[]?[]? metadata = null) - { - ThrowHelper.ThrowIfNull(format); - - OriginalFormat = format; - _format = MessageFormatHelper.Parse(format, out _metadata); - if(metadata != null && _metadata != null) - { - for (int i = 0; i < _metadata.Length; i++) - { - _metadata[i].Metadata = metadata[i]; - } - } - } - - public string OriginalFormat { get; private set; } - public InternalCompositeFormat CompositeFormat => _format; - public int PropertyCount => _metadata != null ? _metadata.Length : 0; - public string GetValueName(int index) => _metadata![index].Name; - - public LogPropertyInfo GetPropertyInfo(int index) => _metadata![index]; - - // NOTE: This method mutates the items in the array if needed to avoid extra allocations, and should only be used when caller expects this to happen - internal string FormatWithOverwrite(object?[]? values) - { - if (values != null) - { - for (int i = 0; i < values.Length; i++) - { - values[i] = FormatArgument(values[i]); - } - } - - return string.Format(CultureInfo.InvariantCulture, _format.Format, values ?? Array.Empty()); - } - - internal string Format() - { - return _format.Format; - } - internal string Format(TArg0 arg0) - { - string? arg0String = null; - return - !TryFormatArgumentIfNullOrEnumerable(arg0, ref arg0String) ? - string.Format(CultureInfo.InvariantCulture, _format.Format, arg0) : - string.Format(CultureInfo.InvariantCulture, _format.Format, arg0String); - } - - internal string Format(TArg0 arg0, TArg1 arg1) - { - string? arg0String = null, arg1String = null; - return - !TryFormatArgumentIfNullOrEnumerable(arg0, ref arg0String) && - !TryFormatArgumentIfNullOrEnumerable(arg1, ref arg1String) ? - string.Format(CultureInfo.InvariantCulture, _format.Format, arg0, arg1) : - string.Format(CultureInfo.InvariantCulture, _format.Format, (object?)arg0String ?? arg0, (object?)arg1String ?? arg1); - } - - internal string Format(TArg0 arg0, TArg1 arg1, TArg2 arg2) - { - string? arg0String = null, arg1String = null, arg2String = null; - return - !TryFormatArgumentIfNullOrEnumerable(arg0, ref arg0String) && - !TryFormatArgumentIfNullOrEnumerable(arg1, ref arg1String) && - !TryFormatArgumentIfNullOrEnumerable(arg2, ref arg2String) ? - string.Format(CultureInfo.InvariantCulture, _format.Format, arg0, arg1, arg2) : - string.Format(CultureInfo.InvariantCulture, _format.Format, (object?)arg0String ?? arg0, (object?)arg1String ?? arg1, (object?)arg2String ?? arg2); - } - - private static object FormatArgument(object? value) - { - string? stringValue = null; - return TryFormatArgumentIfNullOrEnumerable(value, ref stringValue) ? stringValue : value!; - } - - private static bool TryFormatArgumentIfNullOrEnumerable(object? value, [NotNullWhen(true)] ref string? stringValue) - { - if (value == null) - { - stringValue = NullValue; - return true; - } - - // if the value implements IEnumerable but isn't itself a string, build a comma separated string. - if (value is not string && value is IEnumerable enumerable) - { - var vsb = new ValueStringBuilder(stackalloc char[256]); - bool first = true; - foreach (object? e in enumerable) - { - if (!first) - { - vsb.Append(", "); - } - - vsb.Append(e != null ? e.ToString() : NullValue); - first = false; - } - stringValue = vsb.ToString(); - return true; - } - - return false; - } - } -} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesMetadata.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesMetadata.cs new file mode 100644 index 00000000000000..068a65e3c53ee8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesMetadata.cs @@ -0,0 +1,213 @@ +// 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 static Microsoft.Extensions.Logging.LoggerMessage; + +namespace Microsoft.Extensions.Logging +{ + internal class LogValuesMetadataBase + { + private readonly LogPropertyInfo[]? _metadata; + + public LogValuesMetadataBase(string format, LogLevel level, EventId eventId, object[]?[]? metadata) + { + ThrowHelper.ThrowIfNull(format); + + OriginalFormat = format; + _ = MessageFormatHelper.Parse(format, out _metadata); + if (metadata != null && _metadata != null) + { + for (int i = 0; i < _metadata.Length; i++) + { + _metadata[i].Metadata = metadata[i]; + } + } + + LogLevel = level; + EventId = eventId; + } + + public string OriginalFormat { get; private set; } + public int PropertyCount => _metadata != null ? _metadata.Length : 0; + public string GetValueName(int index) => _metadata![index].Name; + public LogPropertyInfo GetPropertyInfo(int index) => _metadata![index]; + public LogLevel LogLevel { get; } + public EventId EventId { get; } + } + + internal class LogValuesMetadata : LogValuesMetadataBase, ILogMetadata + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) + { + MessageFormatter = LogMetadataExtensions.CreateStringMessageFormatter(this); + } + + public VisitPropertyListAction CreatePropertyListVisitor(IPropertyVisitorFactory _) + { + return Visit; + + static void Visit(ref LogValues value, ref Span spanCookie, ref TCookie cookie) + { + } + } + + public Func MessageFormatter { get; } + } + + internal class LogValuesMetadata : LogValuesMetadataBase, ILogMetadata> + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) + { + MessageFormatter = LogMetadataExtensions.CreateStringMessageFormatter(this); + } + + public VisitPropertyListAction, TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory visitorFactory) + { + VisitPropertyAction visit0 = visitorFactory.GetPropertyVisitor(); + return Visit; + + void Visit(ref LogValues value, ref Span spanCookie, ref TCookie cookie) + { + visit0(0, value._value0, ref spanCookie, ref cookie); + } + } + + public Func, Exception?, string> MessageFormatter { get; } + } + + internal class LogValuesMetadata : LogValuesMetadataBase, ILogMetadata> + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) + { + MessageFormatter = LogMetadataExtensions.CreateStringMessageFormatter(this); + } + + public VisitPropertyListAction, TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory visitorFactory) + { + VisitPropertyAction visit0 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit1 = visitorFactory.GetPropertyVisitor(); + return Visit; + + void Visit(ref LogValues value, ref Span spanCookie, ref TCookie cookie) + { + visit0(0, value._value0, ref spanCookie, ref cookie); + visit1(1, value._value1, ref spanCookie, ref cookie); + } + } + + public Func, Exception?, string> MessageFormatter { get; } + } + + internal class LogValuesMetadata : LogValuesMetadataBase, ILogMetadata> + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) + { + MessageFormatter = LogMetadataExtensions.CreateStringMessageFormatter(this); + } + + public VisitPropertyListAction, TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory visitorFactory) + { + VisitPropertyAction visit0 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit1 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit2 = visitorFactory.GetPropertyVisitor(); + return Visit; + + void Visit(ref LogValues value, ref Span spanCookie, ref TCookie cookie) + { + visit0(0, value._value0, ref spanCookie, ref cookie); + visit1(1, value._value1, ref spanCookie, ref cookie); + visit2(2, value._value2, ref spanCookie, ref cookie); + } + } + + public Func, Exception?, string> MessageFormatter { get; } + } + + internal class LogValuesMetadata : LogValuesMetadataBase, ILogMetadata> + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) + { + MessageFormatter = LogMetadataExtensions.CreateStringMessageFormatter(this); + } + + public VisitPropertyListAction, TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory visitorFactory) + { + VisitPropertyAction visit0 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit1 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit2 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit3 = visitorFactory.GetPropertyVisitor(); + return Visit; + + void Visit(ref LogValues value, ref Span spanCookie, ref TCookie cookie) + { + visit0(0, value._value0, ref spanCookie, ref cookie); + visit1(1, value._value1, ref spanCookie, ref cookie); + visit2(2, value._value2, ref spanCookie, ref cookie); + visit3(3, value._value3, ref spanCookie, ref cookie); + } + } + + public Func, Exception?, string> MessageFormatter { get; } + } + + internal class LogValuesMetadata : LogValuesMetadataBase, ILogMetadata> + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) + { + MessageFormatter = LogMetadataExtensions.CreateStringMessageFormatter(this); + } + + public VisitPropertyListAction, TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory visitorFactory) + { + VisitPropertyAction visit0 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit1 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit2 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit3 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit4 = visitorFactory.GetPropertyVisitor(); + return Visit; + + void Visit(ref LogValues value, ref Span spanCookie, ref TCookie cookie) + { + visit0(0, value._value0, ref spanCookie, ref cookie); + visit1(1, value._value1, ref spanCookie, ref cookie); + visit2(2, value._value2, ref spanCookie, ref cookie); + visit3(3, value._value3, ref spanCookie, ref cookie); + visit4(4, value._value4, ref spanCookie, ref cookie); + } + } + + public Func, Exception?, string> MessageFormatter { get; } + } + + internal class LogValuesMetadata : LogValuesMetadataBase, ILogMetadata> + { + public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) + { + MessageFormatter = LogMetadataExtensions.CreateStringMessageFormatter(this); + } + + public VisitPropertyListAction, TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory visitorFactory) + { + VisitPropertyAction visit0 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit1 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit2 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit3 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit4 = visitorFactory.GetPropertyVisitor(); + VisitPropertyAction visit5 = visitorFactory.GetPropertyVisitor(); + return Visit; + + void Visit(ref LogValues value, ref Span spanCookie, ref TCookie cookie) + { + visit0(0, value._value0, ref spanCookie, ref cookie); + visit1(1, value._value1, ref spanCookie, ref cookie); + visit2(2, value._value2, ref spanCookie, ref cookie); + visit3(3, value._value3, ref spanCookie, ref cookie); + visit4(4, value._value4, ref spanCookie, ref cookie); + visit5(5, value._value5, ref spanCookie, ref cookie); + } + } + + public Func, Exception?, string> MessageFormatter { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs index dc40af58f671e9..9e4b184aef5701 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessage.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Runtime.Serialization; using System.Threading; using Microsoft.Extensions.Logging.Abstractions; @@ -25,11 +26,9 @@ public static class LoggerMessage /// A delegate which when invoked creates a log scope. public static Func DefineScope(string formatString) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 0); + LogValuesMetadata metadata = LogValues.CreateMetadata(LogLevel.None, default, formatString); - var logValues = new LogValues(formatter); - - return logger => logger.BeginScope(logValues); + return logger => logger.BeginScope(new LogValues(metadata)); } /// @@ -40,9 +39,9 @@ public static class LoggerMessage /// A delegate which when invoked creates a log scope. public static Func DefineScope(string formatString) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 1); + LogValuesMetadata metadata = LogValues.CreateMetadata(LogLevel.None, default, formatString); - return (logger, arg1) => logger.BeginScope(new LogValues(formatter, arg1)); + return (logger, arg1) => logger.BeginScope(new LogValues(metadata, arg1)); } /// @@ -56,10 +55,7 @@ public static class LoggerMessage { LogValuesMetadata metadata = LogValues.CreateMetadata(LogLevel.None, default, formatString); - return (logger, arg1, arg2) => - { - return logger.BeginScope(new LogValues(metadata, arg1, arg2)); - }; + return (logger, arg1, arg2) => logger.BeginScope(new LogValues(metadata, arg1, arg2)); } /// @@ -72,9 +68,9 @@ public static class LoggerMessage /// A delegate which when invoked creates a log scope. public static Func DefineScope(string formatString) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 3); + LogValuesMetadata metadata = LogValues.CreateMetadata(LogLevel.None, default, formatString); - return (logger, arg1, arg2, arg3) => logger.BeginScope(new LogValues(formatter, arg1, arg2, arg3)); + return (logger, arg1, arg2, arg3) => logger.BeginScope(new LogValues(metadata, arg1, arg2, arg3)); } /// @@ -88,9 +84,9 @@ public static class LoggerMessage /// A delegate which when invoked creates a log scope. public static Func DefineScope(string formatString) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 4); + LogValuesMetadata metadata = LogValues.CreateMetadata(LogLevel.None, default, formatString); - return (logger, arg1, arg2, arg3, arg4) => logger.BeginScope(new LogValues(formatter, arg1, arg2, arg3, arg4)); + return (logger, arg1, arg2, arg3, arg4) => logger.BeginScope(new LogValues(metadata, arg1, arg2, arg3, arg4)); } /// @@ -105,9 +101,9 @@ public static class LoggerMessage /// A delegate which when invoked creates a log scope. public static Func DefineScope(string formatString) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 5); + LogValuesMetadata metadata = LogValues.CreateMetadata(LogLevel.None, default, formatString); - return (logger, arg1, arg2, arg3, arg4, arg5) => logger.BeginScope(new LogValues(formatter, arg1, arg2, arg3, arg4, arg5)); + return (logger, arg1, arg2, arg3, arg4, arg5) => logger.BeginScope(new LogValues(metadata, arg1, arg2, arg3, arg4, arg5)); } /// @@ -123,9 +119,54 @@ public static class LoggerMessage /// A delegate which when invoked creates a log scope. public static Func DefineScope(string formatString) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 6); + LogValuesMetadata metadata = LogValues.CreateMetadata(LogLevel.None, default, formatString); + + return (logger, arg1, arg2, arg3, arg4, arg5, arg6) => logger.BeginScope(new LogValues(metadata, arg1, arg2, arg3, arg4, arg5, arg6)); + } + + public delegate void Log(ILogger logger, ref TState state, Exception? exception); + + private static void LogCore(ref LogEntryPipeline? cachedPipeline, ILogger logger, ILogMetadata? metadata, ref LogEntry entry, bool needFullEnabledCheck) + { + if (cachedPipeline == null || cachedPipeline.UserState != logger || cachedPipeline.CancelToken.IsCancellationRequested) + { + cachedPipeline = GetLogEntryPipeline(metadata, logger); + } + + if (!cachedPipeline.IsEnabled || + (cachedPipeline.IsDynamicLevelCheckRequired && needFullEnabledCheck && !cachedPipeline.IsEnabledDynamic(entry.LogLevel))) + { + return; + } + + cachedPipeline.HandleLogEntry(ref entry); + } + + private static LogEntryPipeline GetLogEntryPipeline(ILogMetadata? metadata, ILogger logger) + { + if (logger is ILogEntryProcessorFactory) + { + ProcessorContext context = ((ILogEntryProcessorFactory)logger).GetProcessor(); + LogEntryHandler handler = context.Processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicEnableCheckRequired); + return new LogEntryPipeline(handler, logger, enabled, dynamicEnableCheckRequired, context.CancellationToken); + } + else + { + return new LogEntryPipeline(new InvokeLoggerLogHandler(logger), logger, true, true, CancellationToken.None); + } + } + + public static Log Define(ILogMetadata metadata, LogDefineOptions? options = null) + { + LogEntryPipeline? pipeline = null; + bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); + return Log; - return (logger, arg1, arg2, arg3, arg4, arg5, arg6) => logger.BeginScope(new LogValues(formatter, arg1, arg2, arg3, arg4, arg5, arg6)); + void Log(ILogger logger, ref TState state, Exception? exception) + { + LogEntry entry = new LogEntry(metadata.LogLevel, category: null!, metadata.EventId, state, exception, null!); + LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); + } } /// @@ -148,25 +189,17 @@ public static class LoggerMessage /// A delegate which when invoked creates a log message. public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 0); + LogValuesMetadata metadata = LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterMetadata); + LogEntryPipeline? pipeline = null; + bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); + return Log; void Log(ILogger logger, Exception? exception) { - logger.Log(logLevel, eventId, new LogValues(formatter), exception, LogValues.Callback); - } - - if (options != null && options.SkipEnabledCheck) - { - return Log; + LogValues state = new LogValues(metadata); + LogEntry entry = new LogEntry(logLevel, category: null!, eventId, state, exception, metadata.MessageFormatter); + LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); } - - return (logger, exception) => - { - if (logger.IsEnabled(logLevel)) - { - Log(logger, exception); - } - }; } /// @@ -191,72 +224,19 @@ void Log(ILogger logger, Exception? exception) /// A delegate which when invoked creates a log message. public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 1); - - void Log(ILogger logger, T1 arg1, Exception? exception) - { - logger.Log(logLevel, eventId, new LogValues(formatter, arg1), exception, LogValues.Callback); - } - - if (options != null && options.SkipEnabledCheck) - { - return Log; - } - - return (logger, arg1, exception) => - { - if (logger.IsEnabled(logLevel)) - { - Log(logger, arg1, exception); - } - }; - } - - public delegate void Log(ILogger logger, ref TState state, Exception? exception); - - public static Log Define(ILogMetadata metadata, LogDefineOptions? options = null) - { - LogEntryPipeline? pipeline = null; + LogValuesMetadata metadata = LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterMetadata); + LogEntryPipeline>? pipeline = null; bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); return Log; - void Log(ILogger logger, ref TState state, Exception? exception) + void Log(ILogger logger, T1 arg1, Exception? exception) { - LogEntry entry = new LogEntry(metadata.LogLevel, category: null!, metadata.EventId, state, exception, null!); + LogValues state = new LogValues(metadata, arg1); + LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, metadata.MessageFormatter); LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); } } - private static void LogCore(ref LogEntryPipeline? cachedPipeline, ILogger logger, ILogMetadata? metadata, ref LogEntry entry, bool needFullEnabledCheck) - { - if (cachedPipeline == null || cachedPipeline.UserState != logger || cachedPipeline.CancelToken.IsCancellationRequested) - { - cachedPipeline = GetLogEntryPipeline(metadata, logger); - } - - if (!cachedPipeline.IsEnabled || - (cachedPipeline.IsDynamicLevelCheckRequired && needFullEnabledCheck && !cachedPipeline.IsEnabledDynamic(entry.LogLevel))) - { - return; - } - - cachedPipeline.HandleLogEntry(ref entry); - } - - private static LogEntryPipeline GetLogEntryPipeline(ILogMetadata? metadata, ILogger logger) - { - if (logger is ILogEntryProcessorFactory) - { - ProcessorContext context = ((ILogEntryProcessorFactory)logger).GetProcessor(); - LogEntryHandler handler = context.Processor.GetLogEntryHandler(metadata, out bool enabled, out bool dynamicEnableCheckRequired); - return new LogEntryPipeline(handler, logger, enabled, dynamicEnableCheckRequired, context.CancellationToken); - } - else - { - return new LogEntryPipeline(new InvokeLoggerLogHandler(logger), logger, true, true, CancellationToken.None); - } - } - /// /// Creates a delegate which can be invoked for logging a message. /// @@ -320,25 +300,17 @@ void Log(ILogger logger, T1 arg1, T2 arg2, Exception? exception) /// A delegate which when invoked creates a log message. public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 3); + LogValuesMetadata metadata = LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterMetadata); + LogEntryPipeline>? pipeline = null; + bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); + return Log; void Log(ILogger logger, T1 arg1, T2 arg2, T3 arg3, Exception? exception) { - logger.Log(logLevel, eventId, new LogValues(formatter, arg1, arg2, arg3), exception, LogValues.Callback); - } - - if (options != null && options.SkipEnabledCheck) - { - return Log; + LogValues state = new LogValues(metadata, arg1, arg2, arg3); + LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, metadata.MessageFormatter); + LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); } - - return (logger, arg1, arg2, arg3, exception) => - { - if (logger.IsEnabled(logLevel)) - { - Log(logger, arg1, arg2, arg3, exception); - } - }; } /// @@ -369,25 +341,17 @@ void Log(ILogger logger, T1 arg1, T2 arg2, T3 arg3, Exception? exception) /// A delegate which when invoked creates a log message. public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 4); + LogValuesMetadata metadata = LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterMetadata); + LogEntryPipeline>? pipeline = null; + bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); + return Log; void Log(ILogger logger, T1 arg1, T2 arg2, T3 arg3, T4 arg4, Exception? exception) { - logger.Log(logLevel, eventId, new LogValues(formatter, arg1, arg2, arg3, arg4), exception, LogValues.Callback); - } - - if (options != null && options.SkipEnabledCheck) - { - return Log; + LogValues state = new LogValues(metadata, arg1, arg2, arg3, arg4); + LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, metadata.MessageFormatter); + LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); } - - return (logger, arg1, arg2, arg3, arg4, exception) => - { - if (logger.IsEnabled(logLevel)) - { - Log(logger, arg1, arg2, arg3, arg4, exception); - } - }; } /// @@ -420,25 +384,17 @@ void Log(ILogger logger, T1 arg1, T2 arg2, T3 arg3, T4 arg4, Exception? exceptio /// A delegate which when invoked creates a log message. public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 5); + LogValuesMetadata metadata = LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterMetadata); + LogEntryPipeline>? pipeline = null; + bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); + return Log; void Log(ILogger logger, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, Exception? exception) { - logger.Log(logLevel, eventId, new LogValues(formatter, arg1, arg2, arg3, arg4, arg5), exception, LogValues.Callback); - } - - if (options != null && options.SkipEnabledCheck) - { - return Log; + LogValues state = new LogValues(metadata, arg1, arg2, arg3, arg4, arg5); + LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, metadata.MessageFormatter); + LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); } - - return (logger, arg1, arg2, arg3, arg4, arg5, exception) => - { - if (logger.IsEnabled(logLevel)) - { - Log(logger, arg1, arg2, arg3, arg4, arg5, exception); - } - }; } /// @@ -473,34 +429,17 @@ void Log(ILogger logger, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, Exception? /// A delegate which when invoked creates a log message. public static Action Define(LogLevel logLevel, EventId eventId, string formatString, LogDefineOptions? options) { - LogValuesFormatter formatter = CreateLogValuesFormatter(formatString, expectedNamedParameterCount: 6); + LogValuesMetadata metadata = LogValues.CreateMetadata(logLevel, eventId, formatString, options?.ParameterMetadata); + LogEntryPipeline>? pipeline = null; + bool needFullEnabledCheck = (options == null || !options.SkipEnabledCheck); + return Log; void Log(ILogger logger, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, Exception? exception) { - logger.Log(logLevel, eventId, new LogValues(formatter, arg1, arg2, arg3, arg4, arg5, arg6), exception, LogValues.Callback); - } - - if (options != null && options.SkipEnabledCheck) - { - return Log; + LogValues state = new LogValues(metadata, arg1, arg2, arg3, arg4, arg5, arg6); + LogEntry> entry = new LogEntry>(logLevel, category: null!, eventId, state, exception, metadata.MessageFormatter); + LogCore(ref pipeline, logger, metadata, ref entry, needFullEnabledCheck); } - - return (logger, arg1, arg2, arg3, arg4, arg5, arg6, exception) => - { - if (logger.IsEnabled(logLevel)) - { - Log(logger, arg1, arg2, arg3, arg4, arg5, arg6, exception); - } - }; - } - - private static LogValuesFormatter CreateLogValuesFormatter(string formatString, int expectedNamedParameterCount) - { - var logValuesFormatter = new LogValuesFormatter(formatString); - - ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount, logValuesFormatter.PropertyCount); - - return logValuesFormatter; } private static void ValidateFormatStringParameterCount(string formatString, int expectedNamedParameterCount, int actualCount) @@ -512,15 +451,13 @@ private static void ValidateFormatStringParameterCount(string formatString, int } } - private readonly struct LogValues : IReadOnlyList> + internal readonly struct LogValues : IReadOnlyList> { - public static readonly Func Callback = (state, exception) => state.ToString(); - - private readonly LogValuesFormatter _formatter; + private readonly LogValuesMetadata _metadata; - public LogValues(LogValuesFormatter formatter) + public LogValues(LogValuesMetadata metadata) { - _formatter = formatter; + _metadata = metadata; } public KeyValuePair this[int index] @@ -529,7 +466,7 @@ public LogValues(LogValuesFormatter formatter) { if (index == 0) { - return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); + return new KeyValuePair("{OriginalFormat}", _metadata.OriginalFormat); } throw new IndexOutOfRangeException(nameof(index)); } @@ -542,24 +479,29 @@ public LogValues(LogValuesFormatter formatter) yield return this[0]; } - public override string ToString() => _formatter.Format(); + public override string ToString() => _metadata.MessageFormatter(this, null); IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, object[]?[]? parameterMetadata = null) + { + var metadata = new LogValuesMetadata(formatString, level, eventId, parameterMetadata); + ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 0, metadata.PropertyCount); + return metadata; + } } - private readonly struct LogValues : IReadOnlyList> + internal readonly struct LogValues : IReadOnlyList> { - public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); - - private readonly LogValuesFormatter _formatter; - private readonly T0 _value0; + private readonly LogValuesMetadata _metadata; + internal readonly T0 _value0; - public LogValues(LogValuesFormatter formatter, T0 value0) + public LogValues(LogValuesMetadata metadata, T0 value0) { - _formatter = formatter; + _metadata = metadata; _value0 = value0; } @@ -570,9 +512,9 @@ public LogValues(LogValuesFormatter formatter, T0 value0) switch (index) { case 0: - return new KeyValuePair(_formatter.GetValueName(0), _value0); + return new KeyValuePair(_metadata.GetValueName(0), _value0); case 1: - return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); + return new KeyValuePair("{OriginalFormat}", _metadata.OriginalFormat); default: throw new IndexOutOfRangeException(nameof(index)); } @@ -589,36 +531,19 @@ public LogValues(LogValuesFormatter formatter, T0 value0) } } - - public override string ToString() => _formatter.Format(_value0); + public override string ToString() => _metadata.MessageFormatter(this, null); IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } - } - internal class LogValuesMetadata : LogValuesMetadata, ILogMetadata> - { - public LogValuesMetadata(string format, LogLevel level, EventId eventId, object[]?[]? metadata = null) : base(format, level, eventId, metadata) + public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, object[]?[]? parameterMetadata = null) { - MessageFormatter = LogMetadataExtensions.CreateStringMessageFormatter(this); - } - - public VisitPropertyListAction, TCookie> CreatePropertyListVisitor(IPropertyVisitorFactory visitorFactory) - { - VisitPropertyAction visit0 = visitorFactory.GetPropertyVisitor(); - VisitPropertyAction visit1 = visitorFactory.GetPropertyVisitor(); - return Visit; - - void Visit(ref LogValues value, ref Span spanCookie, ref TCookie cookie) - { - visit0(0, value._value0, ref spanCookie, ref cookie); - visit1(1, value._value1, ref spanCookie, ref cookie); - } + var metadata = new LogValuesMetadata(formatString, level, eventId, parameterMetadata); + ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 1, metadata.PropertyCount); + return metadata; } - - public Func, Exception?, string> MessageFormatter; } internal readonly struct LogValues : IReadOnlyList> @@ -677,14 +602,20 @@ public static LogValuesMetadata CreateMetadata(LogLevel level, EventId e } } - private readonly struct LogValues : IReadOnlyList> + internal readonly struct LogValues : IReadOnlyList> { - public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); + private readonly LogValuesMetadata _metadata; + internal readonly T0 _value0; + internal readonly T1 _value1; + internal readonly T2 _value2; - private readonly LogValuesFormatter _formatter; - private readonly T0 _value0; - private readonly T1 _value1; - private readonly T2 _value2; + public LogValues(LogValuesMetadata metadata, T0 value0, T1 value1, T2 value2) + { + _metadata = metadata; + _value0 = value0; + _value1 = value1; + _value2 = value2; + } public int Count => 4; @@ -695,28 +626,20 @@ public static LogValuesMetadata CreateMetadata(LogLevel level, EventId e switch (index) { case 0: - return new KeyValuePair(_formatter.GetValueName(0), _value0); + return new KeyValuePair(_metadata.GetValueName(0), _value0); case 1: - return new KeyValuePair(_formatter.GetValueName(1), _value1); + return new KeyValuePair(_metadata.GetValueName(1), _value1); case 2: - return new KeyValuePair(_formatter.GetValueName(2), _value2); + return new KeyValuePair(_metadata.GetValueName(2), _value2); case 3: - return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); + return new KeyValuePair("{OriginalFormat}", _metadata.OriginalFormat); default: throw new IndexOutOfRangeException(nameof(index)); } } } - public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1, T2 value2) - { - _formatter = formatter; - _value0 = value0; - _value1 = value1; - _value2 = value2; - } - - public override string ToString() => _formatter.Format(_value0, _value1, _value2); + public override string ToString() => _metadata.MessageFormatter(this, null); public IEnumerator> GetEnumerator() { @@ -730,17 +653,22 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, object[]?[]? parameterMetadata = null) + { + var metadata = new LogValuesMetadata(formatString, level, eventId, parameterMetadata); + ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 3, metadata.PropertyCount); + return metadata; + } } - private readonly struct LogValues : IReadOnlyList> + internal readonly struct LogValues : IReadOnlyList> { - public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); - - private readonly LogValuesFormatter _formatter; - private readonly T0 _value0; - private readonly T1 _value1; - private readonly T2 _value2; - private readonly T3 _value3; + private readonly LogValuesMetadata _metadata; + internal readonly T0 _value0; + internal readonly T1 _value1; + internal readonly T2 _value2; + internal readonly T3 _value3; public int Count => 5; @@ -751,33 +679,31 @@ IEnumerator IEnumerable.GetEnumerator() switch (index) { case 0: - return new KeyValuePair(_formatter.GetValueName(0), _value0); + return new KeyValuePair(_metadata.GetValueName(0), _value0); case 1: - return new KeyValuePair(_formatter.GetValueName(1), _value1); + return new KeyValuePair(_metadata.GetValueName(1), _value1); case 2: - return new KeyValuePair(_formatter.GetValueName(2), _value2); + return new KeyValuePair(_metadata.GetValueName(2), _value2); case 3: - return new KeyValuePair(_formatter.GetValueName(3), _value3); + return new KeyValuePair(_metadata.GetValueName(3), _value3); case 4: - return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); + return new KeyValuePair("{OriginalFormat}", _metadata.OriginalFormat); default: throw new IndexOutOfRangeException(nameof(index)); } } } - public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1, T2 value2, T3 value3) + public LogValues(LogValuesMetadata metadata, T0 value0, T1 value1, T2 value2, T3 value3) { - _formatter = formatter; + _metadata = metadata; _value0 = value0; _value1 = value1; _value2 = value2; _value3 = value3; } - private object?[] ToArray() => new object?[] { _value0, _value1, _value2, _value3 }; - - public override string ToString() => _formatter.FormatWithOverwrite(ToArray()); + public override string ToString() => _metadata.MessageFormatter(this, null); public IEnumerator> GetEnumerator() { @@ -791,18 +717,33 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, object[]?[]? parameterMetadata = null) + { + var metadata = new LogValuesMetadata(formatString, level, eventId, parameterMetadata); + ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 4, metadata.PropertyCount); + return metadata; + } } - private readonly struct LogValues : IReadOnlyList> + internal readonly struct LogValues : IReadOnlyList> { - public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); + private readonly LogValuesMetadata metadata; + internal readonly T0 _value0; + internal readonly T1 _value1; + internal readonly T2 _value2; + internal readonly T3 _value3; + internal readonly T4 _value4; - private readonly LogValuesFormatter _formatter; - private readonly T0 _value0; - private readonly T1 _value1; - private readonly T2 _value2; - private readonly T3 _value3; - private readonly T4 _value4; + public LogValues(LogValuesMetadata metadata, T0 value0, T1 value1, T2 value2, T3 value3, T4 value4) + { + this.metadata = metadata; + _value0 = value0; + _value1 = value1; + _value2 = value2; + _value3 = value3; + _value4 = value4; + } public int Count => 6; @@ -813,36 +754,24 @@ IEnumerator IEnumerable.GetEnumerator() switch (index) { case 0: - return new KeyValuePair(_formatter.GetValueName(0), _value0); + return new KeyValuePair(metadata.GetValueName(0), _value0); case 1: - return new KeyValuePair(_formatter.GetValueName(1), _value1); + return new KeyValuePair(metadata.GetValueName(1), _value1); case 2: - return new KeyValuePair(_formatter.GetValueName(2), _value2); + return new KeyValuePair(metadata.GetValueName(2), _value2); case 3: - return new KeyValuePair(_formatter.GetValueName(3), _value3); + return new KeyValuePair(metadata.GetValueName(3), _value3); case 4: - return new KeyValuePair(_formatter.GetValueName(4), _value4); + return new KeyValuePair(metadata.GetValueName(4), _value4); case 5: - return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); + return new KeyValuePair("{OriginalFormat}", metadata.OriginalFormat); default: throw new IndexOutOfRangeException(nameof(index)); } } } - public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1, T2 value2, T3 value3, T4 value4) - { - _formatter = formatter; - _value0 = value0; - _value1 = value1; - _value2 = value2; - _value3 = value3; - _value4 = value4; - } - - private object?[] ToArray() => new object?[] { _value0, _value1, _value2, _value3, _value4 }; - - public override string ToString() => _formatter.FormatWithOverwrite(ToArray()); + public override string ToString() => metadata.MessageFormatter(this, null); public IEnumerator> GetEnumerator() { @@ -856,19 +785,35 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, object[]?[]? parameterMetadata = null) + { + var metadata = new LogValuesMetadata(formatString, level, eventId, parameterMetadata); + ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 5, metadata.PropertyCount); + return metadata; + } } - private readonly struct LogValues : IReadOnlyList> + internal readonly struct LogValues : IReadOnlyList> { - public static readonly Func, Exception?, string> Callback = (state, exception) => state.ToString(); + private readonly LogValuesMetadata _metadata; + internal readonly T0 _value0; + internal readonly T1 _value1; + internal readonly T2 _value2; + internal readonly T3 _value3; + internal readonly T4 _value4; + internal readonly T5 _value5; - private readonly LogValuesFormatter _formatter; - private readonly T0 _value0; - private readonly T1 _value1; - private readonly T2 _value2; - private readonly T3 _value3; - private readonly T4 _value4; - private readonly T5 _value5; + public LogValues(LogValuesMetadata metadata, T0 value0, T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + _metadata = metadata; + _value0 = value0; + _value1 = value1; + _value2 = value2; + _value3 = value3; + _value4 = value4; + _value5 = value5; + } public int Count => 7; @@ -879,39 +824,26 @@ IEnumerator IEnumerable.GetEnumerator() switch (index) { case 0: - return new KeyValuePair(_formatter.GetValueName(0), _value0); + return new KeyValuePair(_metadata.GetValueName(0), _value0); case 1: - return new KeyValuePair(_formatter.GetValueName(1), _value1); + return new KeyValuePair(_metadata.GetValueName(1), _value1); case 2: - return new KeyValuePair(_formatter.GetValueName(2), _value2); + return new KeyValuePair(_metadata.GetValueName(2), _value2); case 3: - return new KeyValuePair(_formatter.GetValueName(3), _value3); + return new KeyValuePair(_metadata.GetValueName(3), _value3); case 4: - return new KeyValuePair(_formatter.GetValueName(4), _value4); + return new KeyValuePair(_metadata.GetValueName(4), _value4); case 5: - return new KeyValuePair(_formatter.GetValueName(5), _value5); + return new KeyValuePair(_metadata.GetValueName(5), _value5); case 6: - return new KeyValuePair("{OriginalFormat}", _formatter.OriginalFormat); + return new KeyValuePair("{OriginalFormat}", _metadata.OriginalFormat); default: throw new IndexOutOfRangeException(nameof(index)); } } } - public LogValues(LogValuesFormatter formatter, T0 value0, T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) - { - _formatter = formatter; - _value0 = value0; - _value1 = value1; - _value2 = value2; - _value3 = value3; - _value4 = value4; - _value5 = value5; - } - - private object?[] ToArray() => new object?[] { _value0, _value1, _value2, _value3, _value4, _value5 }; - - public override string ToString() => _formatter.FormatWithOverwrite(ToArray()); + public override string ToString() => _metadata.MessageFormatter(this, null); public IEnumerator> GetEnumerator() { @@ -925,6 +857,13 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + public static LogValuesMetadata CreateMetadata(LogLevel level, EventId eventId, string formatString, object[]?[]? parameterMetadata = null) + { + var metadata = new LogValuesMetadata(formatString, level, eventId, parameterMetadata); + ValidateFormatStringParameterCount(formatString, expectedNamedParameterCount: 6, metadata.PropertyCount); + return metadata; + } } } } From 1e864066e55677fb50b1dd2c90a0290c335990a8 Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Thu, 15 Jun 2023 22:36:04 -0700 Subject: [PATCH 25/26] Feedback from James --- .../Microsoft.Extensions.Logging.Abstractions.cs | 2 +- .../src/FormattedLogValues.cs | 6 +++--- .../src/FormattingState.cs | 8 +++++--- .../src/LogEntryNew.cs | 3 ++- .../src/LogMetadataExtensions.cs | 16 +++++++++------- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index 8ec6fc0e6c45ca..255a83f62b0dd4 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -195,7 +195,7 @@ public partial struct LogPropertyInfo private object _dummy; private int _dummyPrimitive; public LogPropertyInfo(string name, object[]? metadata) { throw null; } - public readonly object[]? Metadata { get { throw null; } } + public readonly System.Collections.Generic.IReadOnlyList? Metadata { get { throw null; } } public readonly string Name { get { throw null; } } } public readonly partial struct ProcessorContext diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs index 8ec7402379488a..cc8a6bad7f429c 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs @@ -118,9 +118,9 @@ IEnumerator IEnumerable.GetEnumerator() } } - internal class FormattedLogValuesMetadata : ILogMetadata + internal sealed class FormattedLogValuesMetadata : ILogMetadata { - private LogPropertyInfo[] _propertyInfo; + private readonly LogPropertyInfo[] _propertyInfo; private Func? _formatter; public FormattedLogValuesMetadata(string originalFormat) { @@ -130,7 +130,7 @@ public FormattedLogValuesMetadata(string originalFormat) public LogLevel LogLevel => throw new NotImplementedException(); public EventId EventId => throw new NotImplementedException(); - public string OriginalFormat { get; private set; } + public string OriginalFormat { get; } public int PropertyCount => _propertyInfo != null ? _propertyInfo.Length : 0; public LogPropertyInfo GetPropertyInfo(int index) => _propertyInfo[index]; public VisitPropertyListAction CreatePropertyListVisitor(IPropertyVisitorFactory propertyVisitorFactory) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattingState.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattingState.cs index 80cc339536cf41..4dc97af772225f 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattingState.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattingState.cs @@ -32,7 +32,8 @@ namespace Microsoft.Extensions.Logging /// /// Usage: /// 1. Construct with a CompositeFormat and an IBufferWriter that receives the formatted data - /// 2. Call Init() to get an initial Span. A ref to this span will need to passed into each subsequent call. + /// 2. Call Init() to get an initial Span. A ref to this span will need to passed into each subsequent call. If the + /// span is ever too small a new Span will automatically be created from the IBufferWriter. /// 3. In order from left to right for each hole in the format string call AppendPropertyUtf16 or AppendSpanPropertyUtf16 /// to provide the value that fills that hole. /// 4. Call Finish() to flush @@ -42,10 +43,11 @@ namespace Microsoft.Extensions.Logging /// internal struct FormattingState { - private InternalCompositeFormat _format; + private readonly InternalCompositeFormat _format; + private readonly IBufferWriter _writer; private int _index; private int _allocated; - private IBufferWriter _writer; + private const string NullValue = "(null)"; private const int GuessedLengthPerHole = 11; diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs index 74edb6a7071942..df64f28d8d2e78 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntryNew.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Threading; using Microsoft.Extensions.Logging.Abstractions; @@ -70,7 +71,7 @@ public LogPropertyInfo(string name, object[]? metadata = null) Metadata = metadata; } public string Name { get; } - public object[]? Metadata { get; internal set; } + public IReadOnlyList? Metadata { get; internal set; } } public interface ILogMetadata diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogMetadataExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogMetadataExtensions.cs index 3a7e8af91fa421..0b3ec105971d1e 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogMetadataExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogMetadataExtensions.cs @@ -46,21 +46,23 @@ private class Slot { public PooledByteBufferWriter? Buffer; } string FormatMessage(TState state, Exception? exception) { - Slot? tstate = t_slot.Value; - if (tstate == null) + Slot? slot = t_slot.Value; + if (slot == null) { - tstate = new Slot(); - t_slot.Value = tstate; + slot = new Slot(); + t_slot.Value = slot; } - PooledByteBufferWriter buffer = tstate.Buffer ?? new PooledByteBufferWriter(256); - tstate.Buffer = null; + PooledByteBufferWriter buffer = slot.Buffer ?? new PooledByteBufferWriter(256); + //Setting this to null ensures that if there is a re-entrant call on the same thread or + //code throws an exception we will not reuse a partially written buffer. + slot.Buffer = null; FormattingState formattingState = new FormattingState(format, buffer); formattingState.Init(out Span span); visitProperties(ref state, ref span, ref formattingState); formattingState.Finish(ref span); string ret = MemoryMarshal.Cast(buffer.WrittenMemory.Span).ToString(); buffer.Clear(); - tstate.Buffer = buffer; + slot.Buffer = buffer; return ret; } } From 7a270121b949a10c744a2f2132fdeb897ebc62e1 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 20 Jun 2023 16:20:42 -0700 Subject: [PATCH 26/26] Using new data classification model --- .../Microsoft.Extensions.Logging.Tests.csproj | 19 +- .../Compliance/DataClassification.cs | 155 ++++++++++++++ .../Compliance/DataClassificationAttribute.cs | 44 ++++ .../Redaction/Compliance/IRedactorBuilder.cs | 37 ++++ .../Redaction/Compliance/IRedactorProvider.cs | 19 ++ .../Compliance/PrivateDataAttributes.cs | 20 ++ .../Compliance/PublicDataAttribute.cs | 20 ++ .../Common/Redaction/Compliance/Redactor.cs | 199 ++++++++++++++++++ .../Compliance/SimpleClassifications.cs | 27 +++ .../Redaction/Compliance/SimpleTaxonomy.cs | 34 +++ .../Common/Redaction/RedactionProcessor.cs | 44 +--- .../tests/Common/Redaction/RedactionTests.cs | 42 +--- .../tests/Common/Redaction/TestRedactor.cs | 34 +++ 13 files changed, 611 insertions(+), 83 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/DataClassification.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/DataClassificationAttribute.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/IRedactorBuilder.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/IRedactorProvider.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/PrivateDataAttributes.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/PublicDataAttribute.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/Redactor.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/SimpleClassifications.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/SimpleTaxonomy.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/TestRedactor.cs diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Microsoft.Extensions.Logging.Tests.csproj b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Microsoft.Extensions.Logging.Tests.csproj index 6586026447c58a..c044487bb29ec5 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Microsoft.Extensions.Logging.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Microsoft.Extensions.Logging.Tests.csproj @@ -3,21 +3,16 @@ $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent);$(NetFrameworkMinimum) true + true - - - - - - + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/DataClassification.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/DataClassification.cs new file mode 100644 index 00000000000000..f751e1fb69d1ce --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/DataClassification.cs @@ -0,0 +1,155 @@ +// 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; + +namespace Microsoft.Extensions.Compliance.Classification; + +/// +/// Represents a set of data classes as a part of a data taxonomy. +/// +public readonly struct DataClassification : IEquatable +{ + /// + /// Represents unclassified data. + /// + public const ulong NoneTaxonomyValue = 0UL; + + /// + /// Represents the unknown classification. + /// + public const ulong UnknownTaxonomyValue = 1UL << 63; + + /// + /// Gets the value to represent data with no defined classification. + /// + public static DataClassification None => new(NoneTaxonomyValue); + + /// + /// Gets the value to represent data with an unknown classification. + /// + public static DataClassification Unknown => new(UnknownTaxonomyValue); + + /// + /// Gets the name of the taxonomy that recognizes this classification. + /// + public string TaxonomyName { get; } + + /// + /// Gets the bit mask representing the data classes. + /// + public ulong Value { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The name of the taxonomy this classification belongs to. + /// The taxonomy-specific bit vector representing the data classes. + /// Bit 63, which corresponds to , is set in the value. + public DataClassification(string taxonomyName, ulong value) + { + if (string.IsNullOrEmpty(taxonomyName)) + { + throw new ArgumentNullException(nameof(taxonomyName)); + } + + TaxonomyName = taxonomyName; + Value = value; + + if (((value & UnknownTaxonomyValue) != 0) || (value == NoneTaxonomyValue)) + { + throw new ArgumentException($"Cannot create a classification with a value of 0x{value:x}.", nameof(value)); + } + } + + private DataClassification(ulong taxonomyValue) + { + TaxonomyName = string.Empty; + Value = taxonomyValue; + } + + /// + /// Checks if object is equal to this instance of . + /// + /// Object to check for equality. + /// if object instances are equal otherwise. + public override bool Equals(object? obj) => (obj is DataClassification dc) && Equals(dc); + + /// + /// Checks if object is equal to this instance of . + /// + /// Instance of to check for equality. + /// if object instances are equal otherwise. + public bool Equals(DataClassification other) => other.TaxonomyName == TaxonomyName && other.Value == Value; + + /// + /// Get the hash code the current instance. + /// + /// Hash code. + public override int GetHashCode() + { + return HashCode.Combine(TaxonomyName, Value); + } + + /// + /// Check if two instances are equal. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// if object instances are equal, or otherwise. + public static bool operator ==(DataClassification left, DataClassification right) + { + return left.Equals(right); + } + + /// + /// Check if two instances are not equal. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// if object instances are equal, or otherwise. + public static bool operator !=(DataClassification left, DataClassification right) + { + return !left.Equals(right); + } + + /// + /// Combines together two data classifications. + /// + /// The first classification to combine. + /// The second classification to combine. + /// A new classification object representing the combination of the two input classifications. + /// The two classifications aren't part of the same taxonomy. + public static DataClassification Combine(DataClassification left, DataClassification right) + { + if (string.IsNullOrEmpty(left.TaxonomyName)) + { + return (left.Value == NoneTaxonomyValue) ? right : Unknown; + } + else if (string.IsNullOrEmpty(right.TaxonomyName)) + { + return (right.Value == NoneTaxonomyValue) ? left : Unknown; + } + + if (left.TaxonomyName != right.TaxonomyName) + { + throw new ArgumentException($"Mismatched data taxonomies: {left.TaxonomyName} and {right.TaxonomyName} cannot be combined", nameof(right)); + } + + return new(left.TaxonomyName, left.Value | right.Value); + } + + /// + /// Combines together two data classifications. + /// + /// The first classification to combine. + /// The second classification to combine. + /// A new classification object representing the combination of the two input classifications. + /// The two classifications aren't part of the same taxonomy. + [SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "It's called Combine")] + public static DataClassification operator |(DataClassification left, DataClassification right) + { + return Combine(left, right); + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/DataClassificationAttribute.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/DataClassificationAttribute.cs new file mode 100644 index 00000000000000..117b793e183aae --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/DataClassificationAttribute.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Compliance.Classification; + +/// +/// Base attribute for data classification. +/// +[AttributeUsage( + AttributeTargets.Field + | AttributeTargets.Property + | AttributeTargets.Parameter + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Interface + | AttributeTargets.ReturnValue + | AttributeTargets.GenericParameter, + AllowMultiple = true)] +#pragma warning disable CA1813 // Avoid unsealed attributes +public class DataClassificationAttribute : Attribute +#pragma warning restore CA1813 // Avoid unsealed attributes +{ + /// + /// Gets or sets the notes. + /// + /// Optional free-form text to provide context during a privacy audit. + public string Notes { get; set; } = string.Empty; + + /// + /// Gets the data class represented by this attribute. + /// + public DataClassification Classification { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The data classification to apply. + protected DataClassificationAttribute(DataClassification classification) + { + Classification = classification; + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/IRedactorBuilder.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/IRedactorBuilder.cs new file mode 100644 index 00000000000000..30148f45a1563c --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/IRedactorBuilder.cs @@ -0,0 +1,37 @@ +// 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.Compliance.Classification; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Adds redactors to the application. +/// +public interface IRedactionBuilder +{ + /// + /// Gets the service collection into which the redactor instances are registered. + /// + IServiceCollection Services { get; } + + /// + /// Sets the redactor to use for a set of data classes. + /// + /// Redactor type. + /// The data classes for which the redactor type should be used. + /// The value of this instance. + /// is . + IRedactionBuilder SetRedactor(params DataClassification[] classifications) + where T : Redactor; + + /// + /// Sets the redactor to use when processing classified data for which no specific redactor has been registered. + /// + /// Redactor type. + /// The value of this instance. + IRedactionBuilder SetFallbackRedactor() + where T : Redactor; +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/IRedactorProvider.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/IRedactorProvider.cs new file mode 100644 index 00000000000000..fc6e6f14d59632 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/IRedactorProvider.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 Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Provides redactors for different data classes. +/// +public interface IRedactorProvider +{ + /// + /// Gets the redactor configured to handle the specified data class. + /// + /// Data classification of the data to redact. + /// A redactor suitable to redact data of the given class. + Redactor GetRedactor(DataClassification classification); +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/PrivateDataAttributes.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/PrivateDataAttributes.cs new file mode 100644 index 00000000000000..843b980a00be05 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/PrivateDataAttributes.cs @@ -0,0 +1,20 @@ +// 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.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Private data. +/// +public sealed class PrivateDataAttribute : DataClassificationAttribute +{ + /// + /// Initializes a new instance of the class. + /// + public PrivateDataAttribute() + : base(SimpleClassifications.PrivateData) + { + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/PublicDataAttribute.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/PublicDataAttribute.cs new file mode 100644 index 00000000000000..8d1b0aef53562f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/PublicDataAttribute.cs @@ -0,0 +1,20 @@ +// 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.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Public data. +/// +public sealed class PublicDataAttribute : DataClassificationAttribute +{ + /// + /// Initializes a new instance of the class. + /// + public PublicDataAttribute() + : base(SimpleClassifications.PublicData) + { + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/Redactor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/Redactor.cs new file mode 100644 index 00000000000000..233fdedfe694cd --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/Redactor.cs @@ -0,0 +1,199 @@ +// 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 System.Runtime.CompilerServices; + +#if !NETCOREAPP3_1_OR_GREATER +using System.Buffers; +#endif + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Enables the redaction of potentially sensitive data. +/// +public abstract class Redactor +{ +#if NET6_0_OR_GREATER + private const int MaximumStackAllocation = 256; +#endif + + /// + /// Redacts potentially sensitive data. + /// + /// Value to redact. + /// Redacted value. + public string Redact(ReadOnlySpan source) + { + if (source.IsEmpty) + { + return string.Empty; + } + + var length = GetRedactedLength(source); + +#if NETCOREAPP3_1_OR_GREATER + unsafe + { +#pragma warning disable 8500 + return string.Create( + length, + (this, (IntPtr)(&source)), + (destination, state) => state.Item1.Redact(*(ReadOnlySpan*)state.Item2, destination)); +#pragma warning restore 8500 + } +#else + var buffer = ArrayPool.Shared.Rent(length); + + try + { + var charsWritten = Redact(source, buffer); + var redactedString = new string(buffer, 0, charsWritten); + + return redactedString; + } + finally + { + ArrayPool.Shared.Return(buffer); + } +#endif + } + + /// + /// Redacts potentially sensitive data. + /// + /// Value to redact. + /// Buffer to store redacted value. + /// Number of characters produced when redacting the given source input. + /// is too small. + public abstract int Redact(ReadOnlySpan source, Span destination); + + /// + /// Redacts potentially sensitive data. + /// + /// Value to redact. + /// Buffer to redact into. + /// + /// Returns 0 when is . + /// + /// Number of characters written to the buffer. + /// is too small. + public int Redact(string? source, Span destination) => Redact(source.AsSpan(), destination); + + /// + /// Redacts potentially sensitive data. + /// + /// Value to redact. + /// Redacted value. + /// + /// Returns an empty string when is . + /// + /// is . + public virtual string Redact(string? source) => Redact(source.AsSpan()); + + /// + /// Redacts potentially sensitive data. + /// + /// Type of value to redact. + /// Value to redact. + /// + /// The optional format that selects the specific formatting operation performed. Refer to the + /// documentation of the type being formatted to understand the values you can supply here. + /// + /// Format provider to retrieve format for span formattable. + /// Redacted value. + /// is . + [SuppressMessage("Minor Code Smell", "S3247:Duplicate casts should not be made", Justification = "Avoid pattern matching to improve jitted code")] + public string Redact(T value, string? format = null, IFormatProvider? provider = null) + { +#if NET6_0_OR_GREATER + if (value is ISpanFormattable) + { + Span buffer = stackalloc char[MaximumStackAllocation]; + + // Stryker disable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it. + // Null forgiving operator: The null case is checked with default equality comparer, but compiler doesn't understand it. + if (((ISpanFormattable)value).TryFormat(buffer, out var written, format.AsSpan(), provider)) + { + // Stryker enable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it. + + var formatted = buffer.Slice(0, written); + var length = GetRedactedLength(formatted); + + unsafe + { +#pragma warning disable 8500 + return string.Create( + length, + (this, (IntPtr)(&formatted)), + (destination, state) => state.Item1.Redact(*(ReadOnlySpan*)state.Item2, destination)); +#pragma warning restore 8500 + } + } + } +#endif + + if (value is IFormattable) + { + return Redact(((IFormattable)value).ToString(format, provider)); + } + + return Redact(value?.ToString()); + } + + /// + /// Redacts potentially sensitive data. + /// + /// Type of value to redact. + /// Value to redact. + /// Buffer to redact into. + /// + /// The optional format string that selects the specific formatting operation performed. Refer to the + /// documentation of the type being formatted to understand the values you can supply here. + /// + /// Format provider to retrieve format for span formattable. + /// Number of characters written to the buffer. + /// is . + [SuppressMessage("Minor Code Smell", "S3247:Duplicate casts should not be made", Justification = "Avoid pattern matching to improve jitted code")] + public int Redact(T value, Span destination, string? format = null, IFormatProvider? provider = null) + { +#if NET6_0_OR_GREATER + if (value is ISpanFormattable) + { + Span buffer = stackalloc char[MaximumStackAllocation]; + + // Stryker disable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it. + if (((ISpanFormattable)value).TryFormat(buffer, out var written, format.AsSpan(), provider)) + { + // Stryker enable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it. + var formatted = buffer.Slice(0, written); + + return Redact(formatted, destination); + } + } +#endif + + if (value is IFormattable) + { + return Redact(((IFormattable)value).ToString(format, provider), destination); + } + + return Redact(value?.ToString(), destination); + } + + /// + /// Gets the number of characters produced by redacting the input. + /// + /// Value to be redacted. + /// Minimum buffer size. + public abstract int GetRedactedLength(ReadOnlySpan input); + + /// + /// Gets the number of characters produced by redacting the input. + /// + /// Value to be redacted. + /// Minimum buffer size. + public int GetRedactedLength(string? input) => GetRedactedLength(input.AsSpan()); +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/SimpleClassifications.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/SimpleClassifications.cs new file mode 100644 index 00000000000000..4f1375514ac3ab --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/SimpleClassifications.cs @@ -0,0 +1,27 @@ +// 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.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Simple data classifications. +/// +public static class SimpleClassifications +{ + /// + /// Gets the name of this classification taxonomy. + /// + public static string TaxonomyName => typeof(SimpleTaxonomy).FullName!; + + /// + /// Gets the private data classification. + /// + public static DataClassification PrivateData => new(TaxonomyName, (ulong)SimpleTaxonomy.PrivateData); + + /// + /// Gets the public data classification. + /// + public static DataClassification PublicData => new(TaxonomyName, (ulong)SimpleTaxonomy.PublicData); +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/SimpleTaxonomy.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/SimpleTaxonomy.cs new file mode 100644 index 00000000000000..22484630715daf --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/Compliance/SimpleTaxonomy.cs @@ -0,0 +1,34 @@ +// 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.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Classes of data used for simple scenarios. +/// +[Flags] +public enum SimpleTaxonomy : ulong +{ + /// + /// No data classification. + /// + None = DataClassification.NoneTaxonomyValue, + + /// + /// This is public data. + /// + PublicData = 1 << 0, + + /// + /// This is private data. + /// + PrivateData = 1 << 1, + + /// + /// Unknown data classification, handle with care. + /// + Unknown = DataClassification.UnknownTaxonomyValue, +} diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs index 8f94c61c677744..c6b2ff1bca7a09 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionProcessor.cs @@ -12,6 +12,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; @@ -26,36 +28,6 @@ public static ILoggingBuilder AddRedactionProcessor(this ILoggingBuilder builder } } - internal interface IRedactorProvider - { - IRedactor GetRedactor(DataClass dataClass); - } - - internal interface IRedactor - { - string Redact(ReadOnlySpan source); - int Redact(ReadOnlySpan source, Span destination); - int GetRedactedLength(ReadOnlySpan source); - } - - internal enum DataClass - { - Unknown, - EUPI - } - - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true)] - internal abstract class DataClassificationAttribute : Attribute - { - public string Notes { get; set; } = string.Empty; - public DataClass DataClass { get; } - - protected DataClassificationAttribute(DataClass dataClass) - { - DataClass = dataClass; - } - } - internal class RedactionProcessor : ILogEntryProcessor { private readonly ILogEntryProcessor _nextProcessor; @@ -95,7 +67,7 @@ PropertyRedaction[] GetPolicyRedactions(ILogMetadata metadata) DataClassificationAttribute? dataClassAttr = propMetadata.Metadata.OfType().FirstOrDefault(); if (dataClassAttr != null) { - redactions.Add(new PropertyRedaction(i, _redactorProvider!.GetRedactor(dataClassAttr.DataClass))); + redactions.Add(new PropertyRedaction(i, _redactorProvider!.GetRedactor(dataClassAttr.Classification))); } } return redactions.ToArray(); @@ -134,13 +106,13 @@ public override bool IsEnabled(LogLevel level) internal struct PropertyRedaction { - public PropertyRedaction(int index, IRedactor redactor) + public PropertyRedaction(int index, Redactor redactor) { Index = index; Redactor = redactor; } public int Index; - public IRedactor Redactor; + public Redactor Redactor; } internal class RedactedLogMetadata : ILogMetadata> @@ -155,7 +127,7 @@ public RedactedLogMetadata(ILogMetadata metadata, PropertyRedaction[] redacti _redactions = redactions; } - public IRedactor? GetPropertyRedactor(int index) + public Redactor? GetPropertyRedactor(int index) { for (var i = 0; i < _redactions.Length; i++) { @@ -214,7 +186,7 @@ public VisitPropertyAction GetPropertyVisitor() void Visit(int propIndex, PropType value, ref Span spanCookie, ref TCookie cookie) { - IRedactor? redactor = _metadata.GetPropertyRedactor(propIndex); + Redactor? redactor = _metadata.GetPropertyRedactor(propIndex); if(redactor == null) { unredactedVisit(propIndex, value, ref spanCookie, ref cookie); @@ -279,7 +251,7 @@ public VisitSpanPropertyAction GetSpanPropertyVisitor() void Visit(int propIndex, scoped ReadOnlySpan value, ref Span spanCookie, ref TCookie cookie) { - IRedactor? redactor = _metadata.GetPropertyRedactor(propIndex); + Redactor? redactor = _metadata.GetPropertyRedactor(propIndex); if (redactor == null) { _spanVisitor(propIndex, value, ref spanCookie, ref cookie); diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs index 6700cdf4cb1049..2cce1cd4089ffe 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/RedactionTests.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Test; using Microsoft.Extensions.Logging.Testing; @@ -33,7 +35,7 @@ public void Redact_Message() Assert.Equal(2, sink.Writes.Count); Assert.Equal("User Frank has now 76 status", sink.Writes.ElementAt(0).Message); - Assert.Equal("User [Redacted - EUPI] has now 76 status", sink.Writes.ElementAt(1).Message); + Assert.Equal("User [Redacted - 2] has now 76 status", sink.Writes.ElementAt(1).Message); } [Fact] @@ -53,14 +55,14 @@ public void Redact_StateValues() Assert.Equal(1, sink.Writes.Count); var write = sink.Writes.ElementAt(0); - Assert.Equal("User [Redacted - EUPI] has now 76 status", write.Message); + Assert.Equal("User [Redacted - 2] has now 76 status", write.Message); var values = Assert.IsAssignableFrom>>(write.State); Assert.Collection(values, kvp => { Assert.Equal("username", kvp.Key); - Assert.Equal("[Redacted - EUPI]", kvp.Value); + Assert.Equal("[Redacted - 2]", kvp.Value); }, kvp => { @@ -73,36 +75,6 @@ public void Redact_StateValues() Assert.Equal("User {username} has now {status} status", kvp.Value); }); } - - private sealed class TestRedactorProvider : IRedactorProvider - { - public IRedactor GetRedactor(DataClass dataClass) => new TestRedactor(dataClass); - } - - private sealed class TestRedactor : IRedactor - { - private readonly string _redactedText; - - public TestRedactor(DataClass dataClass) - { - _redactedText = $"[Redacted - {dataClass}]"; - } - - public int GetRedactedLength(ReadOnlySpan source) => _redactedText.Length; - public string Redact(ReadOnlySpan source) => _redactedText; - public int Redact(ReadOnlySpan source, Span destination) - { - _redactedText.AsSpan().CopyTo(destination); - return _redactedText.Length; - } - } - } - - internal class EUPIAttribute : DataClassificationAttribute - { - public EUPIAttribute() : base(DataClass.EUPI) - { - } } public static partial class LogMessages @@ -122,7 +94,7 @@ public static void LogName(this ILogger logger, string username, int status) //[LoggerMessage(2, LogLevel.Information, "User {username} has now {status} status")] - public static void LogNameRedacted(this ILogger logger, [EUPI] string username, int status) + public static void LogNameRedacted(this ILogger logger, [PrivateData] string username, int status) { // manually writing the code the source generator is proposed to create __LogNameRedactedCallback(logger, username, status, null); @@ -133,7 +105,7 @@ public static void LogNameRedacted(this ILogger logger, [EUPI] string username, LogLevel.Information, new EventId(1, nameof(LogNameRedacted)), "User {username} has now {status} status", - new LogDefineOptions() { ParameterMetadata = new Attribute[]?[] { new Attribute[] { new EUPIAttribute() }, null } }); + new LogDefineOptions() { ParameterMetadata = new Attribute[]?[] { new Attribute[] { new PrivateDataAttribute() }, null } }); } } diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/TestRedactor.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/TestRedactor.cs new file mode 100644 index 00000000000000..ffe9cf2af7f35d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/Redaction/TestRedactor.cs @@ -0,0 +1,34 @@ +// 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.Globalization; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; + +namespace Microsoft.Extensions.Logging.Tests.Redaction +{ + + internal sealed class TestRedactorProvider : IRedactorProvider + { + public Redactor GetRedactor(DataClassification classification) => new TestRedactor(classification); + } + + internal sealed class TestRedactor : Redactor + { + private readonly string _redactedText; + + public TestRedactor(DataClassification classification) + { + _redactedText = $"[Redacted - {classification.Value.ToString(CultureInfo.InvariantCulture)}]"; + } + + override public int GetRedactedLength(ReadOnlySpan source) => _redactedText.Length; + + override public int Redact(ReadOnlySpan source, Span destination) + { + _redactedText.AsSpan().CopyTo(destination); + return _redactedText.Length; + } + } +}