├── test ├── .gitignore ├── Rendering │ ├── Verified │ │ ├── MarginControlRendererTests.RenderWritesExpectedOutputForAdjust.DotNet.verified.txt │ │ ├── MarginControlRendererTests.RenderWritesExpectedOutputForSet.DotNet.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesAggregateInnerExceptions.Core.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesAggregateInnerExceptions.DotNet.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesSourcePaths.Core.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesSourcePaths.DotNet.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesParameterNames.Core.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesParameterTypes.DotNet.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesParameterNames.DotNet.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesParameterTypes.Core.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesSourceLocations.Core.verified.txt │ │ ├── ExceptionRendererTests.RenderWritesExpected.Core.verified.txt │ │ ├── ExceptionRendererTests.RenderHidesSourceLocations.DotNet.verified.txt │ │ ├── ExceptionRendererTests.RenderWritesExpected.DotNet.verified.txt │ │ ├── ExceptionRendererTests.RenderLimitsStackFrames.DotNet.verified.txt │ │ └── ExceptionRendererTests.RenderLimitsStackFrames.Core.verified.txt │ ├── StaticSpanRendererTests.cs │ ├── NewLineRendererTests.cs │ ├── ProcessIdRendererTests.cs │ ├── ThreadIdRendererTests.cs │ ├── DateTimeRendererTests.cs │ ├── ActivityIdRendererTests.cs │ ├── MarginControlRendererTests.cs │ ├── LogLevelRendererTests.cs │ ├── MessageRendererTests.cs │ └── CategoryNameRendererTests.cs ├── Destructuring │ ├── DestructuringWriterTests.WriteObjectRendersExpectedContent.verified.txt │ ├── DestructuringWriterTests.WriteDictionaryRendersExpectedContent.verified.txt │ ├── DestructuringWriterTests.WriteDictionaryRespectsMaxDepth.verified.txt │ ├── DestructuringWriterTests.WriteObjectIndented.verified.txt │ └── CompiledWriterCacheTests.cs ├── Formatting │ ├── NullValueTests.cs │ ├── MultiTypeFormatProviderTests.cs │ ├── IntegralFormattingTests.cs │ ├── ValueFormatterTests.cs │ ├── ProviderFormatterTests.cs │ └── MultiTypeFormatterTests.cs ├── Infrastructure │ ├── Factory.cs │ ├── Person.cs │ ├── ExceptionHelper.cs │ ├── SharedSettings.cs │ └── RendererTestHarness.cs ├── DependencyInjectionTests.cs ├── Options │ ├── SerilogStyleLoggerOptionsTests.cs │ └── MicrosoftStyleLoggerOptionsTests.cs ├── Internal │ └── DelegateLogEventFilterTests.cs ├── Reflection │ ├── EnumerableWriterFactoryTests.cs │ ├── DictionaryWriterFactoryTests.cs │ ├── ObjectWriterFactoryTests.cs │ └── TypeActivatorTests.cs ├── TestHelpers.cs ├── Vertical.SpectreLogger.Tests.csproj ├── Output │ ├── BackgroundConsoleWriterTests.cs │ └── ActualConsoleWriterTests.cs ├── Templates │ └── TemplatePatternBuilderTests.cs └── Threading │ └── MultiThreadedLoggingTests.cs ├── assets ├── cap1.png ├── cap2.png ├── cap3.png ├── icon.ai ├── icon.png └── vertical-software.snk ├── bug-repro ├── Directory.Build.props ├── MultiThreaded │ ├── MultiThreaded.csproj │ └── Program.cs ├── Destructuring │ ├── Destructuring.csproj │ ├── Model.cs │ └── Program.cs └── solution.sln ├── src ├── Reflection │ ├── CompiledWriter.cs │ ├── CompiledWriterFactory.cs │ └── ObjectWriterFactory.cs ├── Templates │ ├── TemplateCallback.cs │ ├── ITemplateRendererBuilder.cs │ ├── TemplateString.cs │ ├── TemplateDescriptor.cs │ └── TemplateRendererBuilder.cs ├── Core │ ├── LogEventFilterDelegate.cs │ ├── IRendererPipeline.cs │ ├── IScopeValues.cs │ ├── ILogEventFilter.cs │ ├── ITemplateRenderer.cs │ └── LogEventContext.cs ├── Destructuring │ ├── DestructuredKeyValue.cs │ ├── CompiledWriterCache.cs │ └── IDestructuringWriter.cs ├── Scopes │ ├── ScopeValues.cs │ ├── SingleScopeValue.cs │ ├── EmptyScopeValues.cs │ ├── MultiScopeValues.cs │ ├── LoggerScope.cs │ └── ScopeManager.cs ├── Rendering │ ├── NewLineRenderer.cs │ ├── StaticSpanRenderer.cs │ ├── MessageRenderer.cs │ ├── ActivityIdRenderer.cs │ ├── CategoryNameRenderer.cs │ ├── MarginControlRenderer.cs │ ├── ExceptionRenderer.Options.cs │ ├── LogLevelRenderer.cs │ ├── ProcessIdRenderer.cs │ ├── ThreadIdRenderer.cs │ ├── ScopeValuesRenderer.cs │ ├── DateTimeRenderer.cs │ ├── RendererPipeline.cs │ ├── ExceptionRenderer.Formatting.cs │ └── CategoryNameRenderer.Formatting.cs ├── Output │ ├── IConsoleWriter.cs │ ├── ForegroundConsoleWriter.cs │ ├── ConsoleWriter.cs │ ├── IWriteBuffer.cs │ ├── BackgroundConsoleWriter.cs │ └── WriteBuffer.cs ├── Internal │ ├── DelegateLogEventFilter.cs │ ├── SpectreLoggerOptionsExtensions.cs │ ├── WriteBufferPooledObjectPolicy.cs │ ├── TypeNameFormatter.cs │ └── CollectionExtensions.cs ├── Properties │ └── AssemblyInfo.cs ├── Formatting │ ├── NullValue.cs │ ├── ValueFormatter.cs │ ├── MultiTypeFormatProvider.cs │ ├── TypeFormatterAttribute.cs │ ├── ProviderFormatter.cs │ ├── MultiTypeFormatter.cs │ └── ValueWrapper.cs ├── Options │ ├── DestructuringOptions.cs │ ├── MicrosoftStyleLoggerOptions.cs │ ├── OptionsCollection.cs │ ├── SpectreLoggerOptions.cs │ └── LogLevelProfile.cs ├── SpectreLoggerProvider.cs ├── LoggingBuilderExtensions.cs ├── SpectreLogger.cs └── Vertical.SpectreLogger.csproj ├── examples ├── Exceptions │ └── Exceptions.csproj ├── Scopes │ └── Scopes.csproj ├── OutOfBoxStyles │ └── OutOfBoxStyles.csproj ├── DestructuredValues │ ├── DestructuredValues.csproj │ └── Program.cs ├── Directory.Build.props ├── Formatting │ ├── Formatting.csproj │ └── Program.cs ├── CustomRenderer │ ├── CustomRenderer.csproj │ ├── Program.cs │ ├── IncrementingIdRenderer.cs │ └── IncrementingId.cs └── MinimumLevelOverrides │ ├── MinimumLevelOverrides.csproj │ └── Program.cs ├── docs ├── log-level.md ├── newline.md ├── extending.md ├── thread-id.md ├── process-id.md ├── activity-id.md ├── message.md ├── scope-value.md ├── advanced-config.md ├── date-time.md ├── category-name.md ├── margin-control.md ├── scope-values.md ├── docs-home.md ├── styling.md ├── destructuring.md └── output-template.md ├── install-local.sh ├── .github └── workflows │ ├── release.yml │ ├── dev-build.yml │ ├── pre-release.yml │ └── dev-build-windows.yml ├── LICENSE └── README.md /test/.gitignore: -------------------------------------------------------------------------------- 1 | *.received.txt 2 | -------------------------------------------------------------------------------- /assets/cap1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verticalsoftware/vertical-spectreconsolelogger/HEAD/assets/cap1.png -------------------------------------------------------------------------------- /assets/cap2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verticalsoftware/vertical-spectreconsolelogger/HEAD/assets/cap2.png -------------------------------------------------------------------------------- /assets/cap3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verticalsoftware/vertical-spectreconsolelogger/HEAD/assets/cap3.png -------------------------------------------------------------------------------- /assets/icon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verticalsoftware/vertical-spectreconsolelogger/HEAD/assets/icon.ai -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verticalsoftware/vertical-spectreconsolelogger/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/vertical-software.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verticalsoftware/vertical-spectreconsolelogger/HEAD/assets/vertical-software.snk -------------------------------------------------------------------------------- /test/Rendering/Verified/MarginControlRendererTests.RenderWritesExpectedOutputForAdjust.DotNet.verified.txt: -------------------------------------------------------------------------------- 1 | Indent-0 2 | Indent+2 3 | Indent-0 -------------------------------------------------------------------------------- /test/Rendering/Verified/MarginControlRendererTests.RenderWritesExpectedOutputForSet.DotNet.verified.txt: -------------------------------------------------------------------------------- 1 | Indent-0 2 | Indent-5 3 | Indent-0 -------------------------------------------------------------------------------- /bug-repro/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/Destructuring/DestructuringWriterTests.WriteObjectRendersExpectedContent.verified.txt: -------------------------------------------------------------------------------- 1 | {FirstName: Testy, LastName: McTesterson, Address: {Street: 123 Main Street, City: Denver, State: CO}} -------------------------------------------------------------------------------- /test/Destructuring/DestructuringWriterTests.WriteDictionaryRendersExpectedContent.verified.txt: -------------------------------------------------------------------------------- 1 | {firstName: Testy, lastName: McTesterson, address: {street: 123 Main Street, city: Denver, state: CO}} -------------------------------------------------------------------------------- /src/Reflection/CompiledWriter.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Destructuring; 2 | 3 | namespace Vertical.SpectreLogger.Reflection 4 | { 5 | internal delegate void CompiledWriter(IDestructuringWriter writer, object? value); 6 | } -------------------------------------------------------------------------------- /test/Destructuring/DestructuringWriterTests.WriteDictionaryRespectsMaxDepth.verified.txt: -------------------------------------------------------------------------------- 1 | {Name: Testy, Children: [[{Name: Testy Jr., Children: [[{Name: Testy III, Children: Vertical.SpectreLogger.Tests.Infrastructure.Person[[]]}]]}]]} -------------------------------------------------------------------------------- /examples/Exceptions/Exceptions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | $(ExamplesRuntime) 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/Scopes/Scopes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | $(ExamplesRuntime) 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/OutOfBoxStyles/OutOfBoxStyles.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | $(ExamplesRuntime) 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/DestructuredValues/DestructuredValues.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | $(ExamplesRuntime) 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/Formatting/Formatting.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | $(ExamplesRuntime) 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/CustomRenderer/CustomRenderer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | $(ExamplesRuntime) 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Templates/TemplateCallback.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Templates 2 | { 3 | /// 4 | /// Defines a delegate that receives template segments during split operations. 5 | /// 6 | public delegate void TemplateCallback(in TemplateSegment segment); 7 | } -------------------------------------------------------------------------------- /bug-repro/MultiThreaded/MultiThreaded.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /bug-repro/Destructuring/Destructuring.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/MinimumLevelOverrides/MinimumLevelOverrides.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Core/LogEventFilterDelegate.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Core 2 | { 3 | /// 4 | /// Defines a delegate that receives log event data and returns a boolean 5 | /// indicating whether or not the event should be filtered from the output. 6 | /// 7 | public delegate bool LogEventFilterDelegate(in LogEventContext context); 8 | } -------------------------------------------------------------------------------- /test/Destructuring/DestructuringWriterTests.WriteObjectIndented.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Id: 5e093875-2c3f-4d28-9718-1339aa03f9ca, 3 | FirstName: Testy, 4 | LastName: McTesterson, 5 | Address: { 6 | Street: 6715 W Colfax Ave, 7 | City: Lakewood, 8 | State: CO, 9 | ZipCode: 80214 10 | }, 11 | Roles: [[ 12 | Manager, 13 | Cook 14 | ]] 15 | } -------------------------------------------------------------------------------- /test/Formatting/NullValueTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using Vertical.SpectreLogger.Formatting; 3 | using Xunit; 4 | 5 | namespace Vertical.SpectreLogger.Tests.Formatting 6 | { 7 | public class NullValueTests 8 | { 9 | [Fact] 10 | public void FormatReturnsExpectedConstant() 11 | { 12 | NullValue.Default.ToString(null, null).ShouldBe("(null)"); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Core/IRendererPipeline.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Core 2 | { 3 | /// 4 | /// Defines a renderer pipeline. 5 | /// 6 | public interface IRendererPipeline 7 | { 8 | /// 9 | /// Renders the log event. 10 | /// 11 | /// Log event info 12 | void Render(in LogEventContext logEventContext); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Destructuring/DestructuredKeyValue.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Formatting; 2 | 3 | namespace Vertical.SpectreLogger.Destructuring 4 | { 5 | /// 6 | /// Wraps the key of a destructured value. 7 | /// 8 | public class DestructuredKeyValue : ValueWrapper 9 | { 10 | /// 11 | public DestructuredKeyValue(string key) : base(key) 12 | { 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /test/Infrastructure/Factory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Vertical.SpectreLogger.Tests.Infrastructure 6 | { 7 | public static class Factory 8 | { 9 | public static T New(Func function) => function(); 10 | 11 | public static IEnumerable New(Func function, int count) => 12 | Enumerable.Range(0, count).Select(_ => function()); 13 | } 14 | } -------------------------------------------------------------------------------- /docs/log-level.md: -------------------------------------------------------------------------------- 1 | # Log Level Renderer 2 | 3 | ## Overview 4 | 5 | Prints the log level of the current event. 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {LogLevel[,alignment]} 11 | ``` 12 | 13 | ### Emitted Types 14 | 15 | The following type(s) can be formatted & styled: 16 | 17 | |Type|Description| 18 | |---|---| 19 | |`Microsoft.Extensions.Logging.LogLevel`|The event severity| 20 | 21 | ## See Also 22 | - [Next: Margin Control](./margin-control.md) 23 | - [Rendering Overview](./renderer-overview.md) -------------------------------------------------------------------------------- /src/Scopes/ScopeValues.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Core; 2 | 3 | namespace Vertical.SpectreLogger.Scopes 4 | { 5 | internal static class ScopeValues 6 | { 7 | internal static IScopeValues Create(LoggerScope? scope) 8 | { 9 | return scope switch 10 | { 11 | {PreviousScope: { }} => new MultiScopeValues(scope), 12 | { } => new SingleScopeValue(scope), 13 | _ => EmptyScopeValues.Default 14 | }; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Rendering/NewLineRenderer.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Core; 2 | using Vertical.SpectreLogger.Output; 3 | using Vertical.SpectreLogger.Templates; 4 | 5 | namespace Vertical.SpectreLogger.Rendering 6 | { 7 | /// 8 | /// Renders a new line. 9 | /// 10 | [Template("{NewLine}")] 11 | public class NewLineRenderer : ITemplateRenderer 12 | { 13 | /// 14 | public void Render(IWriteBuffer buffer, in LogEventContext context) => buffer.WriteLine(); 15 | } 16 | } -------------------------------------------------------------------------------- /test/Infrastructure/Person.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Tests.Infrastructure 2 | { 3 | public class Person 4 | { 5 | public Person(string name, Person[] children) 6 | { 7 | Name = name; 8 | Children = children; 9 | } 10 | 11 | public string Name { get; } 12 | 13 | public Person[] Children { get; } 14 | 15 | /// 16 | public override string ToString() => $"{Name}, Children={Children?.Length}"; 17 | } 18 | } -------------------------------------------------------------------------------- /test/DependencyInjectionTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Shouldly; 3 | using Xunit; 4 | 5 | namespace Vertical.SpectreLogger.Tests 6 | { 7 | public class DependencyInjectionTests 8 | { 9 | [Fact] 10 | public void ServicesAndDependenciesCreated() 11 | { 12 | var factory = LoggerFactory.Create(builder => builder.AddSpectreConsole()); 13 | 14 | var logger = factory.CreateLogger(); 15 | 16 | logger.ShouldNotBeNull(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /bug-repro/Destructuring/Model.cs: -------------------------------------------------------------------------------- 1 | namespace Destructuring; 2 | 3 | public record Model( 4 | Guid MigrationId, 5 | string DbVersion, 6 | DateTime DateApplied, 7 | Guid LogId, 8 | string SourcePath, 9 | string SourceFile, 10 | string Sha, 11 | string Agent, 12 | string Host, 13 | IDictionary Metrics, 14 | IDictionary OperationTags, 15 | IDictionary Metadata) 16 | { 17 | /// 18 | public override string ToString() => $"{MigrationId} (version={DbVersion})"; 19 | } -------------------------------------------------------------------------------- /src/Core/IScopeValues.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Vertical.SpectreLogger.Core 4 | { 5 | /// 6 | /// Provides access to scope values. 7 | /// 8 | public interface IScopeValues 9 | { 10 | /// 11 | /// Gets whether the collection has any values. 12 | /// 13 | bool HasValues { get; } 14 | 15 | /// 16 | /// Gets the items in the collection; 17 | /// 18 | IReadOnlyList Values { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /test/Options/SerilogStyleLoggerOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Shouldly; 3 | using Vertical.SpectreLogger.Options; 4 | using Xunit; 5 | 6 | namespace Vertical.SpectreLogger.Tests.Options 7 | { 8 | public class SerilogStyleLoggerOptionsTests 9 | { 10 | [Fact] 11 | public void UseMicrosoftStyleNoThrow() 12 | { 13 | LoggerFactory.Create(builder => builder 14 | .AddSpectreConsole(config => config.UseSerilogConsoleStyle())) 15 | .CreateLogger("Test").ShouldNotBeNull(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /test/Options/MicrosoftStyleLoggerOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Shouldly; 3 | using Vertical.SpectreLogger.Options; 4 | using Xunit; 5 | 6 | namespace Vertical.SpectreLogger.Tests.Options 7 | { 8 | public class MicrosoftStyleLoggerOptionsTests 9 | { 10 | [Fact] 11 | public void UseMicrosoftStyleNoThrow() 12 | { 13 | LoggerFactory.Create(builder => builder 14 | .AddSpectreConsole(config => config.UseMicrosoftConsoleStyle())) 15 | .CreateLogger("Test").ShouldNotBeNull(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesAggregateInnerExceptions.Core.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -------------------------------------------------------------------------------- /src/Core/ILogEventFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Core 2 | { 3 | /// 4 | /// Determines when events should be rendered. 5 | /// 6 | public interface ILogEventFilter 7 | { 8 | /// 9 | /// Determines whether the given event should be rendered. 10 | /// 11 | /// that describes the event. 12 | /// True if the event should be printed; false to ignore the event. 13 | bool Filter(in LogEventContext eventContext); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Output/IConsoleWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Output 2 | { 3 | /// 4 | /// Provides an interface to IAnsiConsole 5 | /// 6 | public interface IConsoleWriter 7 | { 8 | /// 9 | /// When implemented, resets the output device for the next operation. 10 | /// 11 | void ResetLine(); 12 | 13 | /// 14 | /// Writes content to the underlying console. 15 | /// 16 | /// Content to write. 17 | void Write(string content); 18 | } 19 | } -------------------------------------------------------------------------------- /test/Internal/DelegateLogEventFilterTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using Vertical.SpectreLogger.Core; 3 | using Vertical.SpectreLogger.Internal; 4 | using Xunit; 5 | 6 | namespace Vertical.SpectreLogger.Tests.Internal 7 | { 8 | public class DelegateLogEventFilterTests 9 | { 10 | [Fact] 11 | public void FilterInvokesDelegate() 12 | { 13 | var invoked = false; 14 | var testInstance = new DelegateLogEventFilter((in LogEventContext _) => invoked = true); 15 | 16 | testInstance.Filter(default); 17 | 18 | invoked.ShouldBeTrue(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Core/ITemplateRenderer.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Output; 2 | 3 | namespace Vertical.SpectreLogger.Core 4 | { 5 | /// 6 | /// An object that renders the content represented by a template. 7 | /// 8 | public interface ITemplateRenderer 9 | { 10 | /// 11 | /// Renders the template portion of the log event to the provided buffer. 12 | /// 13 | /// Write buffer 14 | /// Log event data. 15 | void Render(IWriteBuffer buffer, in LogEventContext context); 16 | } 17 | } -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesAggregateInnerExceptions.DotNet.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(System.Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(System.Int32 millisecondsTimeout, System.Threading.CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -------------------------------------------------------------------------------- /src/Rendering/StaticSpanRenderer.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Core; 2 | using Vertical.SpectreLogger.Output; 3 | 4 | namespace Vertical.SpectreLogger.Rendering 5 | { 6 | internal sealed class StaticSpanRenderer : ITemplateRenderer 7 | { 8 | private readonly string _content; 9 | 10 | internal StaticSpanRenderer(string content) 11 | { 12 | _content = content; 13 | } 14 | 15 | /// 16 | public void Render(IWriteBuffer buffer, in LogEventContext context) 17 | { 18 | buffer.Write(_content, 0, _content.Length); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /test/Destructuring/CompiledWriterCacheTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using Vertical.SpectreLogger.Destructuring; 3 | using Xunit; 4 | 5 | namespace Vertical.SpectreLogger.Tests.Destructuring 6 | { 7 | public class CompiledWriterCacheTests 8 | { 9 | [Fact] 10 | public void CacheReturnsExpectedRef() 11 | { 12 | var type = new {type = "TestClass"}.GetType(); 13 | 14 | var writer = CompiledWriterCache.GetInstance(type, (_, _) => { }); 15 | 16 | writer.ShouldNotBeNull(); 17 | CompiledWriterCache.GetInstance(type, (_, _) => {}).ShouldBe(writer); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /docs/newline.md: -------------------------------------------------------------------------------- 1 | # New Line Renderer 2 | 3 | ## Overview 4 | 5 | Inserts or enqueues a line break in event output. 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {NewLine} 11 | ``` 12 | 13 | ### Parameters 14 | 15 | None 16 | 17 | ### Remarks 18 | 19 | Use this renderer to control event output that spans multiple lines. For instance, you may want exceptions to be written on a new line if they are present in the log event. 20 | 21 | ### Example 22 | 23 | ``` 24 | info/{CategoryName}: {Message}{NewLine}{Exception} 25 | ``` 26 | 27 | ## See Also 28 | - [Next: Process Id](./process-id.md) 29 | - [Rendering Overview](./renderer-overview.md) 30 | 31 | -------------------------------------------------------------------------------- /src/Rendering/MessageRenderer.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Core; 2 | using Vertical.SpectreLogger.Output; 3 | using Vertical.SpectreLogger.Templates; 4 | 5 | namespace Vertical.SpectreLogger.Rendering 6 | { 7 | /// 8 | /// Renders the message portion of the log event. 9 | /// 10 | [Template("{Message}")] 11 | public class MessageRenderer : ITemplateRenderer 12 | { 13 | /// 14 | public void Render(IWriteBuffer buffer, in LogEventContext context) 15 | { 16 | buffer.WriteTemplateValue(context.Profile, destructureValues: false, context.State); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Scopes/SingleScopeValue.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Vertical.SpectreLogger.Core; 3 | 4 | namespace Vertical.SpectreLogger.Scopes 5 | { 6 | internal sealed class SingleScopeValue : IScopeValues 7 | { 8 | internal SingleScopeValue(LoggerScope scope) 9 | { 10 | Values = new[] {scope.Value}; 11 | } 12 | 13 | /// 14 | public bool HasValues => true; 15 | 16 | /// 17 | public IReadOnlyList Values { get; } 18 | 19 | /// 20 | public override string ToString() => Values[0]?.ToString() ?? string.Empty; 21 | } 22 | } -------------------------------------------------------------------------------- /test/Formatting/MultiTypeFormatProviderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NSubstitute; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Formatting; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Formatting 8 | { 9 | public class MultiTypeFormatProviderTests 10 | { 11 | [Fact] 12 | public void GetFormatReturnsInstance() 13 | { 14 | var testInstance = new MultiTypeFormatProvider(Substitute.For()); 15 | 16 | testInstance.GetFormat(typeof(ICustomFormatter)) 17 | .ShouldNotBeNull() 18 | .ShouldBeAssignableTo(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Internal/DelegateLogEventFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Vertical.SpectreLogger.Core; 3 | 4 | namespace Vertical.SpectreLogger.Internal 5 | { 6 | internal sealed class DelegateLogEventFilter : ILogEventFilter 7 | { 8 | private readonly LogEventFilterDelegate _filter; 9 | 10 | internal DelegateLogEventFilter(LogEventFilterDelegate filter) 11 | { 12 | _filter = filter ?? throw new ArgumentNullException(nameof(filter)); 13 | } 14 | 15 | /// 16 | public bool Filter(in LogEventContext eventContext) 17 | { 18 | return _filter(eventContext); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Output/ForegroundConsoleWriter.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console; 2 | 3 | namespace Vertical.SpectreLogger.Output 4 | { 5 | internal class ForegroundConsoleWriter : ConsoleWriter, IConsoleWriter 6 | { 7 | /// 8 | /// Creates a new instance 9 | /// 10 | /// Console instance 11 | public ForegroundConsoleWriter(IAnsiConsole console) : base(console) 12 | { 13 | } 14 | 15 | /// 16 | public void ResetLine() => ResetLineCore(); 17 | 18 | /// 19 | public void Write(string content) => WriteToConsole(content); 20 | } 21 | } -------------------------------------------------------------------------------- /test/Rendering/StaticSpanRendererTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Shouldly; 3 | using Vertical.SpectreLogger.Tests.Infrastructure; 4 | using Xunit; 5 | 6 | namespace Vertical.SpectreLogger.Tests.Rendering 7 | { 8 | public class StaticSpanRendererTests 9 | { 10 | [Fact] 11 | public void RenderWritesExpectedOutput() 12 | { 13 | RendererTestHarness.Capture( 14 | config => config.ConfigureProfile(LogLevel.Information, profile => profile.OutputTemplate = "{Message}+static"), 15 | logger => logger.LogInformation("test")) 16 | .ShouldBe("test+static"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /test/Formatting/IntegralFormattingTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Shouldly; 3 | using Vertical.SpectreLogger.Tests.Infrastructure; 4 | using Xunit; 5 | 6 | namespace Vertical.SpectreLogger.Tests.Formatting 7 | { 8 | public class IntegralFormattingTests 9 | { 10 | [Fact] 11 | public void FormatDecimal_ReturnsExpected() 12 | { 13 | RendererTestHarness.Capture( 14 | cfg => cfg.ConfigureProfile(LogLevel.Information, p => p.OutputTemplate="{Message}"), 15 | logger => logger.LogInformation("Double: {Value:F2}", 1d)) 16 | .ShouldBe("Double: [magenta3_2]1.00[/]"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /install-local.sh: -------------------------------------------------------------------------------- 1 | if [[ -z "$1" ]]; then 2 | echo "Local package version required (e.g. ./install-local.sh 1.0.0-dev" 3 | exit 1 4 | fi 5 | 6 | nugetPath="$HOME/.nuget/packages" 7 | installPath="$nugetPath/vertical-spectreconsolelogger" 8 | version=$1 9 | packagePath="$installPath/$version" 10 | 11 | if [ -d "$packagePath" ]; then 12 | echo "Remove existing installation from $packagePath" 13 | fi 14 | 15 | dotnet clean 16 | dotnet restore --force 17 | dotnet pack src/Vertical.SpectreLogger.csproj --no-restore -o ./pack -c Debug /p:Version=$version --include-symbols 18 | dotnet nuget push "./pack/vertical-spectreconsolelogger.$version.nupkg" -s $nugetPath 19 | 20 | rm -rf ./pack 21 | rm -rf ./lib 22 | -------------------------------------------------------------------------------- /docs/extending.md: -------------------------------------------------------------------------------- 1 | # Extending The Provider 2 | 3 | ## Overview 4 | 5 | Extending the logging provider cam be accomplished primarily by creating and registering your own [ICustomFormatter](https://docs.microsoft.com/en-us/dotnet/api/system.icustomformatter?view=net-5.0) and customizing output templates, but if you need to add functionality to the output template you do so by writing your own custom renderer. 6 | 7 | ### Creating a custom renderer 8 | 9 | Imagine we're writing an application that on events with a specific event ID, we need to display the application's memory usage. The easiest way to do this would be to create a custom renderer. In the example below, we'll create a renderer that even supports custom formatting. -------------------------------------------------------------------------------- /docs/thread-id.md: -------------------------------------------------------------------------------- 1 | # Thread ID Renderer 2 | 3 | ## Overview 4 | 5 | Prints the managed thread id (obtained by `Thread.CurrentThread.ManagedThreadId`). 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {ThreadId[,alignment]} 11 | ``` 12 | 13 | ### Parameters 14 | 15 | |Parameter|Description| 16 | |---|---| 17 | |`[,alignment]`|The preferred formatted field width.| 18 | 19 | 20 | ### Emitted Types 21 | 22 | The following type can be formatted & styled (the default formatter prints the `ManagedThreadId`). 23 | 24 | | Type | Description | 25 | | ------------------------- | ------------------------------- | 26 | | `ThreadIdRenderer.Value` | Represents the captured thread. | 27 | -------------------------------------------------------------------------------- /test/Infrastructure/ExceptionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace Vertical.SpectreLogger.Tests.Infrastructure 6 | { 7 | // Note - changing this class may alter Verify outputs 8 | public static class ExceptionHelper 9 | { 10 | public static Exception GetAggregateException() 11 | { 12 | try 13 | { 14 | Task.Run(() => Array.Empty().First()).Wait(); 15 | } 16 | catch (Exception exception) 17 | { 18 | return exception; 19 | } 20 | 21 | return new Exception(); 22 | } 23 | 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /test/Rendering/NewLineRendererTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Tests.Infrastructure; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Rendering 8 | { 9 | public class NewLineRendererTests 10 | { 11 | [Fact] 12 | public void RenderWritesExpectedOutput() 13 | { 14 | RendererTestHarness.Capture(config => config.ConfigureProfile(LogLevel.Information, 15 | profile => profile.OutputTemplate = "Line1{NewLine}Line2"), 16 | logger => logger.LogInformation("")) 17 | .ShouldBe($"Line1{Environment.NewLine}Line2"); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Scopes/EmptyScopeValues.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Vertical.SpectreLogger.Core; 4 | 5 | namespace Vertical.SpectreLogger.Scopes 6 | { 7 | internal sealed class EmptyScopeValues : IScopeValues 8 | { 9 | internal static readonly IScopeValues Default = new EmptyScopeValues(); 10 | 11 | private EmptyScopeValues() 12 | { 13 | } 14 | 15 | /// 16 | public bool HasValues => false; 17 | 18 | /// 19 | public IReadOnlyList Values { get; } = Array.Empty(); 20 | 21 | /// 22 | public override string ToString() => "(empty)"; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Templates/ITemplateRendererBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Vertical.SpectreLogger.Core; 3 | 4 | namespace Vertical.SpectreLogger.Templates 5 | { 6 | /// 7 | /// Defines an object that produces an ordered sequence of template renderers. 8 | /// 9 | public interface ITemplateRendererBuilder 10 | { 11 | /// 12 | /// Builds an ordered collection of template renderers. 13 | /// 14 | /// Template string. 15 | /// List of objects. 16 | IReadOnlyList GetRenderers(string templateString); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | // [assembly: InternalsVisibleTo("Vertical.SpectreLogger.Tests, PublicKey=" + 4 | // "00240000048000009400000006020000002400005253413100040000010001001d91b3216d0e70" + 5 | // "1d56dd8cb417ca1615726cc7c7e43ebce7436a357f9c0c82ed60b8d097677054a44040fa8ab12d" + 6 | // "9c638776d768df87e6d50156e57ade9345302ba722dd82ed04b591b3fcc4cd0c820c5285fc4754" + 7 | // "e789a5aafc64ba4f2d33999a925162fd59f06fca2611ee0f0114c00b01bf80bb67a49ddb82f6f1" + 8 | // "7bfd03c1")] 9 | 10 | [assembly: InternalsVisibleTo("Vertical.SpectreLogger.Tests")] -------------------------------------------------------------------------------- /src/Formatting/NullValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Vertical.SpectreLogger.Formatting 4 | { 5 | /// 6 | /// Provides a special representation of null that can be used for formatting. 7 | /// 8 | public sealed class NullValue : IFormattable 9 | { 10 | private NullValue() 11 | { 12 | } 13 | 14 | /// 15 | /// Defines a default instance. 16 | /// 17 | public static readonly NullValue Default = new NullValue(); 18 | 19 | /// 20 | public override string ToString() => string.Empty; 21 | 22 | /// 23 | public string ToString(string? format, IFormatProvider? formatProvider) => "(null)"; 24 | } 25 | } -------------------------------------------------------------------------------- /test/Formatting/ValueFormatterTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using Vertical.SpectreLogger.Formatting; 3 | using Xunit; 4 | 5 | namespace Vertical.SpectreLogger.Tests.Formatting 6 | { 7 | public class ValueFormatterTests 8 | { 9 | [Fact] 10 | public void FormatInvokesInternalDelegate() 11 | { 12 | const string format = "x4"; 13 | const int input = 100; 14 | 15 | var output = new ValueFormatter((fmt, value) => 16 | { 17 | fmt.ShouldBe(format); 18 | value.ShouldBe(input); 19 | return "true"; 20 | }) 21 | .Format(format, input, null); 22 | 23 | output.ShouldBe("true"); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /docs/process-id.md: -------------------------------------------------------------------------------- 1 | # Process ID Renderer 2 | 3 | ## Overview 4 | 5 | Prints the process id (obtained by `Environment.ProcessId`). 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {ProcessId[,alignment]} 11 | ``` 12 | 13 | ### Parameters 14 | 15 | |Parameter|Description| 16 | |---|---| 17 | |`[,alignment]`|The preferred formatted field width.| 18 | 19 | 20 | ### Emitted Types 21 | 22 | The following type can be formatted & styled (the default formatter prints the `Id`). 23 | 24 | | Type | Description | 25 | | ------------------------- | ------------------------------- | 26 | | `ProcessIdRenderer.Value` | Represents the captured thread. | 27 | 28 | ## See Also 29 | - [Next: Single Scope Value](./scope-values.md) 30 | - [Rendering Overview](./renderer-overview.md) -------------------------------------------------------------------------------- /src/Formatting/ValueFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Vertical.SpectreLogger.Formatting 4 | { 5 | /// 6 | /// Represents a strongly typed formatter for a value type. 7 | /// 8 | /// Value type being formatted. 9 | internal sealed class ValueFormatter : ICustomFormatter where T : notnull 10 | { 11 | private readonly Func _function; 12 | 13 | internal ValueFormatter(Func function) => _function = function 14 | ?? throw new ArgumentNullException(nameof(function)); 15 | 16 | /// 17 | public string Format(string? format, object? arg, IFormatProvider? formatProvider) 18 | { 19 | return _function(format, (T) arg!); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Formatting/MultiTypeFormatProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Vertical.SpectreLogger.Formatting 4 | { 5 | /// 6 | /// Represents a provider that returns a 7 | /// 8 | internal class MultiTypeFormatProvider : IFormatProvider 9 | { 10 | private readonly ICustomFormatter _typeFormatter; 11 | 12 | internal MultiTypeFormatProvider(ICustomFormatter typeFormatter) 13 | { 14 | _typeFormatter = typeFormatter ?? throw new ArgumentNullException(nameof(typeFormatter)); 15 | } 16 | 17 | /// 18 | public object? GetFormat(Type? formatType) 19 | { 20 | return typeof(ICustomFormatter) == formatType 21 | ? _typeFormatter 22 | : null; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Formatting/TypeFormatterAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Vertical.SpectreLogger.Formatting 4 | { 5 | /// 6 | /// Attributes used to describe the type a formatter supports. 7 | /// 8 | [AttributeUsage(AttributeTargets.Class)] 9 | public class TypeFormatterAttribute : Attribute 10 | { 11 | /// 12 | /// Creates a new instance. 13 | /// 14 | /// The type this instance provides formatting for. 15 | public TypeFormatterAttribute(Type type) 16 | { 17 | Type = type ?? throw new ArgumentNullException(nameof(type)); 18 | } 19 | 20 | /// 21 | /// Gets the type to associate with the formatter. 22 | /// 23 | public Type Type { get; } 24 | } 25 | } -------------------------------------------------------------------------------- /test/Rendering/ProcessIdRendererTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.Extensions.Logging; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Tests.Infrastructure; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Rendering 8 | { 9 | public class ProcessIdRendererTests 10 | { 11 | [Fact] 12 | public void RenderWritesExpectedOutput() 13 | { 14 | RendererTestHarness.Capture(config => config.ConfigureProfile(LogLevel.Information, 15 | profile => 16 | { 17 | profile.DefaultLogValueStyle = null; 18 | profile.OutputTemplate = "{ProcessId:x4}"; 19 | }), 20 | logger => logger.LogInformation("")) 21 | .ShouldBe(Process.GetCurrentProcess().Id.ToString("x4")); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /test/Rendering/ThreadIdRendererTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using Microsoft.Extensions.Logging; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Tests.Infrastructure; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Rendering 8 | { 9 | public class ThreadIdRendererTests 10 | { 11 | [Fact] 12 | public void RenderWritesExpectedOutput() 13 | { 14 | RendererTestHarness.Capture(config => config.ConfigureProfile(LogLevel.Information, 15 | profile => 16 | { 17 | profile.DefaultLogValueStyle = null; 18 | profile.OutputTemplate = "{ThreadId:x4}"; 19 | }), 20 | logger => logger.LogInformation("")) 21 | .ShouldBe(Thread.CurrentThread.ManagedThreadId.ToString("x4")); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Formatting/ProviderFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Vertical.SpectreLogger.Formatting 4 | { 5 | /// 6 | /// Represents a strongly typed formatter for values. 7 | /// 8 | /// Value type 9 | internal sealed class ProviderFormatter : ICustomFormatter where T : notnull 10 | { 11 | private readonly Func _function; 12 | 13 | internal ProviderFormatter(Func function) => _function = function 14 | ?? throw new ArgumentNullException(nameof(function)); 15 | 16 | /// 17 | public string Format(string? format, object? arg, IFormatProvider? formatProvider) 18 | { 19 | return _function(format, (T) arg!, formatProvider); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /docs/activity-id.md: -------------------------------------------------------------------------------- 1 | # Activity ID Renderer 2 | 3 | ## Overview 4 | 5 | If available, prints the `TraceId` portion of the current `System.Diagnostics.Activity`. 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {ActivityId[,alignment][:format]} 11 | ``` 12 | 13 | ### Parameters 14 | 15 | |Parameter|Description| 16 | |---|---| 17 | |`[,alignment]`|The preferred formatted field width.| 18 | |`[:format]`|Custom format string. Note there are no out-of-box formatters.| 19 | 20 | ### Emitted Types 21 | 22 | The following type can be formatted & styled: 23 | 24 | |Type|Description| 25 | |---|---| 26 | |`System.Diagnostics.ActivityTraceId`|The [trace id](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.activity.traceid?view=net-5.0) portion of the activity id.| 27 | 28 | ## See Also 29 | - [Next: Category Name](./category-name.md) 30 | - [Rendering Overview](./renderer-overview.md) -------------------------------------------------------------------------------- /src/Internal/SpectreLoggerOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.Extensions.Logging; 4 | using Vertical.SpectreLogger.Options; 5 | 6 | namespace Vertical.SpectreLogger.Internal 7 | { 8 | internal static class SpectreLoggerOptionsExtensions 9 | { 10 | internal static LogLevel GetLogLevelFilter(this SpectreLoggerOptions options, 11 | string loggerCategory) 12 | { 13 | var bestMatch = options 14 | .MinimumLevelOverrides 15 | .Where(kv => loggerCategory.StartsWith(kv.Key, StringComparison.OrdinalIgnoreCase)) 16 | .OrderByDescending(kv => kv.Key.Length) 17 | .FirstOrDefault(); 18 | 19 | return bestMatch is { Key: not null } && bestMatch.Value > options.MinimumLogLevel 20 | ? bestMatch.Value 21 | : options.MinimumLogLevel; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | dotnet-version: ["6.0.x"] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup .NET Code SDK ${{ matrix.dotnet-version }} 15 | uses: actions/setup-dotnet@v1.7.2 16 | with: 17 | dotnet-version: ${{ matrix.dotnet-version }} 18 | - name: Install dependencies 19 | run: dotnet restore 20 | - name: Build 21 | run: dotnet build -c Release --no-restore 22 | - name: Test 23 | run: dotnet test --no-restore 24 | - name: Build pre-release packages 25 | run: dotnet pack -c Release --no-restore -o ./pack 26 | - name: Publish packages to NuGet 27 | run: dotnet nuget push ./pack/*[^.symbols].nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_ORG_PUSH_KEY }} 28 | -------------------------------------------------------------------------------- /test/Infrastructure/SharedSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using VerifyTests; 3 | 4 | namespace Vertical.SpectreLogger.Tests.Infrastructure 5 | { 6 | public static class SharedSettings 7 | { 8 | public static readonly VerifySettings Verifier = Factory.New(() => 9 | { 10 | var settings = new VerifySettings(); 11 | 12 | settings.UniqueForRuntime(); 13 | settings.UseDirectory("Verified"); 14 | settings.ScrubLinesWithReplace(src => Regex.Replace( 15 | src, 16 | @"b__\d+_\d+\(", 17 | "b__ANY(")); 18 | settings.ScrubLinesWithReplace(src => Regex.Replace( 19 | src, 20 | "at System.Threading.ExecutionContext.Run(.+)", 21 | "at System.Threading.ExecutionContext.Run[ThreadMethod]")); 22 | 23 | return settings; 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesSourcePaths.Core.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First[[TSource]](IEnumerable`1 source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.<>c.b__0_0() 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.Tasks.Task.<>c.<.cctor>b__274_0(Object obj) 13 | +4 more... 14 | -------------------------------------------------------------------------------- /test/Formatting/ProviderFormatterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NSubstitute; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Formatting; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Formatting 8 | { 9 | public class ProviderFormatterTests 10 | { 11 | [Fact] 12 | public void FormatInvokesInternalDelegateWithProvider() 13 | { 14 | const string format = "x4"; 15 | const int input = 100; 16 | 17 | var providerSub = Substitute.For(); 18 | 19 | var output = new ProviderFormatter((fmt, value, provider) => 20 | { 21 | fmt.ShouldBe(format); 22 | providerSub.ShouldBe(provider); 23 | value.ShouldBe(input); 24 | return "true"; 25 | }) 26 | .Format(format, input, providerSub); 27 | 28 | output.ShouldBe("true"); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /examples/CustomRenderer/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Vertical.SpectreLogger; 3 | 4 | namespace CustomRenderer 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | var factory = LoggerFactory.Create(builder => 11 | { 12 | builder.AddSpectreConsole(config => 13 | { 14 | config.AddTemplateRenderers(); 15 | 16 | const string template = "[grey85][[{DateTime:T} [green3_1]Info[/]]] (logId={IncrementingId}) {Message}{NewLine}{Exception}[/]"; 17 | 18 | config.ConfigureProfiles(profile => profile.OutputTemplate = template); 19 | }); 20 | }); 21 | 22 | var logger = factory.CreateLogger(); 23 | 24 | for (var c = 0; c < 5; c++) 25 | { 26 | logger.LogInformation("Operation successful"); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/CustomRenderer/IncrementingIdRenderer.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Core; 2 | using Vertical.SpectreLogger.Output; 3 | using Vertical.SpectreLogger.Templates; 4 | 5 | namespace CustomRenderer 6 | { 7 | public class IncrementingIdRenderer : ITemplateRenderer 8 | { 9 | [Template] 10 | public static readonly string Template = TemplatePatternBuilder 11 | .ForKey("IncrementingId") 12 | .AddAlignment() 13 | .AddFormatting() 14 | .Build(); 15 | 16 | private readonly TemplateSegment _templateSegment; 17 | 18 | public IncrementingIdRenderer(TemplateSegment templateSegment) => _templateSegment = templateSegment; 19 | 20 | /// 21 | public void Render(IWriteBuffer buffer, in LogEventContext context) 22 | { 23 | buffer.WriteLogValue( 24 | context.Profile, 25 | _templateSegment, 26 | IncrementingId.Create()); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesSourcePaths.DotNet.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(System.Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(System.Int32 millisecondsTimeout, System.Threading.CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First(System.Collections.Generic.IEnumerable`1 source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.<>c.b__ANY() 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.ExecutionContext.Run[ThreadMethod] 13 | +3 more... 14 | -------------------------------------------------------------------------------- /src/Destructuring/CompiledWriterCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using Vertical.SpectreLogger.Reflection; 4 | 5 | namespace Vertical.SpectreLogger.Destructuring 6 | { 7 | /// 8 | /// Maintains a cache of delegates. 9 | /// 10 | internal static class CompiledWriterCache 11 | { 12 | private static readonly ConcurrentDictionary Cache = new(); 13 | 14 | /// 15 | /// Returns a for the specified type. 16 | /// 17 | /// Type to resolve. 18 | /// Writer to use as a default. 19 | /// 20 | internal static CompiledWriter GetInstance(Type type, CompiledWriter defaultWriter) 21 | { 22 | return Cache.GetOrAdd(type, t => CompiledWriterFactory.CreateWriter(t) ?? defaultWriter); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesParameterNames.Core.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean) 4 | at System.Threading.Tasks.Task.Wait(Int32, CancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First[[TSource]](IEnumerable`1) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.<>c.b__0_0() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.Tasks.Task.<>c.<.cctor>b__274_0(Object) 13 | +4 more... 14 | -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesParameterTypes.DotNet.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(millisecondsTimeout, cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First(source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.<>c.b__ANY() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.ExecutionContext.Run[ThreadMethod] 13 | +3 more... 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Vertical Software 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesParameterNames.DotNet.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(System.Boolean) 4 | at System.Threading.Tasks.Task.Wait(System.Int32, System.Threading.CancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First(System.Collections.Generic.IEnumerable`1) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.<>c.b__ANY() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.ExecutionContext.Run[ThreadMethod] 13 | +3 more... 14 | -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesParameterTypes.Core.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(millisecondsTimeout, cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First[[TSource]](source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.<>c.b__0_0() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.Tasks.Task.<>c.<.cctor>b__274_0(obj) 13 | +4 more... 14 | -------------------------------------------------------------------------------- /.github/workflows/dev-build.yml: -------------------------------------------------------------------------------- 1 | name: Dev build 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | branches: 9 | - dev 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | dotnet-version: ["9.0.x"] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup .NET Code SDK ${{ matrix.dotnet-version }} 21 | uses: actions/setup-dotnet@v3 22 | with: 23 | dotnet-version: ${{ matrix.dotnet-version }} 24 | - name: Install dependencies 25 | run: dotnet restore 26 | - name: Build package sources 27 | run: dotnet build -c Release --no-restore 28 | - name: Build samples 29 | run: dotnet build ./examples/solution.sln 30 | - name: Test 31 | run: dotnet test --no-restore --collect:"XPlat Code Coverage" 32 | - name: Publish code coverage 33 | uses: codecov/codecov-action@v1 34 | with: 35 | files: "**/coverage.cobertura.xml" 36 | flags: unittests 37 | name: vertical-spectreconsolelogger-codecov 38 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Pre release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | dotnet-version: ["9.0.x"] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup .NET Code SDK ${{ matrix.dotnet-version }} 15 | uses: actions/setup-dotnet@v3 16 | with: 17 | dotnet-version: ${{ matrix.dotnet-version }} 18 | - name: Install dependencies 19 | run: dotnet restore 20 | - name: Build 21 | run: dotnet build -c Release --no-restore 22 | - name: Test 23 | run: dotnet test --no-restore 24 | - name: Set build label 25 | run: echo "DEVLABEL=$(date +'%Y%m%d')" >> $GITHUB_ENV 26 | - name: Build pre-release packages 27 | run: dotnet pack -c Release --no-restore -p:VersionSuffix=dev.${{ env.DEVLABEL }}.${{ github.run_number }} -o ./pack 28 | - name: Publish packages to NuGet 29 | run: dotnet nuget push ./pack/*[^.symbols].nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_ORG_PUSH_KEY }} 30 | -------------------------------------------------------------------------------- /test/Reflection/EnumerableWriterFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using Shouldly; 3 | using Vertical.SpectreLogger.Destructuring; 4 | using Vertical.SpectreLogger.Reflection; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Reflection 8 | { 9 | public class EnumerableWriterFactoryTests 10 | { 11 | [Fact] 12 | public void TryCreateReturnsCompiledWriter() 13 | { 14 | var obj = new[] {"one", "two", "three"}; 15 | 16 | EnumerableWriterFactory.TryCreate(obj.GetType(), out var function).ShouldBeTrue(); 17 | 18 | var writer = Substitute.For(); 19 | writer.WriteElement(Arg.Any()).Returns(true); 20 | 21 | function!(writer, obj); 22 | 23 | writer.Received().WriteStartArray(); 24 | writer.Received().WriteElement((object)"one"); 25 | writer.Received().WriteElement((object)"two"); 26 | writer.Received().WriteElement((object)"three"); 27 | writer.Received().WriteEndArray(); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /.github/workflows/dev-build-windows.yml: -------------------------------------------------------------------------------- 1 | name: Dev build - Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | branches: 9 | - dev 10 | 11 | jobs: 12 | build: 13 | runs-on: windows-latest 14 | strategy: 15 | matrix: 16 | dotnet-version: ["9.0.x"] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup .NET Code SDK ${{ matrix.dotnet-version }} 21 | uses: actions/setup-dotnet@v3 22 | with: 23 | dotnet-version: ${{ matrix.dotnet-version }} 24 | - name: Install dependencies 25 | run: dotnet restore 26 | - name: Build package sources 27 | run: dotnet build -c Release --no-restore 28 | - name: Build samples 29 | run: dotnet build ./examples/solution.sln 30 | - name: Test 31 | run: dotnet test --no-restore --collect:"XPlat Code Coverage" 32 | - name: Publish code coverage 33 | uses: codecov/codecov-action@v1 34 | with: 35 | files: "**/coverage.cobertura.xml" 36 | flags: unittests 37 | name: vertical-spectreconsolelogger-codecov 38 | -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesSourceLocations.Core.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First[[TSource]](IEnumerable`1 source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.<>c.b__0_0() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.Tasks.Task.<>c.<.cctor>b__274_0(Object obj) 13 | +4 more... 14 | -------------------------------------------------------------------------------- /test/Rendering/DateTimeRendererTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | using Vertical.SpectreLogger.Options; 4 | using Vertical.SpectreLogger.Rendering; 5 | using Vertical.SpectreLogger.Tests.Infrastructure; 6 | using Xunit; 7 | 8 | namespace Vertical.SpectreLogger.Tests.Rendering 9 | { 10 | public class DateTimeRendererTests 11 | { 12 | [Fact] 13 | public void RenderWritesExpectedOutput() 14 | { 15 | var now = DateTimeOffset.Now; 16 | 17 | RendererTestHarness.RunScenario( 18 | opt => 19 | { 20 | opt.ConfigureProfiles(profile => 21 | { 22 | profile.OutputTemplate = "{DateTime:g}"; 23 | // Also tests the factory 24 | profile.ConfigureOptions(dt => dt.ValueFactory = () => now); 25 | }); 26 | }, 27 | logger => logger.LogInformation(""), 28 | $@"^\[\w+\]{now:g}\[/\]$"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderWritesExpected.Core.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First[[TSource]](IEnumerable`1 source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.<>c.b__0_0() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.Tasks.Task.<>c.<.cctor>b__274_0(Object obj) 13 | +4 more... 14 | -------------------------------------------------------------------------------- /docs/message.md: -------------------------------------------------------------------------------- 1 | # Message Renderer 2 | 3 | ## Overview 4 | 5 | Prints the message of the log event with applicable structured log value substitutions. 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {Message} 11 | ``` 12 | 13 | ### Parameters 14 | 15 | None 16 | 17 | ### Remarks 18 | 19 | The message of a log event is string that is given to the `logger.Log()` method. If the message contains structured values, these values are substituted in the output. Log values that are substituted in the message are formatted and styled using the configuration in the applicable log level profile. 20 | 21 | > 💡 Note 22 | > 23 | > Log value placeholders can use alignment, formatting, and destructuring specifiers. 24 | 25 | ### Example 26 | 27 | ```csharp 28 | 29 | // Given the output template: {LogLevel}: The message is \"{Message}\" 30 | 31 | logger.LogInformation("Hello from the {provider}!", "vertical spectre logger"); 32 | 33 | // Output 34 | // Information: The message is "Hello from the vertical spectre logger!" 35 | 36 | ``` 37 | 38 | ## See Also 39 | - [Next: New Line](./newline.md) 40 | - [Rendering Overview](./renderer-overview.md) -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderHidesSourceLocations.DotNet.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(System.Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(System.Int32 millisecondsTimeout, System.Threading.CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First(System.Collections.Generic.IEnumerable`1 source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.<>c.b__ANY() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.ExecutionContext.Run[ThreadMethod] 13 | +3 more... 14 | -------------------------------------------------------------------------------- /src/Internal/WriteBufferPooledObjectPolicy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.ObjectPool; 3 | using Vertical.SpectreLogger.Output; 4 | 5 | namespace Vertical.SpectreLogger.Internal 6 | { 7 | internal class WriteBufferPooledObjectPolicy : PooledObjectPolicy 8 | { 9 | private readonly IConsoleWriter _consoleWriter; 10 | private readonly Func _writeBufferFactory; 11 | 12 | internal WriteBufferPooledObjectPolicy(IConsoleWriter consoleWriter, Func writeBufferFactory) 13 | { 14 | _consoleWriter = consoleWriter ?? throw new ArgumentNullException(nameof(consoleWriter)); 15 | _writeBufferFactory = writeBufferFactory ?? throw new ArgumentNullException(nameof(writeBufferFactory)); 16 | } 17 | 18 | /// 19 | public override IWriteBuffer Create() => _writeBufferFactory(); 20 | 21 | /// 22 | public override bool Return(IWriteBuffer obj) 23 | { 24 | obj.Flush(); 25 | obj.Margin = 0; 26 | 27 | return true; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Scopes/MultiScopeValues.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Vertical.SpectreLogger.Core; 4 | 5 | namespace Vertical.SpectreLogger.Scopes 6 | { 7 | internal sealed class MultiScopeValues : IScopeValues 8 | { 9 | private readonly Lazy> _lazyCollection; 10 | 11 | internal MultiScopeValues(LoggerScope scope) 12 | { 13 | _lazyCollection = new(() => 14 | { 15 | var list = new Stack(5); 16 | var current = (LoggerScope?) scope; 17 | 18 | for (; current != null; current = current.PreviousScope) 19 | { 20 | list.Push(current.Value); 21 | } 22 | 23 | return list.ToArray(); 24 | }); 25 | } 26 | 27 | /// 28 | public bool HasValues => true; 29 | 30 | /// 31 | public IReadOnlyList Values => _lazyCollection.Value; 32 | 33 | /// 34 | public override string ToString() => string.Join(" => ", Values); 35 | } 36 | } -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderWritesExpected.DotNet.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(System.Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(System.Int32 millisecondsTimeout, System.Threading.CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First(System.Collections.Generic.IEnumerable`1 source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.<>c.b__ANY() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.ExecutionContext.Run[ThreadMethod] 13 | +3 more... 14 | -------------------------------------------------------------------------------- /src/Rendering/ActivityIdRenderer.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Vertical.SpectreLogger.Core; 3 | using Vertical.SpectreLogger.Output; 4 | using Vertical.SpectreLogger.Templates; 5 | 6 | namespace Vertical.SpectreLogger.Rendering 7 | { 8 | /// 9 | /// Prints the activity id if available. 10 | /// 11 | public class ActivityIdRenderer : ITemplateRenderer 12 | { 13 | /// 14 | /// Defines the template for this renderer. 15 | /// 16 | [Template] 17 | public static readonly string Template = TemplatePatternBuilder 18 | .ForKey("[Aa]ctivity[Ii]d") 19 | .AddAlignment() 20 | .AddFormatting() 21 | .Build(); 22 | 23 | /// 24 | public void Render(IWriteBuffer buffer, in LogEventContext context) 25 | { 26 | var activity = Activity.Current; 27 | 28 | if (activity == null) 29 | return; 30 | 31 | buffer.WriteLogValue( 32 | context.Profile, 33 | null, 34 | activity.TraceId); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Scopes/LoggerScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Vertical.SpectreLogger.Scopes 4 | { 5 | internal sealed class LoggerScope : IDisposable 6 | { 7 | private readonly ScopeManager _scopeManager; 8 | private bool _disposed; 9 | 10 | internal LoggerScope(ScopeManager scopeManager, 11 | LoggerScope? previousScope, 12 | object value) 13 | { 14 | PreviousScope = previousScope; 15 | Value = value; 16 | _scopeManager = scopeManager; 17 | } 18 | 19 | /// 20 | public void Dispose() 21 | { 22 | if (_disposed) 23 | return; 24 | 25 | _disposed = true; 26 | _scopeManager.ScopeDisposed(this); 27 | } 28 | 29 | /// 30 | /// Gets the previous scope. 31 | /// 32 | internal LoggerScope? PreviousScope { get; } 33 | 34 | /// 35 | /// Gets the scope value. 36 | /// 37 | internal object Value { get; } 38 | 39 | /// 40 | public override string ToString() => Value?.ToString() ?? "(null)"; 41 | } 42 | } -------------------------------------------------------------------------------- /test/Rendering/ActivityIdRendererTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.Extensions.Logging; 3 | using Vertical.SpectreLogger.Tests.Infrastructure; 4 | using Xunit; 5 | 6 | namespace Vertical.SpectreLogger.Tests.Rendering 7 | { 8 | public class ActivityIdRendererTests 9 | { 10 | [Fact] 11 | public void RendererWritesExpectedValue() 12 | { 13 | var activity = new Activity("test"); 14 | activity.Start(); 15 | 16 | try 17 | { 18 | RendererTestHarness.RunScenario( 19 | config => config.ConfigureProfiles(p => p.OutputTemplate = "{ActivityId}"), 20 | logger => logger.LogInformation(""), 21 | @$"^\[\w+\]{activity.TraceId}\[/\]$"); 22 | } 23 | finally 24 | { 25 | activity.Stop(); 26 | } 27 | } 28 | 29 | [Fact] 30 | public void RendererWritesNothingWithNoActivityId() 31 | { 32 | RendererTestHarness.RunScenario( 33 | config => config.ConfigureProfiles(p => p.OutputTemplate = "{ActivityId}"), 34 | logger => logger.LogInformation(""), 35 | ""); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Options/DestructuringOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Options 2 | { 3 | /// 4 | /// Options used to render destructured items. 5 | /// 6 | public class DestructuringOptions 7 | { 8 | /// 9 | /// Gets or sets the max depth at which to descend in an object's 10 | /// property graph. 11 | /// 12 | /// Defaults to a depth of 5. 13 | public int MaxDepth { get; set; } = 5; 14 | 15 | /// 16 | /// Gets or sets the maximum number of items to show from a collection. 17 | /// 18 | public int MaxCollectionItems { get; set; } = 10; 19 | 20 | /// 21 | /// Gets or sets the maximum number of properties to show of an object. 22 | /// 23 | public int MaxProperties { get; set; } = 10; 24 | 25 | /// 26 | /// Gets or sets whether to pretty-print (e.g. indenting) 27 | /// 28 | public bool WriteIndented { get; set; } 29 | 30 | /// 31 | /// Gets or sets the number of spaces that comprise an indent. 32 | /// 33 | public int IndentSpaces { get; set; } = 4; 34 | } 35 | } -------------------------------------------------------------------------------- /docs/scope-value.md: -------------------------------------------------------------------------------- 1 | # Scope Value Renderer 2 | 3 | ## Overview 4 | 5 | Prints a scope value found matching a specific key in a key/value pair collection. 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {Scope=} 11 | ``` 12 | 13 | ### Parameters 14 | 15 | |Parameter|Description| 16 | |---|---| 17 | |``|The key of the scope value to print.| 18 | 19 | ### Remarks 20 | 21 | This renderer prints a scope value by inspecting the scopes available in the log event, determining if any are assignable to `IReadOnlyList>`, and printing the value of the first entry whose key matches the parameter. If the scope cannot be found, nothing is printed. 22 | 23 | > 💡 Note 24 | > 25 | > Scope value placeholders can use alignment, formatting, and destructuring specifiers. 26 | 27 | ### Example 28 | 29 | ```csharp 30 | // Given output template: {LogLevel}: userId={Scope=UserId} => {Message} 31 | 32 | var userProperty = new KeyValuePair("UserId","admin"); 33 | var scope = logger.BeginScope(new[]{userProperty}); 34 | 35 | logger.LogInformation("User connected successfully"); 36 | 37 | // Output 38 | // Information: userId=admin => User connected successfully 39 | ``` 40 | 41 | ## See Also 42 | - [Next: Scope Values](./scope-values.md) 43 | - [Rendering Overview](./renderer-overview.md) -------------------------------------------------------------------------------- /docs/advanced-config.md: -------------------------------------------------------------------------------- 1 | # Advanced Configuration 2 | 3 | ## Overview 4 | 5 | The following sections details some uncommon but useful configuration settings. 6 | 7 | ### Controlling the output thread 8 | 9 | By default, the logging provider will render marked up content to the `AnsiConsole` on the calling thread. This means the thread will be blocked for the logging event cycle. In most situations this is acceptable. If you need the rendering cycle to not block the calling thread for performance reasons, enact the background thread mode. 10 | 11 | ```csharp 12 | // Render events in the background 13 | config.WriteInBackground(); 14 | 15 | // Render events in the foreground (default) 16 | config.WriteInForeground(); 17 | ``` 18 | 19 | ### Controlling the number of buffers 20 | 21 | The logging provider pools write buffers to try to reuse string builders (defaults to 5 buffers). You can decrease or increase this value depending on the needs of your application and how multi-threaded it is. If your application primarily operates on a single thread, a single buffer is all that is needed, while thread-intensive applications may make use of additional buffers efficiently. 22 | 23 | ```csharp 24 | // Set buffers to 1 for a single-threaded application 25 | config.MaxPooledBuffers = 3; 26 | ``` 27 | 28 | ## See Also 29 | - [Basic Configuration](./basic-configuration.md) -------------------------------------------------------------------------------- /test/Formatting/MultiTypeFormatterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Formatting; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Formatting 8 | { 9 | public class MultiTypeFormatterTests 10 | { 11 | [Fact] 12 | public void FormatReturnsValueForTypeUsingRegisteredFormatter() 13 | { 14 | var testInstance = new MultiTypeFormatter(new Dictionary 15 | { 16 | [typeof(int)] = new ValueFormatter((_,_) => nameof(Int32)) 17 | }); 18 | 19 | testInstance.Format(null, 100, null).ShouldBe(nameof(Int32)); 20 | } 21 | 22 | [Fact] 23 | public void FormatReturnsValueUsingIFormattableFunction() 24 | { 25 | var testInstance = new MultiTypeFormatter(new Dictionary()); 26 | 27 | testInstance.Format("x4", 100, null).ShouldBe("0064"); 28 | } 29 | 30 | [Fact] 31 | public void FormatReturnsDefaultToString() 32 | { 33 | var testInstance = new MultiTypeFormatter(new Dictionary()); 34 | 35 | testInstance.Format(null, new{value="test"}, null).ShouldBe("{ value = test }"); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Formatting/MultiTypeFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | 5 | namespace Vertical.SpectreLogger.Formatting 6 | { 7 | /// 8 | /// Represents a that uses the profile type 9 | /// formatters. 10 | /// 11 | internal class MultiTypeFormatter : ICustomFormatter 12 | { 13 | private readonly Dictionary _typeFormatters; 14 | 15 | internal MultiTypeFormatter(Dictionary typeFormatters) 16 | { 17 | _typeFormatters = typeFormatters ?? throw new ArgumentNullException(nameof(typeFormatters)); 18 | } 19 | 20 | /// 21 | public string Format(string? format, object? arg, IFormatProvider? formatProvider) 22 | { 23 | if (arg == null) 24 | { 25 | return string.Empty; 26 | } 27 | 28 | if (_typeFormatters.TryGetValue(arg.GetType(), out var formatter)) 29 | return formatter.Format(format, arg, formatProvider); 30 | 31 | if (arg is IFormattable formattableValue) 32 | return formattableValue.ToString(format, CultureInfo.CurrentCulture); 33 | 34 | return arg.ToString() ?? string.Empty; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /bug-repro/MultiThreaded/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Vertical.SpectreLogger; 3 | 4 | const int threadCount = 25; 5 | const int logsPerThread = 150; 6 | const int delayMs = 250; 7 | 8 | var loggerFactory = LoggerFactory.Create(builder => builder.AddSpectreConsole()); 9 | var logger = loggerFactory.CreateLogger("Test"); 10 | var threads = Enumerable.Range(0, threadCount).Select(i => 11 | Task.Run(async () => 12 | { 13 | await Task.Delay(delayMs); 14 | 15 | for (var c = 0; c < logsPerThread; c++) 16 | { 17 | if (c % 5 == 0) 18 | { 19 | try 20 | { 21 | Throw(); 22 | } 23 | catch (InvalidOperationException exception) 24 | { 25 | logger.LogError(exception, "An InvalidOperationException was thrown"); 26 | } 27 | continue; 28 | } 29 | 30 | logger.LogInformation("I'm thread {thread}, and I am on iteration {iteration}", 31 | i, 32 | c); 33 | } 34 | return Task.CompletedTask; 35 | })); 36 | 37 | await Task.WhenAll(threads); 38 | 39 | Console.Write("Press to exit"); 40 | Console.ReadLine(); 41 | 42 | static void Throw() 43 | { 44 | throw new InvalidOperationException(); 45 | } -------------------------------------------------------------------------------- /test/TestHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NSubstitute; 5 | using Shouldly; 6 | using Vertical.SpectreLogger.Core; 7 | using Vertical.SpectreLogger.Output; 8 | 9 | namespace Vertical.SpectreLogger.Tests 10 | { 11 | public static class TestHelpers 12 | { 13 | public static void ShouldHaveElementsEqualTo( 14 | this IEnumerable source, 15 | IEnumerable expected, 16 | Action comparer) 17 | { 18 | var queue = new Queue(expected); 19 | 20 | foreach (var item in source) 21 | { 22 | queue.Any().ShouldBeTrue(); 23 | 24 | var nextExpected = queue.Dequeue(); 25 | 26 | comparer(item, nextExpected!); 27 | } 28 | 29 | queue.ShouldBeEmpty($"{queue.Count} position control(s) not matched."); 30 | } 31 | 32 | public static void VerifyOutput( 33 | this ITemplateRenderer renderer, 34 | in LogEventContext eventContext, 35 | string expectedContent) 36 | { 37 | var buffer = new WriteBuffer(Substitute.For()); 38 | 39 | renderer.Render(buffer, eventContext); 40 | 41 | buffer.ToString().ShouldBe(expectedContent); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Rendering/CategoryNameRenderer.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Core; 2 | using Vertical.SpectreLogger.Output; 3 | using Vertical.SpectreLogger.Templates; 4 | 5 | namespace Vertical.SpectreLogger.Rendering 6 | { 7 | /// 8 | /// Renders the logger category. 9 | /// 10 | public partial class CategoryNameRenderer : ITemplateRenderer 11 | { 12 | private readonly TemplateSegment _template; 13 | 14 | /// 15 | /// Defines the template for this renderer. 16 | /// 17 | [Template] 18 | public static readonly string Template = TemplatePatternBuilder 19 | .ForKey("[Cc]ategory[Nn]ame") 20 | .AddAlignment() 21 | .AddFormatting() 22 | .Build(); 23 | 24 | /// 25 | /// Creates a new instance of this type. 26 | /// 27 | /// Matching segment from the output template. 28 | public CategoryNameRenderer(TemplateSegment template) => _template = template; 29 | 30 | /// 31 | public void Render(IWriteBuffer buffer, in LogEventContext context) 32 | { 33 | buffer.WriteLogValue( 34 | context.Profile, 35 | _template, 36 | new Value(context.CategoryName)); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /test/Reflection/DictionaryWriterFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NSubstitute; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Destructuring; 5 | using Vertical.SpectreLogger.Reflection; 6 | using Xunit; 7 | 8 | namespace Vertical.SpectreLogger.Tests.Reflection 9 | { 10 | public class DictionaryWriterFactoryTests 11 | { 12 | [Fact] 13 | public void TryCreateReturnsCompiledInstance() 14 | { 15 | var dictionary = new Dictionary() 16 | { 17 | [1] = "one", 18 | [2] = "two", 19 | [3] = "three" 20 | }; 21 | 22 | DictionaryWriterFactory.TryCreate(dictionary.GetType(), out var function) 23 | .ShouldBeTrue(); 24 | 25 | var writer = Substitute.For(); 26 | writer.WriteProperty(Arg.Any(), Arg.Any()).Returns(true); 27 | 28 | function.ShouldNotBeNull(); 29 | function(writer, dictionary); 30 | 31 | writer.Received().WriteStartObject(); 32 | writer.Received().WriteProperty("1", (object)"one"); 33 | writer.Received().WriteProperty("2", (object)"two"); 34 | writer.Received().WriteProperty("3", (object)"three"); 35 | writer.Received().WriteEndObject(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderLimitsStackFrames.DotNet.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(System.Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(System.Int32 millisecondsTimeout, System.Threading.CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First(System.Collections.Generic.IEnumerable`1 source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.<>c.b__ANY() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.ExecutionContext.Run[ThreadMethod] 13 | at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() 14 | at System.Threading.ExecutionContext.Run[ThreadMethod] 15 | at System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task& currentTaskSlot, System.Threading.Thread threadPoolThread) 16 | -------------------------------------------------------------------------------- /test/Vertical.SpectreLogger.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | false 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/date-time.md: -------------------------------------------------------------------------------- 1 | # DateTime Renderer 2 | 3 | ## Overview 4 | 5 | Prints the value of `DateTimeOffset.Now`, or the value returned by a factory registered in this type's options. 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {DateTime[,alignment][:format-string]} 11 | ``` 12 | 13 | ### Example 14 | 15 | ``` 16 | {DateTime:yyyy/MM/dd hh:mm:ss} 17 | ``` 18 | 19 | ### Parameters 20 | 21 | |Parameter|Description| 22 | |---|---| 23 | |`[,alignment]`|The preferred formatted field width.| 24 | |`[:format-string]`|A format string that can control how the date is printed (see [DateTime Formatting](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings)).| 25 | 26 | ### Options configuration 27 | 28 | This renderer defines an `Options` type. You can customize the date/time value returned by assigning a factory. For example: 29 | 30 | ```csharp 31 | // Use DateTimeOffset.UtcNow instead of local time: 32 | 33 | config.ConfigureProfiles(profile => 34 | { 35 | profile.ConfigureOptions(renderer => 36 | renderer.ValueFactory = () => DateTimeOffset.UtcNow); 37 | }); 38 | ``` 39 | 40 | ### Emitted Types 41 | 42 | The following type(s) can be formatted & styled: 43 | 44 | |Type|Description| 45 | |---|---| 46 | |`System.DateTimeOffset`|Represents the category name| 47 | 48 | ## See Also 49 | - [Next: Exceptions](./exceptions.md) 50 | - [Rendering Overview](./renderer-overview.md) -------------------------------------------------------------------------------- /test/Reflection/ObjectWriterFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using Shouldly; 3 | using Vertical.SpectreLogger.Destructuring; 4 | using Vertical.SpectreLogger.Reflection; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Reflection 8 | { 9 | public class ObjectWriterFactoryTests 10 | { 11 | [Fact] 12 | public void TryCreateReturnsWriter() 13 | { 14 | var obj = new 15 | { 16 | FirstName = "Testy", 17 | LastName = "McTesterson", 18 | Address = new 19 | { 20 | Street = "123 Main Street", 21 | City = "Denver", 22 | State = "CO" 23 | } 24 | }; 25 | 26 | ObjectWriterFactory.TryCreate(obj.GetType(), out var writer).ShouldBeTrue(); 27 | 28 | var destructingWriter = Substitute.For(); 29 | 30 | writer!(destructingWriter, obj); 31 | 32 | destructingWriter.Received(1).WriteStartObject(); 33 | destructingWriter.Received().WriteProperty("FirstName", (object)"Testy"); 34 | destructingWriter.Received().WriteProperty("LastName", (object)"McTesterson"); 35 | destructingWriter.Received().WriteProperty("Address", Arg.Any()); 36 | destructingWriter.Received(1).WriteEndObject(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Internal/TypeNameFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Text; 4 | using Microsoft.Extensions.ObjectPool; 5 | 6 | namespace Vertical.SpectreLogger.Internal 7 | { 8 | internal static class TypeNameFormatter 9 | { 10 | private static readonly ObjectPool StringBuilderPool = new DefaultObjectPool( 11 | new StringBuilderPooledObjectPolicy(), 16); 12 | 13 | private static readonly ConcurrentDictionary Cached = new(); 14 | 15 | internal static string Format(Type type) 16 | { 17 | return Cached.GetOrAdd(type, Build); 18 | } 19 | 20 | private static string Build(Type type) 21 | { 22 | var sb = StringBuilderPool.Get(); 23 | 24 | try 25 | { 26 | Build(type, sb); 27 | return sb.ToString(); 28 | } 29 | finally 30 | { 31 | StringBuilderPool.Return(sb); 32 | } 33 | } 34 | 35 | private static void Build(Type type, StringBuilder sb) 36 | { 37 | if (!string.IsNullOrWhiteSpace(type.Namespace)) 38 | { 39 | sb.Append(type.Namespace); 40 | sb.Append('.'); 41 | } 42 | 43 | sb.Append(type.Name); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Internal/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Vertical.SpectreLogger.Internal 5 | { 6 | internal static class CollectionExtensions 7 | { 8 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 9 | internal static bool TryGetValue( 10 | this IReadOnlyList> formattedLogValues, 11 | string key, 12 | out object? value) 13 | { 14 | var count = formattedLogValues.Count; 15 | 16 | for (var c = 0; c < count; c++) 17 | { 18 | // ReSharper disable once UseDeconstruction 19 | var entry = formattedLogValues[c]; 20 | 21 | if (key != entry.Key) 22 | continue; 23 | 24 | value = entry.Value; 25 | return true; 26 | } 27 | 28 | value = default; 29 | return false; 30 | } 31 | 32 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 33 | internal static TValue? GetValueOrDefault( 34 | this Dictionary dictionary, 35 | TKey key, 36 | TValue? defaultValue) 37 | where TKey : notnull 38 | { 39 | return dictionary.TryGetValue(key, out var value) 40 | ? value 41 | : defaultValue; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /docs/category-name.md: -------------------------------------------------------------------------------- 1 | # Category Name Renderer 2 | 3 | ## Overview 4 | 5 | Prints the category name of the logger. 6 | 7 | ### Placeholder syntax 8 | 9 | ``` 10 | {CategoryName[,alignment][:format-string]} 11 | ``` 12 | 13 | ### Example 14 | 15 | The following example would print the category name using compact-class notation: 16 | 17 | ``` 18 | {CategoryName:C} 19 | ``` 20 | 21 | ### Parameters 22 | 23 | |Parameter|Description| 24 | |---|---| 25 | |`[,alignment]`|The preferred formatted field width.| 26 | |`[:format-string]`|A format string that can control how the category name is printed.| 27 | 28 | ### Formatting 29 | 30 | The format string for this renderer currently supports two notations: 31 | 32 | |Format|Description| 33 | |---|---| 34 | |`C`|Prints the class name part of the category name (e.g.`Logger` -> `Logger`, `Microsoft.Extensions.Logging.ILogger` -> `ILogger`).| 35 | |`S`|Prints a subset of the category name by splitting it into segments between the dot (.) characters and printing only the last number of segments indicated by a numeric specifier (e.g. for category `Microsoft.Extensions.Logging.Ilogger`, `S1` -> `Ilogger`, `S2` -> `Logging.Ilogger`, etc.) 36 | 37 | ### Emitted Types 38 | 39 | The following type(s) can be formatted & styled: 40 | 41 | |Type|Description| 42 | |---|---| 43 | |`System.Diagnostics.ActivityTraceId`|Represents the category name| 44 | 45 | ## See Also 46 | - [Next: Date/Time](./date-time.md) 47 | - [Rendering Overview](./renderer-overview.md) -------------------------------------------------------------------------------- /test/Rendering/MarginControlRendererTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Logging; 3 | using VerifyXunit; 4 | using Vertical.SpectreLogger.Tests.Infrastructure; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Rendering 8 | { 9 | public class MarginControlRendererTests 10 | { 11 | [Fact] 12 | [Trait("Category", "flaky_on_CI")] 13 | public Task RenderWritesExpectedOutputForSet() 14 | { 15 | var output = RendererTestHarness.Capture( 16 | config => config.ConfigureProfile(LogLevel.Information, profile => 17 | { 18 | profile.OutputTemplate = "Indent-0{Margin=5}{NewLine}Indent-5{Margin=0}{NewLine}Indent-0"; 19 | }), 20 | logger => logger.LogInformation("")); 21 | 22 | return Verifier.Verify(output, SharedSettings.Verifier); 23 | } 24 | 25 | [Fact] 26 | [Trait("Category", "flaky_on_CI")] 27 | public Task RenderWritesExpectedOutputForAdjust() 28 | { 29 | var output = RendererTestHarness.Capture( 30 | config => config.ConfigureProfile(LogLevel.Information, profile => profile 31 | .OutputTemplate = "Indent-0{Margin+2}{NewLine}Indent+2{Margin-2}{NewLine}Indent-0"), 32 | logger => logger.LogInformation("")); 33 | 34 | return Verifier.Verify(output, SharedSettings.Verifier); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Rendering/LogLevelRendererTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.Extensions.Logging; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Tests.Infrastructure; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Rendering 8 | { 9 | public class LogLevelRendererTests 10 | { 11 | [Theory, MemberData(nameof(Theories))] 12 | public void RenderWritesExpectedValue(LogLevel logLevel, string expected) 13 | { 14 | var output = RendererTestHarness.Capture( 15 | config => 16 | { 17 | config.ConfigureProfiles(profile => 18 | { 19 | profile.DefaultLogValueStyle = null; 20 | profile.OutputTemplate = "{LogLevel}"; 21 | }); 22 | config.SetMinimumLevel(LogLevel.Trace); 23 | }, 24 | logger => logger.Log(logLevel, "")); 25 | 26 | output.ShouldBe(expected); 27 | } 28 | 29 | public static IEnumerable Theories = new[] 30 | { 31 | new object[] {LogLevel.Trace, "Trce"}, 32 | new object[] {LogLevel.Debug, "Dbug"}, 33 | new object[] {LogLevel.Information, "Info"}, 34 | new object[] {LogLevel.Warning, "Warn"}, 35 | new object[] {LogLevel.Error, "Fail"}, 36 | new object[] {LogLevel.Critical, "Crit"}, 37 | }; 38 | } 39 | } -------------------------------------------------------------------------------- /bug-repro/Destructuring/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using Destructuring; 3 | using Microsoft.Extensions.Logging; 4 | using Vertical.SpectreLogger; 5 | using Vertical.SpectreLogger.Options; 6 | 7 | var loggerFactory = LoggerFactory.Create(builder => builder.AddSpectreConsole( 8 | sc => sc.ConfigureProfile(LogLevel.Information, profile => profile 9 | .ConfigureOptions(opt => 10 | { 11 | opt.IndentSpaces = 2; 12 | opt.WriteIndented = true; 13 | opt.MaxProperties = 10; 14 | })))); 15 | 16 | var logger = loggerFactory.CreateLogger("Program"); 17 | 18 | var model = new Model( 19 | Guid.NewGuid(), 20 | "1.0.0", 21 | DateTime.Now, 22 | Guid.NewGuid(), 23 | "/Users/me", 24 | "migration.00001.sql", 25 | RandomNumberGenerator.GetHexString(32, lowercase: true), 26 | "agent", 27 | "host", 28 | Metrics: new Dictionary 29 | { 30 | ["db/rowsAffected"] = "10" 31 | }, 32 | OperationTags: new Dictionary 33 | { 34 | ["postgres/npgSqlVersion"] = "2.1.4", 35 | ["postgres/dapperVersion"] = "8.0.5" 36 | }, 37 | Metadata: new Dictionary 38 | { 39 | ["build"] = RandomNumberGenerator.GetHexString(8, lowercase: true), 40 | ["task"] = "DEV_2482", 41 | ["author"] = "dan" 42 | }); 43 | 44 | logger.LogInformation("Info = {@model}", model); -------------------------------------------------------------------------------- /src/Scopes/ScopeManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Vertical.SpectreLogger.Core; 4 | using Vertical.SpectreLogger.Formatting; 5 | 6 | namespace Vertical.SpectreLogger.Scopes 7 | { 8 | /// 9 | /// Manages logging scopes. 10 | /// 11 | internal sealed class ScopeManager 12 | { 13 | private readonly AsyncLocal _localScope = new(); 14 | 15 | /// 16 | /// Signaled when a scope is disposed. 17 | /// 18 | /// Scope 19 | internal void ScopeDisposed(LoggerScope scope) 20 | { 21 | _localScope.Value = scope.PreviousScope; 22 | } 23 | 24 | /// 25 | /// Begins a new scope. 26 | /// 27 | /// Scope value 28 | /// Value type 29 | /// 30 | internal IDisposable BeginScope(T value) 31 | { 32 | var current = _localScope.Value; 33 | var newScope = new LoggerScope(this, current, value ?? (object)NullValue.Default); 34 | 35 | _localScope.Value = newScope; 36 | 37 | return newScope; 38 | } 39 | 40 | /// 41 | /// Gets the current scope values. 42 | /// 43 | internal IScopeValues GetValues() => ScopeValues.Create(_localScope.Value); 44 | } 45 | } -------------------------------------------------------------------------------- /src/Rendering/MarginControlRenderer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Vertical.SpectreLogger.Core; 3 | using Vertical.SpectreLogger.Output; 4 | using Vertical.SpectreLogger.Templates; 5 | 6 | namespace Vertical.SpectreLogger.Rendering 7 | { 8 | /// 9 | /// Controls the margin in the template rendering. 10 | /// 11 | [Template(@"{Margin(?<_mode>[+-=])(?<_value>\d+)}")] 12 | public class MarginControlRenderer : ITemplateRenderer 13 | { 14 | private readonly string _mode; 15 | private readonly int _value; 16 | 17 | /// 18 | /// Creates a new instance of this type. 19 | /// 20 | /// Match object 21 | public MarginControlRenderer(Match match) 22 | { 23 | _mode = match.Groups["_mode"].Value; 24 | _value = int.Parse(match.Groups["_value"].Value); 25 | } 26 | 27 | /// 28 | public void Render(IWriteBuffer buffer, in LogEventContext context) 29 | { 30 | switch (_mode) 31 | { 32 | case "-": 33 | buffer.Margin -= _value; 34 | break; 35 | 36 | case "+": 37 | buffer.Margin += _value; 38 | break; 39 | 40 | default: 41 | buffer.Margin = _value; 42 | break; 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /docs/margin-control.md: -------------------------------------------------------------------------------- 1 | # Margin Control 2 | 3 | ## Overview 4 | 5 | Sets the margin for subsequent line breaks in the event output. 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {Margin} 11 | ``` 12 | 13 | ### Parameters 14 | 15 | |Parameter|Description| 16 | |---|---| 17 | |``|A control character that indicates the adjustment. `+` increments the current margin value, `-` decrements the current margin value, and `=` sets the absolute value of the margin.| 18 | |``|The number of spaces to insert before writing characters following a newline break in the event output.| 19 | 20 | ### Examples 21 | 22 | ``` 23 | {Margin+2} >> offset current margin by 2 spaces (to the right) 24 | {Margin-2} >> offset current margin by -2 spaces (to the left) 25 | {Margin=2} >> set the margin to 2 spaces regardless of where it currently is 26 | ``` 27 | 28 | ### Remarks 29 | 30 | This renderer does not output content, but more controls future writes to the console. All effects of the renderer apply only to the current log event. The margin is reset when the pipeline is complete. 31 | 32 | ### Example 33 | 34 | You want the logger to behave like Microsoft's console logger. This can be accomplished by customizing the output template and the log level formatting. 35 | 36 | ```csharp 37 | config.ConfigureProfiles(profile => 38 | profile.OutputTemplate = "{LogLevel}: {CategoryName}{Margin=6}{NewLine}{Message}{NewLine+}{Exception}" 39 | ); 40 | ``` 41 | 42 | ## See Also 43 | - [Next: Message](./message.md) 44 | - [Rendering Overview](./renderer-overview.md) -------------------------------------------------------------------------------- /src/Formatting/ValueWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Vertical.SpectreLogger.Formatting 5 | { 6 | /// 7 | /// Base class for value wrappers. 8 | /// 9 | /// Value type. 10 | public abstract class ValueWrapper : IFormattable where T : notnull 11 | { 12 | private static readonly EqualityComparer Comparer = EqualityComparer.Default; 13 | 14 | /// 15 | /// Creates a new instance of this type. 16 | /// 17 | /// 18 | protected ValueWrapper(T value) => Value = value; 19 | 20 | /// 21 | /// Gets the value. 22 | /// 23 | public T Value { get; } 24 | 25 | /// 26 | public override string ToString() => Value.ToString() ?? string.Empty; 27 | 28 | /// 29 | public string ToString(string? format, IFormatProvider? formatProvider) 30 | { 31 | return Value is IFormattable formattableValue 32 | ? formattableValue.ToString(format, formatProvider) 33 | : Value.ToString() ?? string.Empty; 34 | } 35 | 36 | /// 37 | public override int GetHashCode() => Value.GetHashCode(); 38 | 39 | /// 40 | public override bool Equals(object? obj) => obj is ValueWrapper other 41 | && Comparer.Equals(Value, other.Value); 42 | } 43 | } -------------------------------------------------------------------------------- /test/Rendering/Verified/ExceptionRendererTests.RenderLimitsStackFrames.Core.verified.txt: -------------------------------------------------------------------------------- 1 | Error: Error occurred 2 | System.AggregateException: One or more errors occurred. (Sequence contains no elements) 3 | at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) 4 | at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) 5 | at System.Threading.Tasks.Task.Wait() 6 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.GetAggregateException() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 7 | -> System.InvalidOperationException: Sequence contains no elements 8 | at System.Linq.ThrowHelper.ThrowNoElementsException() 9 | at System.Linq.Enumerable.First[[TSource]](IEnumerable`1 source) 10 | at Vertical.SpectreLogger.Tests.Infrastructure.ExceptionHelper.<>c.b__0_0() in {ProjectDirectory}Infrastructure/ExceptionHelper.cs:line {line} 11 | at System.Threading.Tasks.Task`1.InnerInvoke() 12 | at System.Threading.Tasks.Task.<>c.<.cctor>b__274_0(Object obj) 13 | at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state) 14 | --- End of stack trace from previous location where exception was thrown --- 15 | at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state) 16 | at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread) 17 | -------------------------------------------------------------------------------- /docs/scope-values.md: -------------------------------------------------------------------------------- 1 | # Scope Values Renderer 2 | 3 | ## Overview 4 | 5 | Prints all scopes available in the log event in sequence order. 6 | 7 | ### Placeholder Syntax 8 | 9 | ``` 10 | {Scopes} 11 | ``` 12 | 13 | ### Parameters 14 | 15 | None 16 | 17 | ### Options Configuration 18 | 19 | This renderer defines an `Options` type. The following properties are exposed: 20 | 21 | |Property|Description| 22 | |---|---| 23 | |`ContentBefore`|Gets/sets content that is printed before rendering the first scope value.| 24 | |`ContentBetween`|Gets/sets content that is printed between each scope.| 25 | |`ContentAfter`|Gets/sets content that is printed after the last scope value.| 26 | 27 | ### Remarks 28 | 29 | This renderer will print all scope values in the log event in sequence order. If a scope value is a structured log message, it will print the scope with structured log value substitutions. 30 | 31 | ```csharp 32 | config.ConfigureProfiles(profile => profile 33 | .ConfigureOptions(renderer => 34 | { 35 | renderer.ContentBetween = " => "; 36 | renderer.ContentAfter = " => "; 37 | })) 38 | .OutputTemplate = "{LogLevel}: {Scopes}{Message}"); 39 | 40 | // ... 41 | 42 | using var connectionScope = logger.BeginScope("connection={connection}, connectionId); 43 | using var userScope = logger.BeginScope("user={userId}", userId); 44 | 45 | logger.LogInformation("Session created"); 46 | 47 | // Output 48 | // Information: connection=103ae4ff => user=tester@vertical.com => Session created 49 | ``` 50 | 51 | ## See Also 52 | - [Next: Thread Id](./thread-id.md) 53 | - [Rendering Overview](./renderer-overview.md) -------------------------------------------------------------------------------- /src/Output/ConsoleWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Spectre.Console; 4 | 5 | namespace Vertical.SpectreLogger.Output 6 | { 7 | /// 8 | /// Base class for console writer implementations. 9 | /// 10 | public abstract class ConsoleWriter 11 | { 12 | private readonly IAnsiConsole _console; 13 | 14 | /// 15 | /// Creates a new instance of this type. 16 | /// 17 | /// implementation. 18 | protected ConsoleWriter(IAnsiConsole console) => _console = console; 19 | 20 | /// 21 | /// Writes a value to the console. 22 | /// 23 | /// Content to write. 24 | [ExcludeFromCodeCoverage] 25 | protected void WriteToConsole(string str) 26 | { 27 | try 28 | { 29 | _console.Markup(str); 30 | } 31 | catch (Exception exception) 32 | { 33 | _console.WriteLine("Markup error: " + exception.Message); 34 | _console.WriteLine("Tried to write:"); 35 | _console.WriteLine(str); 36 | } 37 | } 38 | 39 | /// 40 | /// Resets the console position. 41 | /// 42 | protected void ResetLineCore() 43 | { 44 | if (!Console.IsOutputRedirected) 45 | { 46 | AnsiConsole.Cursor.Move(CursorDirection.Left, Console.CursorLeft); 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /examples/Formatting/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Microsoft.Extensions.Logging; 4 | using Spectre.Console; 5 | using Vertical.SpectreLogger; 6 | using Vertical.SpectreLogger.Formatting; 7 | using Vertical.SpectreLogger.Options; 8 | using Vertical.SpectreLogger.Rendering; 9 | 10 | namespace Formatting 11 | { 12 | class ThreadFormatter : ICustomFormatter 13 | { 14 | /// 15 | public string Format(string? format, object? arg, IFormatProvider? formatProvider) 16 | { 17 | var thread = ((ThreadIdRenderer.Value) arg!).Value; 18 | 19 | return $"{thread.ManagedThreadId}:priority={thread.Priority}"; 20 | } 21 | } 22 | 23 | class Program 24 | { 25 | static void Main(string[] args) 26 | { 27 | var logger = LoggerFactory 28 | .Create(builder => 29 | { 30 | builder.AddSpectreConsole(specLogger => 31 | { 32 | specLogger.ConfigureProfiles(profile => 33 | { 34 | profile.AddTypeFormatter(new ThreadFormatter()); 35 | profile.AddTypeStyle($"[{Color.Pink1}]"); 36 | profile.OutputTemplate = "[[{LogLevel}/{ThreadId}]]: {CategoryName} - {Message}"; 37 | }); 38 | }); 39 | }) 40 | .CreateLogger(); 41 | 42 | logger.LogInformation("Displaying the thread id"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Destructuring/IDestructuringWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Destructuring 2 | { 3 | /// 4 | /// Writes an object in destructured format. 5 | /// 6 | public interface IDestructuringWriter 7 | { 8 | /// 9 | /// Writes the start of an object. 10 | /// 11 | void WriteStartObject(); 12 | 13 | /// 14 | /// Writes a property key/value. 15 | /// 16 | /// Property key 17 | /// Property value 18 | /// 19 | bool WriteProperty(string key, object? value); 20 | 21 | /// 22 | /// Writes an integral value. 23 | /// 24 | /// 25 | void WriteIntegral(object? value); 26 | 27 | /// 28 | /// Writes the end notation for an object. 29 | /// 30 | void WriteEndObject(); 31 | 32 | /// 33 | /// Writes the start of an array. 34 | /// 35 | void WriteStartArray(); 36 | 37 | /// 38 | /// Writes the end notation for an array. 39 | /// 40 | void WriteEndArray(); 41 | 42 | /// 43 | /// Writes an element of a collection. 44 | /// 45 | /// Value to write 46 | /// Whether the writer descended into 47 | bool WriteElement(object? value); 48 | } 49 | } -------------------------------------------------------------------------------- /src/SpectreLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using Vertical.SpectreLogger.Core; 5 | using Vertical.SpectreLogger.Options; 6 | using Vertical.SpectreLogger.Scopes; 7 | 8 | namespace Vertical.SpectreLogger 9 | { 10 | /// 11 | /// Logger provider for 12 | /// 13 | public class SpectreLoggerProvider : ILoggerProvider 14 | { 15 | private readonly IOptions _optionsProvider; 16 | private readonly IRendererPipeline _rendererPipeline; 17 | private readonly ScopeManager _scopeManager = new(); 18 | 19 | private readonly ConcurrentDictionary _cachedLoggers = new(); 20 | 21 | /// 22 | /// Creates a new instance of this provider type. 23 | /// 24 | public SpectreLoggerProvider( 25 | IOptions optionsProvider, 26 | IRendererPipeline rendererPipeline) 27 | { 28 | _optionsProvider = optionsProvider; 29 | _rendererPipeline = rendererPipeline; 30 | } 31 | 32 | /// 33 | public void Dispose() 34 | { 35 | // Not implemented 36 | } 37 | 38 | /// 39 | public ILogger CreateLogger(string categoryName) 40 | { 41 | return _cachedLoggers.GetOrAdd(categoryName, name => new SpectreLogger( 42 | _rendererPipeline, 43 | _optionsProvider.Value, 44 | _scopeManager, 45 | name)); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /examples/MinimumLevelOverrides/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using Microsoft.Extensions.Logging; 4 | using Vertical.SpectreLogger; 5 | 6 | var loggerFactory = LoggerFactory.Create(builder => 7 | { 8 | builder.AddSpectreConsole(options => 9 | { 10 | options.SetMinimumLevel(LogLevel.Debug); 11 | options.SetMinimumLevel("Vertical", LogLevel.Warning); 12 | 13 | options.ConfigureProfile(LogLevel.Debug, profile => profile.OutputTemplate = 14 | "[grey46][[{DateTime:T} Dbug]] {CategoryName}: {Message}{NewLine}{Exception}[/]"); 15 | options.ConfigureProfile(LogLevel.Information, profile => profile.OutputTemplate = 16 | "[grey85][[{DateTime:T} [green3_1]Info[/]]] {CategoryName}: {Message}{NewLine}{Exception}[/]"); 17 | options.ConfigureProfile(LogLevel.Warning, profile => profile.OutputTemplate = 18 | "[grey85][[{DateTime:T} [gold1]Warn[/]]] {CategoryName}: {Message}{NewLine}{Exception}[/]"); 19 | options.ConfigureProfile(LogLevel.Error, profile => profile.OutputTemplate = 20 | "[grey85][[{DateTime:T} [red1]Fail[/]]] {CategoryName}: {Message}{NewLine}{Exception}[/]"); 21 | }); 22 | builder.SetMinimumLevel(LogLevel.Debug); 23 | }); 24 | 25 | var logger = loggerFactory.CreateLogger("Vertical"); 26 | 27 | logger.LogDebug("Debug message"); 28 | logger.LogInformation("Info message"); 29 | logger.LogWarning("Warning message"); 30 | logger.LogError("Error message"); 31 | 32 | var otherLogger = loggerFactory.CreateLogger("Program"); 33 | otherLogger.LogDebug("Debug message"); 34 | otherLogger.LogInformation("Info message"); 35 | otherLogger.LogWarning("Warning message"); 36 | otherLogger.LogError("Error message"); -------------------------------------------------------------------------------- /test/Rendering/MessageRendererTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Shouldly; 3 | using Vertical.SpectreLogger.Options; 4 | using Vertical.SpectreLogger.Tests.Infrastructure; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Rendering 8 | { 9 | public class MessageRendererTests 10 | { 11 | [Fact] 12 | public void RenderWritesExpectedOutput() 13 | { 14 | RendererTestHarness.Capture(ConfigureSettings, 15 | logger => logger.LogInformation("plain message")) 16 | .ShouldBe("plain message"); 17 | } 18 | 19 | [Fact] 20 | public void RenderWritesExpectedOutputWithLogValues() 21 | { 22 | RendererTestHarness.Capture(ConfigureSettings, 23 | logger => logger.LogInformation("plain message {x} & {y}", 10, 20)) 24 | .ShouldBe("plain message 10 & 20"); 25 | } 26 | 27 | [Fact] 28 | public void RenderWritesExpectedOutputWithDestructuring() 29 | { 30 | RendererTestHarness.Capture(ConfigureSettings, 31 | logger => logger.LogInformation("plain message {@obj}", 32 | new{ Name="Testy McTesterson", Age=30 })) 33 | .ShouldBe("plain message {Name: Testy McTesterson, Age: 30}"); 34 | } 35 | 36 | private static void ConfigureSettings(SpectreLoggingBuilder config) 37 | { 38 | config.ConfigureProfiles(profile => 39 | { 40 | profile.ClearTypeFormatters(); 41 | profile.ClearTypeStyles(); 42 | profile.DefaultLogValueStyle = null; 43 | profile.OutputTemplate = "{Message}"; 44 | }); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /test/Rendering/CategoryNameRendererTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Logging; 4 | using Shouldly; 5 | using Vertical.SpectreLogger.Rendering; 6 | using Vertical.SpectreLogger.Tests.Infrastructure; 7 | using Xunit; 8 | 9 | namespace Vertical.SpectreLogger.Tests.Rendering 10 | { 11 | public class CategoryNameRendererTests 12 | { 13 | private readonly ICustomFormatter _testInstance = new CategoryNameRenderer.DefaultFormatter(); 14 | 15 | [Fact] 16 | public void RenderWritesExpectedValue() 17 | { 18 | RendererTestHarness.RunScenario( 19 | config => config.ConfigureProfiles(p => p.OutputTemplate = "{CategoryName}"), 20 | logger => logger.LogInformation(""), 21 | $@"^\[\w+\]{nameof(CategoryNameRendererTests)}\[/\]$", 22 | nameof(CategoryNameRendererTests)); 23 | } 24 | 25 | [Theory, MemberData(nameof(Theories))] 26 | public void FormatReturnsExpected(string? format, string arg, string expected) 27 | { 28 | _testInstance.Format(format, new CategoryNameRenderer.Value(arg), null).ShouldBe(expected); 29 | } 30 | 31 | public static IEnumerable Theories => new[] 32 | { 33 | new object?[]{ null, "Logger", "Logger" }, 34 | new object?[]{ null, "", "" }, 35 | new object?[]{ "C", "Logger", "Logger" }, 36 | new object?[]{ "C", "System.Logger", "Logger" }, 37 | new object?[]{ "S1", "System.Logger", "Logger" }, 38 | new object?[]{ "S2", "System.Logger", "System.Logger" }, 39 | new object?[]{ "S2", "System.Logging.Logger", "Logging.Logger" } 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /docs/docs-home.md: -------------------------------------------------------------------------------- 1 | # vertical-spectreconsolelogger 2 | 3 | A seriously customizable [Spectre Console](https://spectreconsole.net/) provider for Microsoft.Extensions.Logging. **Don't** change how your app logs - change how the logs are presented. 4 | 5 | ## Quick Start 6 | 7 | Add a package reference to your `.csproj` file: 8 | 9 | ``` 10 | $ dotnet add package vertical-spectreconsolelogger --prerelease 11 | ``` 12 | 13 | Call `AddSpectreConsole` in your logging setup: 14 | 15 | ```csharp 16 | var loggerFactory = LoggerFactory.Create(builder => builder 17 | .AddSpectreConsole()); 18 | 19 | var logger = loggerFactory.CreateLogger("MyLogger"); 20 | 21 | logger.LogInformation("Hello world!"); 22 | ``` 23 | 24 | ## Documentation 25 | 26 | - Introductory Topics 27 | - [Basic Configuration](basic-configuration.md) 28 | - [Output Templates](output-template.md) 29 | - [Formatting Log Values](formatting.md) 30 | - [Styling Log Values](styling.md) 31 | - [Destructured Output](destructuring.md) 32 | - Output Renderers 33 | - [Renderers Overview](renderer-overview.md) 34 | - [ActivityId](activity-id.md) 35 | - [CategoryName](category-name.md) 36 | - [DateTime](date-time.md) 37 | - [Exception](exceptions.md) 38 | - [MarginControl](margin-control.md) 39 | - [NewLine](newline.md) 40 | - [Scope](scope-value.md) 41 | - [Scopes](scopes-value) 42 | - [ThreadId](thread-id.md) 43 | - Advanced Topics 44 | - [Advanced Configuration](advanced-config.md) 45 | - API 46 | - Browse assembly documentation on [Tripleslash.io](https://tripleslash.io/docs/.net/vertical-spectreconsolelogger/0.10.1-dev.20230712.19/api/@index?view=net7.0) 47 | 48 | 49 | ## Examples 50 | 51 | Checkout and run our [examples](https://github.com/verticalsoftware/vertical-spectreconsolelogger/tree/dev/examples) to see the logger in action. 52 | -------------------------------------------------------------------------------- /src/Output/IWriteBuffer.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Output 2 | { 3 | /// 4 | /// Provides an interface for writing content. 5 | /// 6 | public interface IWriteBuffer 7 | { 8 | /// 9 | /// Gets the number of characters to indent anytime a newline character is 10 | /// encountered. 11 | /// 12 | int Margin { get; set; } 13 | 14 | /// 15 | /// Gets the number of characters that have been written since the last 16 | /// new line character. 17 | /// 18 | int LinePosition { get; } 19 | 20 | /// 21 | /// Writes a character to the buffer. 22 | /// 23 | /// Character to write. 24 | /// The number of times to repeat writing 25 | void Write(char c, int count = 1); 26 | 27 | /// 28 | /// Writes a string to the buffer. 29 | /// 30 | /// String to write 31 | void Write(string str); 32 | 33 | /// 34 | /// Writes a string or string portion. 35 | /// 36 | /// String value 37 | /// Starting index 38 | /// Number of characters to write 39 | void Write(string str, int startIndex, int length); 40 | 41 | /// 42 | /// Gets the length of the buffer. 43 | /// 44 | int Length { get; } 45 | 46 | /// 47 | /// Flushes the content of the buffer to an underlying output. 48 | /// 49 | void Flush(); 50 | } 51 | } -------------------------------------------------------------------------------- /bug-repro/solution.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiThreaded", "MultiThreaded\MultiThreaded.csproj", "{4F69D0AD-84FC-45A4-A05D-2B96A05B1E1A}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "refs", "refs", "{BCB48E71-40B8-444E-8E41-A0A143A25E55}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vertical.SpectreLogger", "..\src\Vertical.SpectreLogger.csproj", "{E18B75C2-DEC6-4E51-8B39-F738A408E872}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {4F69D0AD-84FC-45A4-A05D-2B96A05B1E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {4F69D0AD-84FC-45A4-A05D-2B96A05B1E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {4F69D0AD-84FC-45A4-A05D-2B96A05B1E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {4F69D0AD-84FC-45A4-A05D-2B96A05B1E1A}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {E18B75C2-DEC6-4E51-8B39-F738A408E872}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {E18B75C2-DEC6-4E51-8B39-F738A408E872}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {E18B75C2-DEC6-4E51-8B39-F738A408E872}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {E18B75C2-DEC6-4E51-8B39-F738A408E872}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(NestedProjects) = preSolution 31 | {E18B75C2-DEC6-4E51-8B39-F738A408E872} = {BCB48E71-40B8-444E-8E41-A0A143A25E55} 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /test/Output/BackgroundConsoleWriterTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using NSubstitute; 5 | using Spectre.Console; 6 | using Spectre.Console.Rendering; 7 | using Vertical.SpectreLogger.Output; 8 | using Xunit; 9 | 10 | namespace Vertical.SpectreLogger.Tests.Output 11 | { 12 | public class BackgroundConsoleWriterTests 13 | { 14 | [Fact] 15 | public async Task WriteCapturesContent() 16 | { 17 | var console = Substitute.For(); 18 | var services = new ServiceCollection() 19 | .AddLogging(builder => builder.AddSpectreConsole( 20 | config => 21 | { 22 | config.WriteInBackground(); 23 | config.ConfigureProfile(LogLevel.Information, profile => 24 | { 25 | profile.DefaultLogValueStyle = null; 26 | }); 27 | })) 28 | .AddSingleton(console) 29 | .BuildServiceProvider(); 30 | 31 | var logger = services.GetRequiredService>(); 32 | 33 | logger.LogInformation("Test event successful"); 34 | 35 | await services.DisposeAsync(); 36 | 37 | console.Received().Write(Arg.Any()); 38 | } 39 | 40 | [Fact] 41 | public void WritePushedContentAfterDispose() 42 | { 43 | var console = Substitute.For(); 44 | var writer = new BackgroundConsoleWriter(console); 45 | 46 | writer.Dispose(); 47 | writer.Write("test"); 48 | 49 | console.Received().Write(Arg.Any()); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /examples/DestructuredValues/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | using Vertical.SpectreLogger; 4 | using Vertical.SpectreLogger.Options; 5 | 6 | namespace DestructuredValues 7 | { 8 | internal record Address(string Street, string City, string State, int ZipCode); 9 | internal record Person(Guid Id, string FirstName, string LastName, Address Address, string[] Roles); 10 | 11 | internal class Program 12 | { 13 | static void Main(string[] args) 14 | { 15 | var person = new Person( 16 | Guid.NewGuid(), 17 | "Testy", 18 | "McTesterson", 19 | new Address("123 Main Street", "Denver", "CO", 80101), 20 | new[]{"NA-Colleague", "Manager", "Sales"}); 21 | 22 | WriteNonIndented(person); 23 | 24 | Console.WriteLine(); 25 | 26 | WriteIndented(person); 27 | } 28 | 29 | private static void WriteNonIndented(Person person) 30 | { 31 | var logger = LoggerFactory 32 | .Create(builder => builder.AddSpectreConsole()) 33 | .CreateLogger(); 34 | 35 | logger.LogInformation("User created (non-indented) = {@TheUser}", person); 36 | } 37 | 38 | private static void WriteIndented(Person person) 39 | { 40 | var logger = LoggerFactory 41 | .Create(builder => builder.AddSpectreConsole(console => console.ConfigureProfile( 42 | LogLevel.Information, 43 | profile => profile.ConfigureOptions(ds => ds.WriteIndented = true)))) 44 | .CreateLogger(); 45 | 46 | logger.LogInformation("User created (indented) = \n{@TheUser}", person); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Rendering/ExceptionRenderer.Options.cs: -------------------------------------------------------------------------------- 1 | namespace Vertical.SpectreLogger.Rendering 2 | { 3 | public partial class ExceptionRenderer 4 | { 5 | /// 6 | /// Options for 7 | /// 8 | public class Options 9 | { 10 | /// 11 | /// Gets or sets whether to unwind inner exceptions. 12 | /// 13 | public bool UnwindInnerExceptions { get; set; } = true; 14 | 15 | /// 16 | /// Gets or sets the maximum number of stack frames to show per exception. 17 | /// 18 | public int MaxStackFrames { get; set; } = 5; 19 | 20 | /// 21 | /// Gets or sets the number of characters to indent on each stack frame output. 22 | /// 23 | public int StackFrameIndent { get; set; } = 3; 24 | 25 | /// 26 | /// Gets or sets whether to display the parameter type in stack frame methods. 27 | /// 28 | public bool ShowParameterTypes { get; set; } = true; 29 | 30 | /// 31 | /// Gets or sets whether to display the parameter name in stack frame methods. 32 | /// 33 | public bool ShowParameterNames { get; set; } = true; 34 | 35 | /// 36 | /// Gets or sets whether to display the file name of a stack frame method. 37 | /// 38 | public bool ShowSourcePaths { get; set; } = true; 39 | 40 | /// 41 | /// Gets or sets whether to display the line number in files names of a stack frame 42 | /// method. 43 | /// 44 | public bool ShowSourceLocations { get; set; } = true; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Options/MicrosoftStyleLoggerOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Vertical.SpectreLogger.Options 4 | { 5 | /// 6 | /// Extensions for 7 | /// 8 | public static class MicrosoftStyleLoggerOptions 9 | { 10 | /// 11 | /// Configures the provider output similar to Microsoft's console logger implementation. 12 | /// 13 | /// Builder object 14 | /// 15 | public static SpectreLoggingBuilder UseMicrosoftConsoleStyle(this SpectreLoggingBuilder config) 16 | { 17 | config.ConfigureProfiles(profile => profile 18 | .AddTypeFormatter((_, level) => 19 | level switch 20 | { 21 | LogLevel.Trace => "trce", 22 | LogLevel.Debug => "dbug", 23 | LogLevel.Information => "info", 24 | LogLevel.Warning => "warn", 25 | LogLevel.Error => "fail", 26 | LogLevel.Critical => "crit", 27 | _ => string.Empty 28 | }) 29 | .OutputTemplate = "{LogLevel}: {CategoryName}{Margin=6}{NewLine}{Message}{NewLine}{Exception}"); 30 | 31 | config.ConfigureProfile(LogLevel.Information, profile => profile.AddTypeStyle("[green]")); 32 | config.ConfigureProfile(LogLevel.Warning, profile => profile.AddTypeStyle("[gold3_1]")); 33 | config.ConfigureProfile(LogLevel.Error, profile => profile.AddTypeStyle("[red1]")); 34 | config.ConfigureProfile(LogLevel.Critical, profile => profile.AddTypeStyle("[white on red1]")); 35 | 36 | return config; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Reflection/CompiledWriterFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Vertical.SpectreLogger.Destructuring; 4 | 5 | namespace Vertical.SpectreLogger.Reflection 6 | { 7 | internal abstract class CompiledWriterFactory 8 | { 9 | internal static readonly MethodInfo WriteStartObjectMethod = typeof(IDestructuringWriter) 10 | .GetMethod(nameof(DestructuringWriter.WriteStartObject))!; 11 | 12 | internal static readonly MethodInfo WriteEndObjectMethod = typeof(IDestructuringWriter) 13 | .GetMethod(nameof(DestructuringWriter.WriteEndObject))!; 14 | 15 | internal static readonly MethodInfo WritePropertyMethod = typeof(IDestructuringWriter) 16 | .GetMethod(nameof(DestructuringWriter.WriteProperty))!; 17 | 18 | internal static readonly MethodInfo WriteStartArrayMethod = typeof(IDestructuringWriter) 19 | .GetMethod(nameof(DestructuringWriter.WriteStartArray))!; 20 | 21 | internal static readonly MethodInfo WriteEndArrayMethod = typeof(IDestructuringWriter) 22 | .GetMethod(nameof(DestructuringWriter.WriteEndArray))!; 23 | 24 | internal static readonly MethodInfo WriteElementMethod = typeof(IDestructuringWriter) 25 | .GetMethod(nameof(DestructuringWriter.WriteElement))!; 26 | 27 | internal static readonly Type[] EmptyTypes = Array.Empty(); 28 | 29 | internal static CompiledWriter? CreateWriter(Type type) 30 | { 31 | if (DictionaryWriterFactory.TryCreate(type, out var implementation)) 32 | return implementation!; 33 | 34 | if (EnumerableWriterFactory.TryCreate(type, out implementation)) 35 | return implementation!; 36 | 37 | if (ObjectWriterFactory.TryCreate(type, out implementation)) 38 | return implementation!; 39 | 40 | return null; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /docs/styling.md: -------------------------------------------------------------------------------- 1 | # Styling 2 | 3 | ## Overview 4 | 5 | Like formatting, you can style values and types of values any way you want on an event-severity basis. 6 | 7 | ### Styling values 8 | 9 | Applying markup to values being rendered is configured by calling the `AddValueStyle` and `AddTypeStyle` methods on the `LogLevelProfile` object. When specifying markup, ensure it is enclosed in square brackets like you would using the Spectre Console otherwise. The logger will print closing tags automatically. 10 | 11 | Examples: 12 | 13 | ```csharp 14 | 15 | // Print all strings in orange 16 | config.ConfigureProfiles(profile => profile.AddTypeStyle("[orange3]")); 17 | 18 | // Print all strings in orange in Debug events only 19 | config.ConfigureProfile(LogLevel.Debug, profile => profile.AddTypeStyle("[orange3")); 20 | 21 | // Print all numbers in magenta (see the Types class for more groups) 22 | config.ConfigureProfiles(profile => profile.AddTypeStyle(Types.Numerics, "[magenta1]")); 23 | 24 | // Print boolean values in green and red 25 | config.ConfigureProfiles(profile => profile.AddValueStyle(false, "[red1]")); 26 | config.ConfigureProfiles(profile => profile.AddValueStyle(true, "[palegreen3]")); 27 | 28 | ``` 29 | 30 | The library defines several `Type` arrays that can be used to apply styling to several types at once. 31 | 32 | |Property|Description| 33 | |---|---| 34 | |`Types.Characters`|`char`, `char?`, `string`|, 35 | |`Types.Pointers`|`IntPtr`, `UIntPtr` + their `Nullable` variants 36 | |`Types.RealNumbers`|`float`, `double`, `decimal` + their `Nullable` variants.| 37 | |`Types.SignedIntegers`|`sbyte`, `uhort`, `int`, `long` + their `Nullable` variants.| 38 | |`Types.Temporal`|`DateTime`, `DateTimeOffset`, `TimeSpan` + their `Nullable` variants.| 39 | |`Types.UnsignedIntegers`|`byte`, `ushort`, `uint`, `ulong` + their `Nullable` variants.| 40 | 41 | ## See Also 42 | 43 | - [(Next) Destructured Output](./destructuring.md) 44 | - [Formatting Log Values](./formatting.md) 45 | -------------------------------------------------------------------------------- /src/Options/OptionsCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | 5 | namespace Vertical.SpectreLogger.Options 6 | { 7 | /// 8 | /// Manages options for renderer types. 9 | /// 10 | public class OptionsCollection 11 | { 12 | private readonly ConcurrentDictionary _options = new(); 13 | 14 | internal OptionsCollection() 15 | { 16 | } 17 | 18 | /// 19 | /// Clears all options from the collection. 20 | /// 21 | public void Clear() => _options.Clear(); 22 | 23 | /// 24 | /// Configures an options object for a renderer type. 25 | /// 26 | /// Delegate that configures the provided object. 27 | /// Options type. 28 | /// delegate is null 29 | public void Configure(Action configure) where TOptions : new() 30 | { 31 | if (configure == null) 32 | { 33 | throw new ArgumentNullException(nameof(configure)); 34 | } 35 | 36 | configure(GetOptions()); 37 | } 38 | 39 | /// 40 | /// Retrieves the value of an options object. 41 | /// 42 | /// Options type. 43 | /// 44 | public TOptions GetOptions() where TOptions : new() 45 | { 46 | var type = typeof(TOptions); 47 | 48 | return (TOptions)_options.GetOrAdd(type, new TOptions()); 49 | } 50 | 51 | /// 52 | public override string ToString() => $"[{string.Join(",", _options.Values.Select(v => v.GetType().Name))}]"; 53 | } 54 | } -------------------------------------------------------------------------------- /src/Core/LogEventContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | using Vertical.SpectreLogger.Options; 4 | 5 | namespace Vertical.SpectreLogger.Core 6 | { 7 | /// 8 | /// Defines the properties of a log event. 9 | /// 10 | public readonly struct LogEventContext 11 | { 12 | internal LogEventContext( 13 | string categoryName, 14 | LogLevel logLevel, 15 | EventId eventId, 16 | object? state, 17 | Exception? exception, 18 | IScopeValues scopeValues, 19 | LogLevelProfile profile) 20 | { 21 | CategoryName = categoryName; 22 | LogLevel = logLevel; 23 | EventId = eventId; 24 | State = state; 25 | Exception = exception; 26 | ScopeValues = scopeValues; 27 | Profile = profile; 28 | } 29 | 30 | /// 31 | /// Gets the category name of the logger that received the event. 32 | /// 33 | public string CategoryName { get; } 34 | 35 | /// 36 | /// Gets the log level. 37 | /// 38 | public LogLevel LogLevel { get; } 39 | 40 | /// 41 | /// Gets the event id. 42 | /// 43 | public EventId EventId { get; } 44 | 45 | /// 46 | /// Gets the event state data. 47 | /// 48 | public object? State { get; } 49 | 50 | /// 51 | /// Gets the exception. 52 | /// 53 | public Exception? Exception { get; } 54 | 55 | /// 56 | /// Gets the log event scope values. 57 | /// 58 | public IScopeValues ScopeValues { get; } 59 | 60 | /// 61 | /// Gets the log level profile. 62 | /// 63 | public LogLevelProfile Profile { get; } 64 | } 65 | } -------------------------------------------------------------------------------- /src/Templates/TemplateString.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Vertical.SpectreLogger.Templates 5 | { 6 | /// 7 | /// Defines methods to parse templates. 8 | /// 9 | public static class TemplateString 10 | { 11 | /// 12 | /// Splits a string into template segments. 13 | /// 14 | /// String to split. 15 | /// A callback that receives each 16 | /// is null. 17 | /// is null. 18 | public static void Split(string str, TemplateCallback callback) 19 | { 20 | if (str == null) 21 | { 22 | throw new ArgumentNullException(nameof(str)); 23 | } 24 | 25 | if (callback == null) 26 | { 27 | throw new ArgumentNullException(nameof(callback)); 28 | } 29 | 30 | var pattern = TemplatePatternBuilder.SplitPattern; 31 | 32 | var match = Regex.Match(str, pattern); 33 | var position = 0; 34 | 35 | for (; match.Success; match = match.NextMatch()) 36 | { 37 | if (match.Index > position) 38 | { 39 | // Report non-match segment 40 | callback(new TemplateSegment(null, str, position, match.Index - position)); 41 | } 42 | 43 | callback(new TemplateSegment(match, str, match.Index, match.Length)); 44 | position = match.Index + match.Length; 45 | } 46 | 47 | if (position < str.Length) 48 | { 49 | callback(new TemplateSegment(null, str, position, str.Length - position)); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Options/SpectreLoggerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Logging; 4 | using Vertical.SpectreLogger.Core; 5 | 6 | namespace Vertical.SpectreLogger.Options 7 | { 8 | /// 9 | /// Represents the options global to the logging provider. 10 | /// 11 | public class SpectreLoggerOptions 12 | { 13 | private int _maxPooledBuffers = 5; 14 | 15 | /// 16 | /// Gets or sets the minimum log level. 17 | /// 18 | public LogLevel MinimumLogLevel { get; set; } = LogLevel.Information; 19 | 20 | /// 21 | /// Gets or sets an object that controls log event filtering. 22 | /// 23 | public ILogEventFilter? LogEventFilter { get; set; } 24 | 25 | /// 26 | /// Gets a dictionary of log level overrides. 27 | /// 28 | public IDictionary MinimumLevelOverrides { get; } = new Dictionary(); 29 | 30 | /// 31 | /// Gets the log level profiles. 32 | /// 33 | public IReadOnlyDictionary LogLevelProfiles { get; } = 34 | new Dictionary 35 | { 36 | [LogLevel.Trace] = new(LogLevel.Trace), 37 | [LogLevel.Debug] = new(LogLevel.Debug), 38 | [LogLevel.Information] = new(LogLevel.Information), 39 | [LogLevel.Warning] = new(LogLevel.Warning), 40 | [LogLevel.Error] = new(LogLevel.Error), 41 | [LogLevel.Critical] = new(LogLevel.Critical) 42 | }; 43 | 44 | /// 45 | /// Gets or sets the maximum number of pooled buffers. 46 | /// 47 | public int MaxPooledBuffers 48 | { 49 | get => _maxPooledBuffers; 50 | set => _maxPooledBuffers = Math.Max(1, value); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Templates/TemplateDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Vertical.SpectreLogger.Core; 3 | using Vertical.SpectreLogger.Reflection; 4 | 5 | namespace Vertical.SpectreLogger.Templates 6 | { 7 | /// 8 | /// Represents the descriptor of a 9 | /// 10 | public class TemplateDescriptor : IEquatable 11 | { 12 | internal TemplateDescriptor(Type implementationType) 13 | { 14 | ImplementationType = implementationType ?? throw new ArgumentNullException(nameof(implementationType)); 15 | Template = GetTemplateValue(implementationType); 16 | 17 | if (TypeActivator.CanCreateInstanceOfType(implementationType, out var reason)) 18 | return; 19 | 20 | throw new ArgumentException(reason, nameof(implementationType)); 21 | } 22 | 23 | /// 24 | /// Gets the implementation type. 25 | /// 26 | public Type ImplementationType { get; } 27 | 28 | /// 29 | /// Gets the template that is associated with the renderer. 30 | /// 31 | public string Template { get; } 32 | 33 | /// 34 | public override string ToString() => $"{ImplementationType}=\"{Template}\""; 35 | 36 | /// 37 | public bool Equals(TemplateDescriptor? other) => ImplementationType == other?.ImplementationType && Template == other.Template; 38 | 39 | /// 40 | public override bool Equals(object? obj) => obj is TemplateDescriptor other && Equals(other); 41 | 42 | /// 43 | public override int GetHashCode() => HashCode.Combine(ImplementationType, Template); 44 | 45 | private static string GetTemplateValue(Type type) 46 | { 47 | return 48 | TemplateAttribute.ValueFromType(type) 49 | ?? 50 | throw new ArgumentException($"Type '{type}' does not have a template defined (use TemplateAttribute)"); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /test/Infrastructure/RendererTestHarness.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | using NSubstitute; 6 | using Shouldly; 7 | using Vertical.SpectreLogger.Options; 8 | using Vertical.SpectreLogger.Output; 9 | 10 | namespace Vertical.SpectreLogger.Tests.Infrastructure 11 | { 12 | public static class RendererTestHarness 13 | { 14 | internal static string Capture( 15 | Action configure, 16 | Action log, 17 | string? loggerName = null) 18 | { 19 | var builder = new StringBuilder(); 20 | var consoleWriter = Substitute.For(); 21 | consoleWriter 22 | .When(w => w.Write(Arg.Any())) 23 | .Do(callInfo => builder.Append((string)callInfo.Args()[0])); 24 | var buffer = new WriteBuffer(consoleWriter); 25 | var logger = LoggerFactory.Create(logging => 26 | logging 27 | .SetMinimumLevel(LogLevel.Trace) 28 | .AddSpectreConsole(opt => 29 | { 30 | opt.Services.AddSingleton(buffer); 31 | configure(opt); 32 | })) 33 | .CreateLogger(loggerName ?? "TestLogger"); 34 | 35 | log(logger); 36 | 37 | return builder.ToString(); 38 | } 39 | 40 | internal static void RunScenario( 41 | Action configure, 42 | Action log, 43 | string expectedOutputPattern, 44 | string? loggerName = null) 45 | { 46 | Capture(configure, log, loggerName).ShouldMatch(expectedOutputPattern); 47 | } 48 | 49 | internal static void RunScenario( 50 | Action configure, 51 | Action log, 52 | Action validateOutput, 53 | string? loggerName = null) 54 | { 55 | validateOutput(Capture(configure, log, loggerName)); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Rendering/LogLevelRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | using Vertical.SpectreLogger.Core; 4 | using Vertical.SpectreLogger.Formatting; 5 | using Vertical.SpectreLogger.Output; 6 | using Vertical.SpectreLogger.Templates; 7 | 8 | namespace Vertical.SpectreLogger.Rendering 9 | { 10 | /// 11 | /// Renders the log level. 12 | /// 13 | public class LogLevelRenderer : ITemplateRenderer 14 | { 15 | private readonly TemplateSegment _template; 16 | 17 | /// 18 | /// Defines the template for this renderer. 19 | /// 20 | [Template] 21 | public static readonly string Template = TemplatePatternBuilder 22 | .ForKey("LogLevel") 23 | .AddAlignment() 24 | .AddFormatting() 25 | .Build(); 26 | 27 | /// 28 | /// Implements a formatter for values. 29 | /// 30 | [TypeFormatter(typeof(LogLevel))] 31 | public class Formatter : ICustomFormatter 32 | { 33 | /// 34 | public string Format(string? format, object? arg, IFormatProvider? formatProvider) 35 | { 36 | var logLevel = (LogLevel) (arg ?? LogLevel.None); 37 | 38 | return logLevel switch 39 | { 40 | LogLevel.Trace => "Trce", 41 | LogLevel.Debug => "Dbug", 42 | LogLevel.Information => "Info", 43 | LogLevel.Warning => "Warn", 44 | LogLevel.Error => "Fail", 45 | LogLevel.Critical => "Crit", 46 | _ => string.Empty 47 | }; 48 | } 49 | } 50 | 51 | /// 52 | /// Creates a new instance. 53 | /// 54 | /// The matching template segment. 55 | public LogLevelRenderer(TemplateSegment template) => _template = template; 56 | 57 | /// 58 | public void Render(IWriteBuffer buffer, in LogEventContext context) 59 | { 60 | buffer.WriteLogValue(context.Profile, _template, context.LogLevel); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /test/Reflection/TypeActivatorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Reflection; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Reflection 8 | { 9 | public class TypeActivatorTests 10 | { 11 | public interface IService {} 12 | public class ServiceBase : IService {} 13 | public class Service : ServiceBase {} 14 | 15 | public class ActivatedTypeWithDefaultConstructor 16 | { 17 | } 18 | 19 | public class ActivatedType 20 | { 21 | public IService? Service { get; } 22 | 23 | public ActivatedType(){} 24 | 25 | public ActivatedType(string str, bool b, IService? service) 26 | { 27 | Service = service; 28 | } 29 | 30 | public ActivatedType(string str, bool b) 31 | { 32 | } 33 | } 34 | 35 | [Fact] 36 | public void CreateInstanceReturnsNonNullObject() 37 | { 38 | TypeActivator.CreateInstance(new object[] 39 | { 40 | new Service(), 41 | "a-string", 42 | true 43 | }.ToList()).ShouldNotBeNull(); 44 | } 45 | 46 | [Fact] 47 | public void CreateInstanceSelectsMoreSpecificConstructor() 48 | { 49 | var instance = TypeActivator.CreateInstance(new object[] 50 | { 51 | new Service(), 52 | "a-string", 53 | true 54 | }.ToList()); 55 | 56 | instance.Service.ShouldNotBeNull(); 57 | } 58 | 59 | [Fact] 60 | public void CreateInstanceSelectsLessSpecificConstructor() 61 | { 62 | TypeActivator.CreateInstance(new object[] 63 | { 64 | "a-string", 65 | true 66 | }.ToList()).ShouldNotBeNull(); 67 | } 68 | 69 | [Fact] 70 | public void CreateInstanceSelectsDefaultConstructor() 71 | { 72 | TypeActivator.CreateInstance(new List()).ShouldNotBeNull(); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/Rendering/ProcessIdRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Vertical.SpectreLogger.Core; 4 | using Vertical.SpectreLogger.Formatting; 5 | using Vertical.SpectreLogger.Output; 6 | using Vertical.SpectreLogger.Templates; 7 | 8 | namespace Vertical.SpectreLogger.Rendering 9 | { 10 | /// 11 | /// Renders the thread id (at the time of capture). 12 | /// 13 | public class ProcessIdRenderer : ITemplateRenderer 14 | { 15 | /// 16 | /// Defines the template for this renderer. 17 | /// 18 | [Template] 19 | public static readonly string Template = TemplatePatternBuilder 20 | .ForKey("ProcessId") 21 | .AddAlignment() 22 | .AddFormatting() 23 | .Build(); 24 | 25 | private readonly TemplateSegment _template; 26 | 27 | /// 28 | /// Wraps the thread value. 29 | /// 30 | public class Value : ValueWrapper 31 | { 32 | /// 33 | /// Creates a new instance of this type. The thread id is automatically assigned. 34 | /// 35 | public Value() : base(Process.GetCurrentProcess()) 36 | { 37 | } 38 | } 39 | 40 | /// 41 | /// The default formatter for this type. 42 | /// 43 | [TypeFormatter(typeof(Value))] 44 | public class DefaultFormatter : ICustomFormatter 45 | { 46 | /// 47 | public string Format(string? format, object? arg, IFormatProvider? formatProvider) => 48 | ((Value) arg!).Value.Id.ToString(format, formatProvider); 49 | } 50 | 51 | /// 52 | /// Creates a new instance of this type. 53 | /// 54 | /// Template 55 | public ProcessIdRenderer(TemplateSegment template) => _template = template; 56 | 57 | /// 58 | public void Render(IWriteBuffer buffer, in LogEventContext context) 59 | { 60 | buffer.WriteLogValue( 61 | context.Profile, 62 | _template, 63 | new Value()); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/Rendering/ThreadIdRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Vertical.SpectreLogger.Core; 4 | using Vertical.SpectreLogger.Formatting; 5 | using Vertical.SpectreLogger.Output; 6 | using Vertical.SpectreLogger.Templates; 7 | 8 | namespace Vertical.SpectreLogger.Rendering 9 | { 10 | /// 11 | /// Renders the thread id (at the time of capture). 12 | /// 13 | public class ThreadIdRenderer : ITemplateRenderer 14 | { 15 | /// 16 | /// Defines the template for this renderer. 17 | /// 18 | [Template] 19 | public static readonly string Template = TemplatePatternBuilder 20 | .ForKey("ThreadId") 21 | .AddAlignment() 22 | .AddFormatting() 23 | .Build(); 24 | 25 | private readonly TemplateSegment _template; 26 | 27 | /// 28 | /// Wraps the thread value. 29 | /// 30 | public class Value : ValueWrapper 31 | { 32 | /// 33 | /// Creates a new instance of this type. The thread id is automatically assigned. 34 | /// 35 | public Value() : base(Thread.CurrentThread) 36 | { 37 | } 38 | } 39 | 40 | /// 41 | /// The default formatter for this type. 42 | /// 43 | [TypeFormatter(typeof(Value))] 44 | public class DefaultFormatter : ICustomFormatter 45 | { 46 | /// 47 | public string Format(string? format, object? arg, IFormatProvider? formatProvider) => 48 | ((Value) arg!).Value.ManagedThreadId.ToString(format, formatProvider); 49 | } 50 | 51 | /// 52 | /// Creates a new instance of this type. 53 | /// 54 | /// Template segment 55 | public ThreadIdRenderer(TemplateSegment template) => _template = template; 56 | 57 | /// 58 | public void Render(IWriteBuffer buffer, in LogEventContext context) 59 | { 60 | buffer.WriteLogValue( 61 | context.Profile, 62 | _template, 63 | new Value()); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/Reflection/ObjectWriterFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using Vertical.SpectreLogger.Destructuring; 6 | 7 | namespace Vertical.SpectreLogger.Reflection 8 | { 9 | internal class ObjectWriterFactory : CompiledWriterFactory 10 | { 11 | internal static bool TryCreate(Type type, out CompiledWriter? writer) 12 | { 13 | writer = null; 14 | 15 | // No structs in System 16 | if (type.Namespace == nameof(System) && type.IsValueType) 17 | return false; 18 | 19 | if (type == typeof(string)) 20 | return false; 21 | 22 | var properties = type 23 | .GetProperties() 24 | .Where(prop => !prop.Name.StartsWith("get_") && prop.GetIndexParameters().Length == 0) 25 | .ToArray(); 26 | 27 | if (properties.Length == 0) 28 | return false; 29 | 30 | // Parameters 31 | var sourceParam = Expression.Parameter(typeof(object)); 32 | var writerParam = Expression.Parameter(typeof(IDestructuringWriter)); 33 | 34 | // Variables 35 | var typeParam = Expression.Variable(type); 36 | 37 | // Method body 38 | var body = new List(properties.Length + 5) 39 | { 40 | Expression.Assign(typeParam, Expression.Convert(sourceParam, type)), 41 | Expression.Call(writerParam, WriteStartObjectMethod) 42 | }; 43 | 44 | foreach (var property in properties) 45 | { 46 | body.Add(Expression.Call( 47 | writerParam, 48 | WritePropertyMethod, 49 | Expression.Constant(property.Name), 50 | Expression.Convert(Expression.Property(typeParam, property), typeof(object)))); 51 | } 52 | 53 | body.Add(Expression.Call(writerParam, WriteEndObjectMethod)); 54 | 55 | var lambda = Expression.Lambda(Expression.Block( 56 | new []{typeParam}, body), writerParam, sourceParam); 57 | 58 | writer = lambda.Compile(); 59 | 60 | return true; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Rendering/ScopeValuesRenderer.cs: -------------------------------------------------------------------------------- 1 | using Vertical.SpectreLogger.Core; 2 | using Vertical.SpectreLogger.Output; 3 | using Vertical.SpectreLogger.Templates; 4 | 5 | namespace Vertical.SpectreLogger.Rendering 6 | { 7 | /// 8 | /// Renders scope values in the log event. 9 | /// 10 | [Template("{Scopes}")] 11 | public class ScopeValuesRenderer : ITemplateRenderer 12 | { 13 | /// 14 | /// Options for 15 | /// 16 | public sealed class Options 17 | { 18 | /// 19 | /// Gets or sets content to output before rendering scopes. 20 | /// 21 | public string? ContentBefore { get; set; } 22 | 23 | /// 24 | /// Gets or sets content to output between each item. 25 | /// 26 | public string? ContentBetween { get; set; } = " => "; 27 | 28 | /// 29 | /// Gets or sets content to output after rendering scopes. 30 | /// 31 | public string? ContentAfter { get; set; } = " => "; 32 | } 33 | 34 | /// 35 | public void Render(IWriteBuffer buffer, in LogEventContext context) 36 | { 37 | var scopeValues = context.ScopeValues; 38 | 39 | if (!scopeValues.HasValues) 40 | return; 41 | 42 | var profile = context.Profile; 43 | var options = profile.ConfiguredOptions.GetOptions(); 44 | 45 | if (options.ContentBefore != null) 46 | { 47 | buffer.Write(options.ContentBefore); 48 | } 49 | 50 | var items = scopeValues.Values; 51 | var length = items.Count; 52 | 53 | for (var c = 0; c < length; c++) 54 | { 55 | buffer.WriteTemplateValue(profile, destructureValues: true, items[c]); 56 | 57 | if (options.ContentBetween != null && c != length - 1) 58 | { 59 | buffer.Write(options.ContentBetween); 60 | } 61 | } 62 | 63 | if (options.ContentAfter != null) 64 | { 65 | buffer.Write(options.ContentAfter); 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /test/Templates/TemplatePatternBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | using Shouldly; 4 | using Vertical.SpectreLogger.Templates; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Templates 8 | { 9 | public class TemplatePatternBuilderTests 10 | { 11 | [Fact] 12 | public void SplitPatternFieldReturnsExpectedPattern() 13 | { 14 | var pattern = TemplatePatternBuilder.SplitPattern; 15 | 16 | Should.NotThrow(() => new Regex(pattern)); 17 | 18 | pattern.ShouldBe(@"(?(?<_key>(?<_ds>@)?[^,:{}>]+)(?:(?<_ctl>[^,:{}]+))?(?<_cfmt>(?<_wdspan>,(?<_wd>-?\d+))?(?<_fmspan>:(?<_fm>[^}]+))?))}"); 19 | } 20 | 21 | [Theory, MemberData(nameof(Theories))] 22 | public void ValueReturnsExpectedPattern(TemplatePatternBuilder patternBuilder, string expected) 23 | { 24 | var pattern = patternBuilder.Build(); 25 | 26 | Should.NotThrow(() => new Regex(pattern)); 27 | 28 | pattern.ShouldBe(expected); 29 | } 30 | 31 | public static IEnumerable Theories = new[] 32 | { 33 | new object[] 34 | { 35 | TemplatePatternBuilder.ForKey("Template"), 36 | @"(?(?<_key>Template))}" 37 | }, 38 | new object[] 39 | { 40 | TemplatePatternBuilder.ForKey("Template") 41 | .AddControlPattern(@"\d+"), 42 | @"(?(?<_key>Template)(?:(?<_ctl>\d+))?)}" 43 | }, 44 | new object[] 45 | { 46 | TemplatePatternBuilder.ForKey("Template") 47 | .AddControlPattern(@"\d+") 48 | .AddAlignment(), 49 | @"(?(?<_key>Template)(?:(?<_ctl>\d+))?(?<_cfmt>(?<_wdspan>,(?<_wd>-?\d+))?))}" 50 | }, 51 | new object[] 52 | { 53 | TemplatePatternBuilder.ForKey("Template") 54 | .AddControlPattern(@"\d+") 55 | .AddAlignment() 56 | .AddFormatting(), 57 | @"(?(?<_key>Template)(?:(?<_ctl>\d+))?(?<_cfmt>(?<_wdspan>,(?<_wd>-?\d+))?(?<_fmspan>:(?<_fm>[^}]+))?))}" 58 | } 59 | }; 60 | } 61 | } -------------------------------------------------------------------------------- /test/Threading/MultiThreadedLoggingTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using Microsoft.Extensions.Logging; 7 | using NSubstitute; 8 | using Vertical.SpectreLogger.Output; 9 | using Xunit; 10 | 11 | namespace Vertical.SpectreLogger.Tests.Threading 12 | { 13 | public class MultiThreadedLoggingTests : IClassFixture 14 | { 15 | private readonly Fixture _fixture; 16 | 17 | public MultiThreadedLoggingTests(Fixture fixture) => _fixture = fixture; 18 | public class Fixture 19 | { 20 | private readonly IServiceProvider _provider; 21 | 22 | public Fixture() 23 | { 24 | var services = new ServiceCollection(); 25 | services.AddLogging(builder => builder.AddSpectreConsole()); 26 | services.Replace(ServiceDescriptor.Singleton( 27 | Substitute.For())); 28 | _provider = services.BuildServiceProvider(); 29 | } 30 | 31 | public ILogger GetLogger() => _provider.GetRequiredService() 32 | .CreateLogger("test"); 33 | } 34 | 35 | [Fact] 36 | public async Task LogFromMultipleThreadsDoesNotThrow() 37 | { 38 | var logger = _fixture.GetLogger(); 39 | var threads = Enumerable 40 | .Range(0, 25) 41 | .Select(id => Task.Run(async () => 42 | { 43 | await Task.Delay(250); 44 | for (var c = 0; c < 25; c++) 45 | { 46 | if (c % 10 == 0) 47 | { 48 | var exception = new NotSupportedException(); 49 | logger.LogError(exception, "An error is being reported, iteration {i}, thread {t}", 50 | c, id); 51 | continue; 52 | } 53 | 54 | logger.LogInformation("Information is being reported, iteration {i}, thread{t}", 55 | c, id); 56 | } 57 | })); 58 | await Task.WhenAll(threads); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /docs/destructuring.md: -------------------------------------------------------------------------------- 1 | # Destructured Output 2 | 3 | ## Overview 4 | 5 | Destructuring is decomposing a complex object for output. This involves printing properties of an object, or printing the items of a collection. When using structured log messages, if you proceed a complex type with `@`, the object will be printed in JSON-ish notation. When printing the object, the logging provider will obey formatting and styling rules for each value. 6 | 7 | ### Example 8 | 9 | ```csharp 10 | public record Person(int Id, string Name); 11 | 12 | // .. 13 | 14 | var person = new Person(10, "vertical-dev"); 15 | 16 | logger.LogInformation("Record saved, item={person}", person); 17 | logger.LogInformation("Record saved, destructured item={@person}", person); 18 | 19 | // Output 20 | // Record saved, item=Vertical.Example.Person 21 | // Record saved, destructured item={Id=10, Name=vertical-dev} 22 | ``` 23 | 24 | ### Controlling Destructuring 25 | 26 | You can control certain aspects of destructuring by configuring the `DestructuringOptions` type. The following properties are available: 27 | 28 | |Property|Description| 29 | |---|---| 30 | |IndentSize|Get/sets the number of spaces that comprise an indentation level - defaults to 4.| 31 | |MaxDepth|Gets/sets how many times the writer will recursively descend into the child properties of an object.| 32 | |MaxCollectionItems|Gets/sets the maximum number of items to display in a collection.| 33 | |MaxProperties|Gets/sets the maximum number of properties to display of an object.| 34 | |WriteIndented|Get/sets whether to indent the output on multiple lines, e.g. *pretty-print*| 35 | 36 | ### Emitted Types 37 | 38 | The following types can be formatted & styled: 39 | 40 | | Type | Description | 41 | | ----------------------------------------- | -------------------------------------------------------------------------------------------------- | 42 | | `DestructuredKeyValue` | Wraps the value of a destructured object key. | 43 | 44 | 45 | ### Example 46 | 47 | ```csharp 48 | config.ConfigureProfiles(profile => profile.ConfigureOptions( 49 | destructuring => destructuring.MaxDepth = 3)); 50 | ``` 51 | 52 | ## See Also 53 | 54 | - [Rendering Overview](./renderer-overview.md) 55 | - [Basic Configuration](./basic-configuration.md) -------------------------------------------------------------------------------- /src/Rendering/DateTimeRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Vertical.SpectreLogger.Core; 3 | using Vertical.SpectreLogger.Formatting; 4 | using Vertical.SpectreLogger.Output; 5 | using Vertical.SpectreLogger.Templates; 6 | 7 | namespace Vertical.SpectreLogger.Rendering 8 | { 9 | /// 10 | /// Renders the date time produced by the configured factory. 11 | /// 12 | public class DateTimeRenderer : ITemplateRenderer 13 | { 14 | private readonly TemplateSegment _template; 15 | 16 | /// 17 | /// Options for 18 | /// 19 | public sealed class Options 20 | { 21 | /// 22 | /// Gets or sets a function that returns the desired 23 | /// 24 | /// 25 | public Func? ValueFactory { get; set; } = () => DateTimeOffset.Now; 26 | } 27 | 28 | /// 29 | /// Defines the template for this renderer. 30 | /// 31 | [Template] 32 | public static readonly string Template = TemplatePatternBuilder 33 | .ForKey("[Dd]ate[Tt]ime") 34 | .AddAlignment() 35 | .AddFormatting() 36 | .Build(); 37 | 38 | /// 39 | /// Emits the date/time value 40 | /// 41 | public class Value : ValueWrapper 42 | { 43 | /// 44 | public Value(DateTimeOffset value) : base(value) 45 | { 46 | } 47 | } 48 | 49 | /// 50 | /// Creates a new instance of this type. 51 | /// 52 | /// Matching template segment. 53 | public DateTimeRenderer(TemplateSegment template) => _template = template; 54 | 55 | /// 56 | public void Render(IWriteBuffer buffer, in LogEventContext context) 57 | { 58 | var renderValue = context 59 | .Profile 60 | .ConfiguredOptions 61 | .GetOptions() 62 | .ValueFactory?.Invoke() ?? DateTimeOffset.Now; 63 | 64 | buffer.WriteLogValue( 65 | context.Profile, 66 | _template, 67 | new Value(renderValue)); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /examples/CustomRenderer/IncrementingId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading; 4 | 5 | namespace CustomRenderer 6 | { 7 | public readonly struct IncrementingId : IFormattable 8 | { 9 | private readonly int _increment; 10 | private readonly int _thread; 11 | private readonly int _process; 12 | private readonly int _epoch; 13 | private static int globalIncrement; 14 | 15 | private IncrementingId(int increment, int thread, int process, int epoch) 16 | { 17 | _increment = increment; 18 | _thread = thread; 19 | _process = process; 20 | _epoch = epoch; 21 | } 22 | 23 | public static IncrementingId Create() => new( 24 | Interlocked.Increment(ref globalIncrement), 25 | Thread.CurrentThread.ManagedThreadId, 26 | Environment.ProcessId, 27 | (int)DateTimeOffset.UtcNow.Subtract(DateTimeOffset.UnixEpoch).TotalSeconds); 28 | 29 | /// 30 | public override string ToString() => ToString("", null); 31 | 32 | /// 33 | public string ToString(string? format, IFormatProvider? formatProvider) 34 | { 35 | var space = format == "N" ? string.Empty : "-"; 36 | 37 | return new StringBuilder() 38 | .AppendFormat("{0:x}", (byte) (_epoch >> 24)) 39 | .AppendFormat("{0:x}", (byte) (_epoch >> 16)) 40 | .AppendFormat("{0:x}", (byte) (_epoch >> 8)) 41 | .AppendFormat("{0:x}", (byte) _epoch) 42 | .AppendFormat(space) 43 | .AppendFormat("{0:x}", (byte) (_process >> 24)) 44 | .AppendFormat("{0:x}", (byte) (_process >> 16)) 45 | .AppendFormat("{0:x}", (byte) (_process >> 8)) 46 | .AppendFormat("{0:x}", (byte) _process) 47 | .AppendFormat("{0:x}", (byte) (_thread >> 24)) 48 | .AppendFormat("{0:x}", (byte) (_thread >> 16)) 49 | .AppendFormat("{0:x}", (byte) (_thread >> 8)) 50 | .AppendFormat("{0:x}", (byte) _thread) 51 | .AppendFormat(space) 52 | .AppendFormat("{0:x}", (byte) (_increment >> 24)) 53 | .AppendFormat("{0:x}", (byte) (_increment >> 16)) 54 | .AppendFormat("{0:x}", (byte) (_increment >> 8)) 55 | .AppendFormat("{0:x}", (byte) _increment) 56 | .ToString(); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/LoggingBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.ObjectPool; 5 | using Microsoft.Extensions.Options; 6 | using Spectre.Console; 7 | using Vertical.SpectreLogger.Core; 8 | using Vertical.SpectreLogger.Internal; 9 | using Vertical.SpectreLogger.Options; 10 | using Vertical.SpectreLogger.Output; 11 | using Vertical.SpectreLogger.Rendering; 12 | using Vertical.SpectreLogger.Scopes; 13 | using Vertical.SpectreLogger.Templates; 14 | 15 | namespace Vertical.SpectreLogger 16 | { 17 | /// 18 | /// Extensions for 19 | /// 20 | public static class LoggingBuilderExtensions 21 | { 22 | /// 23 | /// Adds the spectre console logging provider. 24 | /// 25 | /// Logging builder instance 26 | /// A delegate that receives an options that can control 27 | /// how the logger renders events. 28 | /// 29 | public static ILoggingBuilder AddSpectreConsole( 30 | this ILoggingBuilder builder, 31 | Action? configureBuilder = null) 32 | { 33 | var services = builder.Services; 34 | var optionsBuilder = new SpectreLoggingBuilder(services); 35 | 36 | services.AddTransient(); 37 | services.AddSingleton(AnsiConsole.Console); 38 | services.AddSingleton(); 39 | services.AddSingleton(); 40 | services.AddSingleton(); 41 | services.AddTransient(); 42 | services.AddSingleton>(sp => new DefaultObjectPool( 43 | new WriteBufferPooledObjectPolicy( 44 | sp.GetRequiredService(), 45 | sp.GetRequiredService), 46 | sp.GetRequiredService>().Value.MaxPooledBuffers)); 47 | 48 | optionsBuilder 49 | .AddTemplateRenderers() 50 | .WriteInForeground() 51 | .UseConsole(AnsiConsole.Console) 52 | .SetMinimumLevel(LogLevel.Information); 53 | 54 | configureBuilder?.Invoke(optionsBuilder); 55 | 56 | return builder; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/Templates/TemplateRendererBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | using Microsoft.Extensions.Options; 4 | using Vertical.SpectreLogger.Core; 5 | using Vertical.SpectreLogger.Options; 6 | using Vertical.SpectreLogger.Reflection; 7 | using Vertical.SpectreLogger.Rendering; 8 | 9 | namespace Vertical.SpectreLogger.Templates 10 | { 11 | internal class TemplateRendererBuilder : ITemplateRendererBuilder 12 | { 13 | private readonly IEnumerable _descriptors; 14 | private readonly SpectreLoggerOptions _options; 15 | 16 | /// 17 | /// Creates a new instance of this type. 18 | /// 19 | /// Options provider. 20 | /// Descriptors. 21 | public TemplateRendererBuilder( 22 | IOptions optionsProvider, 23 | IEnumerable descriptors) 24 | { 25 | _descriptors = descriptors; 26 | _options = optionsProvider.Value; 27 | } 28 | 29 | /// 30 | public IReadOnlyList GetRenderers(string templateString) 31 | { 32 | var rendererList = new List(16); 33 | 34 | TemplateString.Split(templateString, (in TemplateSegment segment) => 35 | { 36 | rendererList.Add(SelectRenderer(segment)); 37 | }); 38 | 39 | return rendererList; 40 | } 41 | 42 | private ITemplateRenderer SelectRenderer(in TemplateSegment segment) 43 | { 44 | if (!segment.IsTemplate) 45 | { 46 | return new StaticSpanRenderer(segment.Value); 47 | } 48 | 49 | foreach (var descriptor in _descriptors) 50 | { 51 | var match = Regex.Match(segment.Value, descriptor.Template); 52 | 53 | if (!match.Success) 54 | continue; 55 | 56 | var parameters = new List 57 | { 58 | new TemplateSegment(match, 59 | segment.Value, 60 | 0, 61 | segment.Value.Length), 62 | match, 63 | _options 64 | }; 65 | 66 | return (ITemplateRenderer)TypeActivator.CreateInstance(descriptor.ImplementationType, parameters); 67 | } 68 | 69 | return new StaticSpanRenderer(segment.Value); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/Output/BackgroundConsoleWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading; 4 | using Spectre.Console; 5 | 6 | namespace Vertical.SpectreLogger.Output 7 | { 8 | internal class BackgroundConsoleWriter : ConsoleWriter, IConsoleWriter, IDisposable 9 | { 10 | private const int MaxQueuedMessages = 1024; 11 | private const char ResetLineCoreFlag = '\x3'; 12 | 13 | private readonly BlockingCollection _queue = new(MaxQueuedMessages); 14 | private readonly Thread _outputThread; 15 | 16 | /// 17 | /// Creates a new instance of this type. 18 | /// 19 | /// AnsiConsole 20 | public BackgroundConsoleWriter(IAnsiConsole ansiConsole) : base(ansiConsole) 21 | { 22 | _outputThread = new Thread(MessagePump) 23 | { 24 | IsBackground = true, 25 | Name = "SpectreConsole message processing thread" 26 | }; 27 | _outputThread.Start(); 28 | } 29 | 30 | private void MessagePump() 31 | { 32 | try 33 | { 34 | foreach (var entry in _queue.GetConsumingEnumerable()) 35 | { 36 | if (entry.Length == 1 && entry[0] == ResetLineCoreFlag) 37 | { 38 | ResetLineCore(); 39 | continue; 40 | } 41 | 42 | WriteToConsole(entry); 43 | } 44 | } 45 | catch 46 | { 47 | try 48 | { 49 | _queue.CompleteAdding(); 50 | } 51 | catch 52 | { 53 | // Ignored 54 | } 55 | } 56 | } 57 | 58 | public void Dispose() 59 | { 60 | _queue.CompleteAdding(); 61 | 62 | try 63 | { 64 | _outputThread.Join(1500); 65 | } 66 | catch (ThreadStateException) 67 | { 68 | // Ignored because there is no recovery 69 | } 70 | } 71 | 72 | /// 73 | public void ResetLine() 74 | { 75 | _queue.Add($"{ResetLineCoreFlag}"); 76 | } 77 | 78 | /// 79 | public void Write(string content) 80 | { 81 | if (_queue.IsAddingCompleted) 82 | { 83 | WriteToConsole(content); 84 | return; 85 | } 86 | 87 | _queue.Add(content); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/Rendering/RendererPipeline.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.ObjectPool; 5 | using Microsoft.Extensions.Options; 6 | using Vertical.SpectreLogger.Core; 7 | using Vertical.SpectreLogger.Options; 8 | using Vertical.SpectreLogger.Output; 9 | using Vertical.SpectreLogger.Templates; 10 | 11 | namespace Vertical.SpectreLogger.Rendering 12 | { 13 | internal sealed class RendererPipeline : IRendererPipeline 14 | { 15 | private readonly Dictionary> _pipelines; 16 | private readonly ObjectPool _bufferPool; 17 | 18 | /// 19 | /// Creates a new instance. 20 | /// 21 | /// Options provider for 22 | /// Object that builds renderers. 23 | /// Buffer pool 24 | public RendererPipeline( 25 | IOptions optionsProvider, 26 | ITemplateRendererBuilder rendererBuilder, 27 | ObjectPool bufferPool) 28 | { 29 | var options = optionsProvider.Value; 30 | 31 | _pipelines = options 32 | .LogLevelProfiles 33 | .ToDictionary( 34 | entry => entry.Key, 35 | entry => CreatePipeline(rendererBuilder, entry.Value)); 36 | 37 | _bufferPool = bufferPool; 38 | } 39 | 40 | /// 41 | public void Render(in LogEventContext logEventContext) 42 | { 43 | var renderers = _pipelines[logEventContext.LogLevel]; 44 | var count = renderers.Count; 45 | var buffer = _bufferPool.Get(); 46 | 47 | try 48 | { 49 | for (var c = 0; c < count; c++) 50 | { 51 | renderers[c].Render(buffer, logEventContext); 52 | } 53 | 54 | buffer.Margin = 0; 55 | } 56 | finally 57 | { 58 | _bufferPool.Return(buffer); 59 | } 60 | } 61 | 62 | private static IReadOnlyList CreatePipeline( 63 | ITemplateRendererBuilder rendererBuilder, 64 | LogLevelProfile profile) 65 | { 66 | return string.IsNullOrEmpty(profile.OutputTemplate) 67 | ? new[] {new StaticSpanRenderer($"LogLevelProfile '{profile.LogLevel}' has no defined output template.")} 68 | : rendererBuilder.GetRenderers(profile.OutputTemplate!); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/SpectreLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Microsoft.Extensions.Logging; 4 | using Vertical.SpectreLogger.Core; 5 | using Vertical.SpectreLogger.Internal; 6 | using Vertical.SpectreLogger.Options; 7 | using Vertical.SpectreLogger.Scopes; 8 | 9 | namespace Vertical.SpectreLogger 10 | { 11 | /// 12 | /// Implementation of the logger. 13 | /// 14 | public class SpectreLogger : ILogger 15 | { 16 | private readonly ILogEventFilter? _logEventFilter; 17 | private readonly IRendererPipeline _rendererPipeline; 18 | private readonly ScopeManager _scopeManager; 19 | private readonly string _categoryName; 20 | private readonly SpectreLoggerOptions _options; 21 | private readonly LogLevel _minimumLevel; 22 | 23 | internal SpectreLogger( 24 | IRendererPipeline rendererPipeline, 25 | SpectreLoggerOptions options, 26 | ScopeManager scopeManager, 27 | string categoryName) 28 | { 29 | _rendererPipeline = rendererPipeline; 30 | _scopeManager = scopeManager; 31 | _categoryName = categoryName; 32 | _options = options; 33 | _logEventFilter = _options.LogEventFilter; 34 | _minimumLevel = options.GetLogLevelFilter(categoryName); 35 | } 36 | 37 | /// 38 | public void Log( 39 | LogLevel logLevel, 40 | EventId eventId, 41 | TState state, 42 | Exception? exception, 43 | Func formatter) 44 | { 45 | if (!IsEnabled(logLevel)) 46 | return; 47 | 48 | var profile = _options.LogLevelProfiles[logLevel]; 49 | var scopeValues = _scopeManager.GetValues(); 50 | 51 | var eventInfo = new LogEventContext( 52 | _categoryName, 53 | logLevel, 54 | eventId, 55 | state, 56 | exception, 57 | scopeValues, 58 | profile); 59 | 60 | if (!(_logEventFilter?.Filter(eventInfo)).GetValueOrDefault(true)) 61 | return; 62 | 63 | _rendererPipeline.Render(eventInfo); 64 | } 65 | 66 | 67 | /// 68 | [ExcludeFromCodeCoverage] 69 | public bool IsEnabled(LogLevel logLevel) 70 | { 71 | return logLevel != LogLevel.None && logLevel >= _minimumLevel; 72 | } 73 | 74 | /// 75 | public IDisposable BeginScope(TState state) where TState : notnull 76 | { 77 | return _scopeManager.BeginScope(state); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/Options/LogLevelProfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Logging; 4 | using Vertical.SpectreLogger.Formatting; 5 | 6 | namespace Vertical.SpectreLogger.Options 7 | { 8 | /// 9 | /// Defines options to be applied to a specific log level. 10 | /// 11 | public class LogLevelProfile 12 | { 13 | private ICustomFormatter? _formatter; 14 | private IFormatProvider? _formatProvider; 15 | 16 | internal LogLevelProfile(LogLevel logLevel) 17 | { 18 | LogLevel = logLevel; 19 | } 20 | 21 | /// 22 | /// Gets the log level. 23 | /// 24 | public LogLevel LogLevel { get; } 25 | 26 | /// 27 | /// Gets or sets whether to preserve markup found in message template format strings. 28 | /// 29 | public bool PreserveMarkupInFormatStrings { get; set; } 30 | 31 | /// 32 | /// Gets or sets the output template. 33 | /// 34 | public string? OutputTemplate { get; set; } 35 | 36 | /// 37 | /// Gets a dictionary of for value types. 38 | /// 39 | internal Dictionary TypeFormatters { get; } = new(); 40 | 41 | /// 42 | /// Gets a dictionary of markup to apply before a specific value is rendered. 43 | /// 44 | internal Dictionary ValueStyles { get; } = new(); 45 | 46 | /// 47 | /// Gets a dictionary of markup to apply before a value of a specific type is rendered. 48 | /// 49 | public Dictionary TypeStyles { get; } = new(); 50 | 51 | /// 52 | /// Gets the style to apply before rendering a log value when no value or type 53 | /// style is matched. 54 | /// 55 | public string? DefaultLogValueStyle { get; set; } 56 | 57 | /// 58 | /// Gets a dictionary of option objects for renderers. 59 | /// 60 | internal OptionsCollection ConfiguredOptions { get; } = new(); 61 | 62 | /// 63 | /// Gets the custom formatter. 64 | /// 65 | internal ICustomFormatter Formatter => _formatter ??= new MultiTypeFormatter(TypeFormatters); 66 | 67 | /// 68 | /// Gets the format provider. 69 | /// 70 | internal IFormatProvider FormatProvider => _formatProvider ??= new MultiTypeFormatProvider(Formatter); 71 | 72 | /// 73 | public override string ToString() => LogLevel.ToString(); 74 | } 75 | } -------------------------------------------------------------------------------- /src/Output/WriteBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace Vertical.SpectreLogger.Output 5 | { 6 | /// 7 | /// Represents the default implementation of a . 8 | /// 9 | internal class WriteBuffer : IWriteBuffer 10 | { 11 | private readonly IConsoleWriter _consoleWriter; 12 | private readonly StringBuilder _buffer = new(); 13 | private int _margin; 14 | private char _lastChar; 15 | 16 | /// 17 | /// Creates a new instance of this type. 18 | /// 19 | /// Underlying console to flush output to. 20 | /// is null. 21 | public WriteBuffer(IConsoleWriter consoleWriter) 22 | { 23 | _consoleWriter = consoleWriter ?? throw new ArgumentNullException(nameof(consoleWriter)); 24 | } 25 | 26 | /// 27 | public int Margin 28 | { 29 | get => _margin; 30 | set => _margin = Math.Max(0, value); 31 | } 32 | 33 | /// 34 | public int LinePosition { get; private set; } 35 | 36 | /// 37 | public void Write(char c, int count = 1) 38 | { 39 | if (count == 0) 40 | return; 41 | 42 | if (_lastChar == '\n' && Margin > 0) 43 | { 44 | // Set the margin 45 | _buffer.Append(' ', Margin); 46 | LinePosition = Margin; 47 | } 48 | 49 | while (--count >= 0) 50 | { 51 | _buffer.Append(c); 52 | } 53 | 54 | _lastChar = c; 55 | 56 | LinePosition = c == '\n' ? 0 : LinePosition + 1; 57 | } 58 | 59 | 60 | /// 61 | public void Write(string str) => Write(str, 0, str.Length); 62 | 63 | /// 64 | public void Write(string str, int startIndex, int length) 65 | { 66 | var pastLastIndex = startIndex + length; 67 | 68 | for (var c = startIndex; c < pastLastIndex; c++) 69 | { 70 | Write(str[c]); 71 | } 72 | } 73 | 74 | /// 75 | public int Length => _buffer.Length; 76 | 77 | /// 78 | public void Flush() 79 | { 80 | _consoleWriter.Write(_buffer.ToString()); 81 | _consoleWriter.ResetLine(); 82 | _buffer.Clear(); 83 | _margin = 0; 84 | } 85 | 86 | /// 87 | public override string ToString() => _buffer.ToString(); 88 | } 89 | } -------------------------------------------------------------------------------- /src/Vertical.SpectreLogger.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net8.0;net9.0 5 | 9.0 6 | enable 7 | vertical-spectrelogger 8 | 9 | Seriously configurable ILogger/ILoggerProvider implementation for Spectre Console. Don't 10 | change how your app logs - change how the logs are presented. 11 | 12 | Vertical Software Contributors 13 | 0.10.1 14 | vertical-spectreconsolelogger 15 | infrastructure;utilities;logging 16 | https://github.com/verticalsoftware/vertical-spectreconsolelogger 17 | MIT 18 | git 19 | https://github.com/verticalsoftware/vertical-spectreconsolelogger 20 | true 21 | true 22 | false 23 | icon.png 24 | true 25 | snupkg 26 | false 27 | ..\assets\vertical-software.snk 28 | 29 | README.md 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/output-template.md: -------------------------------------------------------------------------------- 1 | # The Output Template 2 | 3 | ## Overview 4 | 5 | The output template controls the content that is displayed for each log event. The output template is a property of a `LogLevelProfile`, so you can customize the output of events specifically for each level. 6 | 7 | The output template is a string that contains Spectre Console markup, static text, and (most importantly) handlebar style placeholders that map to specific rendering components. Displayed below is an example output template: 8 | 9 | ``` 10 | [grey85][[{DateTime:T} [green3_1]Info[/]]] {Message}{NewLine}{Exception}[/] 11 | ``` 12 | This template renders output events as follows: 13 | 1. It sets the [grey85](https://spectreconsole.net/appendix/colors) color using Spectre Console markup. 14 | 2. Prints an open bracket. 15 | 3. Displays the current timestamp, formatting it using the `T` code (see [DateTime formatting](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings)) 16 | 4. Sets the [green3_1](https://spectreconsole.net/appendix/colors) color to display "Info", then closes the markup tag. 17 | 5. Prints a close bracket. 18 | 6. Displays the event message, performing structured log value substitutions. 19 | 7. Begins a new line. 20 | 8. Displays the exception if available in the log event. 21 | 9. Closes the opening markup tag. 22 | 23 | The placeholders in the template string map to specific rendering components in the logging provider. Out-of-box, the following renderers are available: 24 | 25 | |Placeholder|Description| 26 | |---|---| 27 | |`{Category}`|Prints the logger category| 28 | |`{DateTime}`|Prints the current date/timestamp| 29 | |`{LogLevel}`|Displays the log level (useful for a single output template)| 30 | |`{Margin}`|Sets the left margin for newline characters. All output is aligned to this margin for the remained of the event (unless changed again)| 31 | |`{Message}`|Display the log message along with structured log value substitutions| 32 | |`{NewLine}`|Prints a newline character for a multi-line log level event| 33 | |`{ProcessId}`|Prints the current process id| 34 | |`{Scope=}`|Prints the value of a single scope value| 35 | |`{Scopes}`|Prints all scope values| 36 | |`{ThreadId}`|Prints the current thread id| 37 | 38 | 39 | After configuration but before logging startup, the provider will build an efficient rendering pipeline for each log level. The specific renderers are discussed in their own documentation. 40 | 41 | Configure the output template for profiles by setting the `OutputTemplate` property. 42 | 43 | ```csharp 44 | config.ConfigureProfile(LogLevel.Information, profile => 45 | { 46 | profile.OutputTemplate = "[grey85][[{DateTime:T} [green3_1]Info[/]]] {Message}{NewLine+}{Exception}[/]")); 47 | }); 48 | ``` 49 | 50 | ## See Also 51 | 52 | - [(Next) Formatting Log Values](./formatting.md) 53 | - [Styling Log Values](./styling.md) -------------------------------------------------------------------------------- /src/Rendering/ExceptionRenderer.Formatting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Vertical.SpectreLogger.Formatting; 3 | 4 | namespace Vertical.SpectreLogger.Rendering 5 | { 6 | public partial class ExceptionRenderer 7 | { 8 | /// 9 | /// Wraps the exception name emitted value. 10 | /// 11 | public sealed class ExceptionNameValue : ValueWrapper 12 | { 13 | internal ExceptionNameValue(Type exceptionType): base(exceptionType) {} 14 | } 15 | 16 | /// 17 | /// Wraps the exception message emitted value. 18 | /// 19 | public sealed class ExceptionMessageValue : ValueWrapper 20 | { 21 | internal ExceptionMessageValue(string message) : base(message) {} 22 | } 23 | 24 | /// 25 | /// Wraps the method name of a stack frame. 26 | /// 27 | public sealed class MethodNameValue : ValueWrapper 28 | { 29 | internal MethodNameValue(string name) : base(name) {} 30 | } 31 | 32 | /// 33 | /// Wraps the parameter type name of a stack frame method. 34 | /// 35 | public sealed class ParameterTypeValue : ValueWrapper 36 | { 37 | internal ParameterTypeValue(string type) : base(type) {} 38 | } 39 | 40 | /// 41 | /// Wraps the parameter name of a stack frame method. 42 | /// 43 | public sealed class ParameterNameValue : ValueWrapper 44 | { 45 | internal ParameterNameValue(string name) : base(name) {} 46 | } 47 | 48 | /// 49 | /// Wraps the directory name of a stack frame method. 50 | /// 51 | public sealed class SourceDirectoryValue : ValueWrapper 52 | { 53 | internal SourceDirectoryValue(string path) : base(path) {} 54 | } 55 | 56 | /// 57 | /// Wraps the file name of a stack frame method. 58 | /// 59 | public sealed class SourceFileValue : ValueWrapper 60 | { 61 | internal SourceFileValue(string file) : base(file) {} 62 | } 63 | 64 | /// 65 | /// Wraps the source line number of a stack frame method. 66 | /// 67 | public sealed class SourceLocationValue : ValueWrapper 68 | { 69 | internal SourceLocationValue(int lineNumber) : base(lineNumber) {} 70 | } 71 | 72 | /// 73 | /// Wraps any other text in an exception message. 74 | /// 75 | public sealed class TextValue : ValueWrapper 76 | { 77 | internal TextValue(string str) : base(str) {} 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /test/Output/ActualConsoleWriterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | using Xunit; 6 | 7 | namespace Vertical.SpectreLogger.Tests.Output 8 | { 9 | public class ActualConsoleWriterTests 10 | { 11 | [Fact] 12 | public async Task WriteInBackground() 13 | { 14 | var services = new ServiceCollection() 15 | .AddLogging(builder => builder.AddSpectreConsole( 16 | config => 17 | { 18 | config.WriteInBackground(); 19 | })) 20 | .BuildServiceProvider(); 21 | 22 | var logger = services.GetRequiredService>(); 23 | 24 | logger.LogInformation("Test event successful"); 25 | 26 | await services.DisposeAsync(); 27 | } 28 | 29 | [Fact] 30 | public async Task WriteInForeground() 31 | { 32 | var services = new ServiceCollection() 33 | .AddLogging(builder => builder.AddSpectreConsole( 34 | config => 35 | { 36 | config.WriteInForeground(); 37 | })) 38 | .BuildServiceProvider(); 39 | 40 | var logger = services.GetRequiredService>(); 41 | 42 | logger.LogInformation("Test event successful"); 43 | 44 | await services.DisposeAsync(); 45 | } 46 | 47 | [Fact] 48 | public async Task WriteErrorInBackground() 49 | { 50 | var services = new ServiceCollection() 51 | .AddLogging(builder => builder.AddSpectreConsole( 52 | config => 53 | { 54 | config.WriteInBackground(); 55 | })) 56 | .BuildServiceProvider(); 57 | 58 | var logger = services.GetRequiredService>(); 59 | 60 | logger.LogError(new ArgumentException(), "Oops"); 61 | 62 | await services.DisposeAsync(); 63 | } 64 | 65 | [Fact] 66 | public async Task WriteErrorInForeground() 67 | { 68 | var services = new ServiceCollection() 69 | .AddLogging(builder => builder.AddSpectreConsole( 70 | config => 71 | { 72 | config.WriteInForeground(); 73 | })) 74 | .BuildServiceProvider(); 75 | 76 | var logger = services.GetRequiredService>(); 77 | 78 | logger.LogError(new ArgumentException(), "Oops"); 79 | 80 | await services.DisposeAsync(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vertical-spectreconsolelogger 2 | 3 | A seriously customizable [Spectre Console](https://spectreconsole.net/) provider for Microsoft.Extensions.Logging. **Don't** change how your app logs - change how the logs are presented. 4 | 5 | ![.net](https://img.shields.io/badge/Frameworks-.netstandard2.0+net6/7-purple) 6 | ![GitHub](https://img.shields.io/github/license/verticalsoftware/vertical-commandline) 7 | ![Package info](https://img.shields.io/nuget/v/vertical-spectreconsolelogger.svg) 8 | 9 | [![Dev build](https://github.com/verticalsoftware/vertical-commandline/actions/workflows/dev-build.yml/badge.svg)](https://github.com/verticalsoftware/vertical-commandline/actions/workflows/dev-build.yml) 10 | [![codecov](https://codecov.io/gh/verticalsoftware/vertical-spectreconsolelogger/branch/dev/graph/badge.svg?token=MVW0CUWLCW)](https://codecov.io/gh/verticalsoftware/vertical-spectreconsolelogger) 11 | [![Pre release](https://github.com/verticalsoftware/vertical-spectreconsolelogger/actions/workflows/pre-release.yml/badge.svg)](https://github.com/verticalsoftware/vertical-spectreconsolelogger/actions/workflows/pre-release.yml) 12 | [![Release](https://github.com/verticalsoftware/vertical-spectreconsolelogger/actions/workflows/release.yml/badge.svg)](https://github.com/verticalsoftware/vertical-spectreconsolelogger/actions/workflows/release.yml) 13 | 14 | ## Quick Start 15 | 16 | Add a package reference to your `.csproj` file: 17 | 18 | ``` 19 | $ dotnet add package vertical-spectreconsolelogger --prerelease 20 | ``` 21 | 22 | Call `AddSpectreConsole` in your logging setup: 23 | 24 | ```csharp 25 | var loggerFactory = LoggerFactory.Create(builder => builder 26 | .AddSpectreConsole()); 27 | 28 | var logger = loggerFactory.CreateLogger("MyLogger"); 29 | 30 | logger.LogInformation("Hello world!"); 31 | ``` 32 | 33 | ## Features at a glance 34 | 35 | 1. Decouples styling and formatting from logging (e.g. don't change your logging, customize how the events are displayed). 36 | 2. Define different customizations for _each_ log level. 37 | 3. Customize the styling and formatting of specific values or specific types of values. 38 | 4. Destructure and output complex types in JSON(ish) notation. 39 | 5. Customize the rendering completely using output templates. 40 | 6. Extend the logger with your own renderers. 41 | 42 | #### Format/style log values 43 | 44 | ![cap-1](https://raw.githubusercontent.com/verticalsoftware/vertical-spectreconsolelogger/dev/assets/cap1.png) 45 | 46 | #### Destructured output follows configuration 47 | 48 | ![cap-2](https://raw.githubusercontent.com/verticalsoftware/vertical-spectreconsolelogger/dev/assets/cap2.png) 49 | 50 | #### Precisely control exception output 51 | 52 | ![cap-3](https://raw.githubusercontent.com/verticalsoftware/vertical-spectreconsolelogger/dev/assets/cap3.png) 53 | 54 | ## Documentation 55 | 56 | Read the full [docs](https://github.com/verticalsoftware/vertical-spectreconsolelogger/blob/dev/docs/docs-home.md) here. 57 | -------------------------------------------------------------------------------- /src/Rendering/CategoryNameRenderer.Formatting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using Vertical.SpectreLogger.Formatting; 4 | 5 | namespace Vertical.SpectreLogger.Rendering 6 | { 7 | public partial class CategoryNameRenderer 8 | { 9 | /// 10 | /// Wrapping type for the category name. 11 | /// 12 | public class Value : ValueWrapper 13 | { 14 | internal Value(string value) : base(value) 15 | { 16 | } 17 | } 18 | 19 | /// 20 | /// Custom formatter for category name. 21 | /// 22 | [TypeFormatter(typeof(Value))] 23 | public class DefaultFormatter : ICustomFormatter 24 | { 25 | /// 26 | public virtual string Format(string? format, object? arg, IFormatProvider? formatProvider) 27 | { 28 | if (arg == null) 29 | { 30 | return string.Empty; 31 | } 32 | 33 | var categoryName = ((Value) arg).Value; 34 | 35 | if (format == null) 36 | { 37 | return categoryName; 38 | } 39 | 40 | var formatPattern = Regex.Match(format, @"([CS])(\d+)?"); 41 | 42 | var countParam = formatPattern.Groups[2].Success 43 | ? int.Parse(formatPattern.Groups[2].Value) 44 | : (int?) null; 45 | 46 | if (!formatPattern.Success) 47 | { 48 | return categoryName; 49 | } 50 | 51 | switch (formatPattern.Groups[1].Value) 52 | { 53 | case "C" when !countParam.HasValue: 54 | // Compact formatting - last component only 55 | var dot = categoryName.LastIndexOf('.'); 56 | return dot > -1 && dot < categoryName.Length ? categoryName.Substring(dot+1) : categoryName; 57 | 58 | case "S" when countParam.HasValue: 59 | // Compact formatting with max parts 60 | var index = categoryName.Length; 61 | while (countParam!.Value > 0 && index >= 0) 62 | { 63 | index = categoryName.LastIndexOf('.', index-1); 64 | 65 | if (index != -1) 66 | { 67 | countParam--; 68 | } 69 | } 70 | 71 | if (index > -1) index++; 72 | 73 | return categoryName.Substring(Math.Max(index, 0)); 74 | } 75 | 76 | return categoryName; 77 | } 78 | } 79 | } 80 | } --------------------------------------------------------------------------------