From 39d519d08eb66f26c959619568bf415559a77188 Mon Sep 17 00:00:00 2001 From: "Louis S. Berman" Date: Wed, 11 Jun 2025 15:17:10 -0400 Subject: [PATCH 1/3] Added SampleErrorsLogging project --- Ardalis.Result.sln | 7 ++ Directory.Packages.props | 66 ++++++++++--------- .../Ardalis.Result.SampleErrorsLogging.csproj | 22 +++++++ .../Logging/BasicContext.cs | 6 ++ .../Logging/EventIds.cs | 6 ++ .../Logging/LogResultErrorsExtender.cs | 36 ++++++++++ .../Logging/LoggerBuilder.cs | 50 ++++++++++++++ .../Logging/LoggingException.cs | 6 ++ .../Logging/LoggingHelpers.cs | 15 +++++ .../Logging/SerilogExtenders.cs | 20 ++++++ .../Logging/SerilogHelper.cs | 48 ++++++++++++++ .../Program.cs | 22 +++++++ .../appsettings.json | 14 ++++ 13 files changed, 288 insertions(+), 30 deletions(-) create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Ardalis.Result.SampleErrorsLogging.csproj create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Logging/BasicContext.cs create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Logging/EventIds.cs create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Logging/LogResultErrorsExtender.cs create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggerBuilder.cs create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggingException.cs create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggingHelpers.cs create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Logging/SerilogExtenders.cs create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Logging/SerilogHelper.cs create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/Program.cs create mode 100644 sample/Ardalis.Result.SampleErrorsLogging/appsettings.json diff --git a/Ardalis.Result.sln b/Ardalis.Result.sln index 3f92f61..9e42a1e 100644 --- a/Ardalis.Result.sln +++ b/Ardalis.Result.sln @@ -49,6 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ardalis.Result.FluentAssert EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ardalis.Result.FluentAssertions.UnitTests", "tests\Ardalis.Result.FluentAssertions.UnitTests\Ardalis.Result.FluentAssertions.UnitTests.csproj", "{0800019A-0B40-4923-96E1-656A745E79C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ardalis.Result.SampleErrorsLogging", "sample\Ardalis.Result.SampleErrorsLogging\Ardalis.Result.SampleErrorsLogging.csproj", "{15C8FBAA-02CC-65F4-28FA-3E8D73211E8D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -107,6 +109,10 @@ Global {0800019A-0B40-4923-96E1-656A745E79C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {0800019A-0B40-4923-96E1-656A745E79C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {0800019A-0B40-4923-96E1-656A745E79C2}.Release|Any CPU.Build.0 = Release|Any CPU + {15C8FBAA-02CC-65F4-28FA-3E8D73211E8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15C8FBAA-02CC-65F4-28FA-3E8D73211E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15C8FBAA-02CC-65F4-28FA-3E8D73211E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15C8FBAA-02CC-65F4-28FA-3E8D73211E8D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -125,6 +131,7 @@ Global {C522FE42-BBEF-46A7-9E3E-44EA8339AF2E} = {42693FB1-04E1-4635-B249-E1847609E801} {94145FF9-5A56-4479-87AF-3A99939B38F0} = {865A74CD-F478-4AA5-AFA5-6C26FB38B849} {0800019A-0B40-4923-96E1-656A745E79C2} = {42693FB1-04E1-4635-B249-E1847609E801} + {15C8FBAA-02CC-65F4-28FA-3E8D73211E8D} = {FA061DC6-61B7-401F-87A4-D338B396CCD9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7CD0ED8C-3B1C-4F16-8B8D-3D8F1A8F1A5A} diff --git a/Directory.Packages.props b/Directory.Packages.props index 2f46644..4913c86 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,32 +1,38 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Ardalis.Result.SampleErrorsLogging.csproj b/sample/Ardalis.Result.SampleErrorsLogging/Ardalis.Result.SampleErrorsLogging.csproj new file mode 100644 index 0000000..1c22fae --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Ardalis.Result.SampleErrorsLogging.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Logging/BasicContext.cs b/sample/Ardalis.Result.SampleErrorsLogging/Logging/BasicContext.cs new file mode 100644 index 0000000..b8b6c77 --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Logging/BasicContext.cs @@ -0,0 +1,6 @@ +namespace Ardalis.Result.ErrorsLoggingDemo; + +public record BasicContext( + string CalledBy, + string CorrelationId, + Dictionary ExtraInfo); \ No newline at end of file diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Logging/EventIds.cs b/sample/Ardalis.Result.SampleErrorsLogging/Logging/EventIds.cs new file mode 100644 index 0000000..78ee064 --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Logging/EventIds.cs @@ -0,0 +1,6 @@ +namespace Ardalis.Result.ErrorsLoggingDemo; + +internal static class EventIds +{ + public const int ResultErrors = 1000; +} \ No newline at end of file diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Logging/LogResultErrorsExtender.cs b/sample/Ardalis.Result.SampleErrorsLogging/Logging/LogResultErrorsExtender.cs new file mode 100644 index 0000000..38fd394 --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Logging/LogResultErrorsExtender.cs @@ -0,0 +1,36 @@ +using System.Runtime.CompilerServices; + +namespace Ardalis.Result.ErrorsLoggingDemo; + +public record ResultErrorsDetails( + string[] Errors); + +public static partial class ILoggerExtenders +{ + public static void LogResultErrors( + this ILogger logger, + Result result, + Dictionary extraInfo = null!, + Guid correlationId = default, + [CallerMemberName] string calledBy = "") + { + if (!result.IsError()) + return; + + logger.ResultErrors( + new ResultErrorsDetails([.. result.Errors]), + new BasicContext(calledBy, result + .GetCorrelationId(correlationId), extraInfo)); + } + + [LoggerMessage( + EventId = EventIds.ResultErrors, + EventName = nameof(ResultErrors), + Level = LogLevel.Information, + SkipEnabledCheck = true, + Message = $"{nameof(ResultErrors)}={{@Details}};Context={{@Context}}")] + private static partial void ResultErrors( + this ILogger logger, + ResultErrorsDetails details, + BasicContext context); +} \ No newline at end of file diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggerBuilder.cs b/sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggerBuilder.cs new file mode 100644 index 0000000..ffddeef --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggerBuilder.cs @@ -0,0 +1,50 @@ +using Serilog; +using Serilog.Sinks.OpenTelemetry; +using MEL = Microsoft.Extensions.Logging; + +namespace Ardalis.Result.ErrorsLoggingDemo; + +public class LoggerBuilder +{ + // Builds and then returns an initialized + // Microsoft.Extension.Logging.ILogger + public static MEL.ILogger Build(string[] args) + { + var config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", false,false) + .Build(); + + var seqApiUri = new Uri(config["Serilog:SeqApiUri"]!); + + var seqApiKey = config["Serilog:SeqApiKey"]!; + + var logLevel = (LogLevel)Enum.Parse(typeof(LogLevel), + config["Serilog:LogLevel"]! ?? "Debug"); + + SerilogHelper.InitLogDotLogger(logLevel, configure => + { + // One of these need to be specified for each + // logged "details" type + configure.OmitTypeField(); + + configure.WriteTo.OpenTelemetry(x => + { + x.Endpoint = seqApiUri.AbsoluteUri; + x.Protocol = OtlpProtocol.HttpProtobuf; + x.Headers = new Dictionary + { + ["X-Seq-ApiKey"] = seqApiKey + }; + x.ResourceAttributes = new Dictionary + { + ["service.name"] = "ResultErrorsLoggingDemo" + }; + }); + }); + + using var loggerFactory = LoggerFactory.Create( + builder => { builder.AddSerilog(); }); + + return loggerFactory.CreateLogger(); + } +} \ No newline at end of file diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggingException.cs b/sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggingException.cs new file mode 100644 index 0000000..835de8b --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggingException.cs @@ -0,0 +1,6 @@ +namespace Ardalis.Result.ErrorsLoggingDemo; + +public class LoggingException(string message) + : Exception(message) +{ +} \ No newline at end of file diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggingHelpers.cs b/sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggingHelpers.cs new file mode 100644 index 0000000..c44682d --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Logging/LoggingHelpers.cs @@ -0,0 +1,15 @@ +namespace Ardalis.Result.ErrorsLoggingDemo; + +public static class LoggingHelpers +{ + public static string GetCorrelationId( + this Result result, Guid correlationId) + { + if (correlationId != default) + return correlationId.ToString("N"); + else if (string.IsNullOrEmpty(result.CorrelationId)) + return Guid.NewGuid().ToString("N"); + else + return result.CorrelationId; + } +} diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Logging/SerilogExtenders.cs b/sample/Ardalis.Result.SampleErrorsLogging/Logging/SerilogExtenders.cs new file mode 100644 index 0000000..2b34ac7 --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Logging/SerilogExtenders.cs @@ -0,0 +1,20 @@ +using Serilog; + +namespace Ardalis.Result.ErrorsLoggingDemo; + +public static class SerilogExtenders +{ + public static LoggerConfiguration OmitTypeField( + this LoggerConfiguration loggerConfiguration) + { + return loggerConfiguration + .Destructure.ByTransforming(instance => + { + var properties = instance!.GetType() + .GetProperties().Where(p => p.Name != "$type") + .ToDictionary(p => p.Name, p => p.GetValue(instance)); + + return properties; + }); + } +} \ No newline at end of file diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Logging/SerilogHelper.cs b/sample/Ardalis.Result.SampleErrorsLogging/Logging/SerilogHelper.cs new file mode 100644 index 0000000..1d6ca46 --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Logging/SerilogHelper.cs @@ -0,0 +1,48 @@ +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; +using System.Diagnostics; + +namespace Ardalis.Result.ErrorsLoggingDemo; + +public static class SerilogHelper +{ + public static void InitLogDotLogger( + LogLevel minLogLevel = LogLevel.Information, + Action configure = null!) + { + Serilog.Debugging.SelfLog.Enable( + output => Debug.WriteLine(output)); + + Serilog.Debugging.SelfLog.Enable(Console.Error); + + var config = new LoggerConfiguration() + .MinimumLevel.Is(minLogLevel.ToLogEventLevel()) + .OmitTypeField() + .Destructure.ByTransforming(v => v.ToString("MM/dd/yyyy HH:mm:ss.fff")) + .Destructure.ByTransforming(v => v.ToString()) + .Destructure.ByTransforming(v => v.ToString("D")) + .Destructure.ByTransforming(v => v.ToString(@"d\.hh\:mm\:ss\.fff")); + + config.Enrich.FromLogContext(); + + config.WriteTo.Console(theme: AnsiConsoleTheme.Code, outputTemplate: + "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}"); + + configure?.Invoke(config); + + Log.Logger = config.CreateLogger(); + } + + public static LogEventLevel ToLogEventLevel(this LogLevel logLevel) + { + return logLevel switch + { + LogLevel.Debug => LogEventLevel.Debug, + LogLevel.Information => LogEventLevel.Information, + LogLevel.Warning => LogEventLevel.Warning, + LogLevel.Error => LogEventLevel.Error, + _ => throw new LoggingException("A valid \"logLevel\" must be supplied.") + }; + } +} \ No newline at end of file diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Program.cs b/sample/Ardalis.Result.SampleErrorsLogging/Program.cs new file mode 100644 index 0000000..49220cb --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/Program.cs @@ -0,0 +1,22 @@ +using Ardalis.Result; +using Ardalis.Result.ErrorsLoggingDemo; +using Serilog; + +// Build a configured logger +var logger = LoggerBuilder.Build(args); + +// Log Result Errors, with optional ExtraInfo and an optional +// CorrelationId override, If a CorrelationId is not provided, +// it will be taken from Result.CorrelationId (if it exists) +// or created using Guid.NewGuid(). +logger.LogResultErrors( + Result.Error(new ErrorList(["Oops!", "Whoopsie!"])), + new Dictionary + { + { "Code", "ABC123" }, + { "Number", "987654321" } + }, + Guid.NewGuid()); + +// Always close and flush before terminating your app +Log.CloseAndFlush(); diff --git a/sample/Ardalis.Result.SampleErrorsLogging/appsettings.json b/sample/Ardalis.Result.SampleErrorsLogging/appsettings.json new file mode 100644 index 0000000..538bf16 --- /dev/null +++ b/sample/Ardalis.Result.SampleErrorsLogging/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Serilog": { + "SeqApiUri": "http://localhost:5341/ingest/otlp/v1/logs", + "SeqApiKey": "", + "LogLevel": "Debug" + } +} From 71c4f7079bc3e98679d117e480ccc89e5398d0cd Mon Sep 17 00:00:00 2001 From: "Louis S. Berman" Date: Wed, 11 Jun 2025 15:52:06 -0400 Subject: [PATCH 2/3] Smal tweak to the demo code --- sample/Ardalis.Result.SampleErrorsLogging/Program.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Program.cs b/sample/Ardalis.Result.SampleErrorsLogging/Program.cs index 49220cb..afeec68 100644 --- a/sample/Ardalis.Result.SampleErrorsLogging/Program.cs +++ b/sample/Ardalis.Result.SampleErrorsLogging/Program.cs @@ -5,6 +5,9 @@ // Build a configured logger var logger = LoggerBuilder.Build(args); +// A custom CorrelationId +var correlationId = Guid.NewGuid(); + // Log Result Errors, with optional ExtraInfo and an optional // CorrelationId override, If a CorrelationId is not provided, // it will be taken from Result.CorrelationId (if it exists) @@ -16,7 +19,7 @@ { "Code", "ABC123" }, { "Number", "987654321" } }, - Guid.NewGuid()); + correlationId); // Always close and flush before terminating your app Log.CloseAndFlush(); From 03e7cb8af4883d334036ad5da20de77a3b0ddc2a Mon Sep 17 00:00:00 2001 From: "Louis S. Berman" Date: Thu, 12 Jun 2025 10:35:11 -0400 Subject: [PATCH 3/3] Minor tweak to Ardalis.Result.SampleErrorsLogging --- sample/Ardalis.Result.SampleErrorsLogging/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/Ardalis.Result.SampleErrorsLogging/Program.cs b/sample/Ardalis.Result.SampleErrorsLogging/Program.cs index afeec68..3ad48ca 100644 --- a/sample/Ardalis.Result.SampleErrorsLogging/Program.cs +++ b/sample/Ardalis.Result.SampleErrorsLogging/Program.cs @@ -8,7 +8,7 @@ // A custom CorrelationId var correlationId = Guid.NewGuid(); -// Log Result Errors, with optional ExtraInfo and an optional +// Logs Result Errors, with optional ExtraInfo and an optional // CorrelationId override, If a CorrelationId is not provided, // it will be taken from Result.CorrelationId (if it exists) // or created using Guid.NewGuid().