diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 77d5b1c..874e68c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,13 +9,6 @@ ], "rollForward": false }, - "dotnet-format": { - "version": "5.1.250801", - "commands": [ - "dotnet-format" - ], - "rollForward": false - }, "csharpier": { "version": "0.29.0", "commands": [ diff --git a/.editorconfig b/.editorconfig index faea99e..f92bd13 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,7 +18,8 @@ root = true # Global settings [*] -end_of_line = crlf +charset = utf-8 +end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes index 1ff0c42..e49d078 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto +*.cs text eol=lf ############################################################################### # Set default behavior for command prompt diff. diff --git a/.husky/pre-commit b/.husky/pre-commit index ca0140d..51949be 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,7 @@ -npm run format +npm run fix-analyzers +npm run fix-style +npm run fix-csharpier-format +npm run format npm run style - npm run analyzers diff --git a/Vertical.Slice.Template.sln b/Vertical.Slice.Template.sln index 5e9f198..1a58d87 100644 --- a/Vertical.Slice.Template.sln +++ b/Vertical.Slice.Template.sln @@ -96,6 +96,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vertical.Slice.Template.Api EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "src\Shared\Shared.csproj", "{A5FB3B9E-FF0A-45F4-8D37-080C770A5AB4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".husky", ".husky", "{727B615D-D77D-4FEF-ADC6-66CA01B922C1}" + ProjectSection(SolutionItems) = preProject + .husky\commit-msg = .husky\commit-msg + .husky\pre-commit = .husky\pre-commit + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -126,6 +132,7 @@ Global {AC478225-5EA9-4895-875A-0B01217C4576} = {AF80152C-AF0D-475E-AD69-A7867D1ACA26} {23F3C3DD-6630-4743-BE50-9BD6F719665E} = {AF80152C-AF0D-475E-AD69-A7867D1ACA26} {A5FB3B9E-FF0A-45F4-8D37-080C770A5AB4} = {54C91A04-E128-4ADB-9B49-E0FEAC783069} + {727B615D-D77D-4FEF-ADC6-66CA01B922C1} = {798579C1-7DEC-47A2-9C18-CA3DBE4A6573} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {5F41BF51-E13B-48BF-8D81-874FBA9BC961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/package.json b/package.json index 7699a52..02021f4 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,12 @@ "prepare": "husky && dotnet tool restore", "install-dev-cert-bash": "curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v vs2019 -l ~/vsdbg", "upgrade-packages": "dotnet outdated Vertical.Slice.Template.sln", - "apply-formatting": "dotnet csharpier . && git add -A .", "format": "dotnet csharpier . --check", "style": "dotnet format style Vertical.Slice.Template.sln --verify-no-changes --severity error --verbosity diagnostic", - "analyzers": "dotnet format analyzers Vertical.Slice.Template.sln --verify-no-changes --severity error --verbosity diagnostic" + "analyzers": "dotnet format analyzers Vertical.Slice.Template.sln --verify-no-changes --severity error --verbosity diagnostic", + "fix-csharpier-format": "dotnet csharpier . && git add -A .", + "fix-style": "dotnet format style Vertical.Slice.Template.sln --severity error --verbosity diagnostic && git add -A .", + "fix-analyzers": "dotnet format analyzers Vertical.Slice.Template.sln --severity error --verbosity diagnostic && git add -A ." }, "repository": { "type": "git", @@ -27,4 +29,3 @@ "husky": "^9.1.6" } } - diff --git a/src/App/Vertical.Slice.Template.Api/Program.cs b/src/App/Vertical.Slice.Template.Api/Program.cs index aae3fae..75fa058 100644 --- a/src/App/Vertical.Slice.Template.Api/Program.cs +++ b/src/App/Vertical.Slice.Template.Api/Program.cs @@ -1,6 +1,6 @@ using Serilog; using Serilog.Events; -using Shared.Logging; +using Shared.Logging.Extensions; using Shared.Swagger; using Shared.Web.Extensions; using Shared.Web.Minimal.Extensions; diff --git a/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProductEndpoint.cs b/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProductEndpoint.cs index 4ca6833..76117d5 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProductEndpoint.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProductEndpoint.cs @@ -20,15 +20,16 @@ internal static RouteHandlerBuilder MapCreateProductEndpoint(this IEndpointRoute // StatusCodes.Status201Created, // getId: response => response.Id // ); + return app.MapPost("/", Handle) .WithName(nameof(CreateProduct)) .WithDisplayName(nameof(CreateProduct).Humanize()) .WithSummaryAndDescription(nameof(CreateProduct).Humanize(), nameof(CreateProduct).Humanize()) .WithTags(ProductConfigurations.Tag) - // .Produces("Product created successfully.", StatusCodes.Status201Created) - // .ProducesValidationProblem("Invalid input for creating product.", StatusCodes.Status400BadRequest) - // .ProducesProblem("UnAuthorized request.", StatusCodes.Status401Unauthorized) .MapToApiVersion(1.0); + // .Produces("Product created successfully.", StatusCodes.Status201Created) + // .ProducesValidationProblem("Invalid input for creating product.", StatusCodes.Status400BadRequest) + // .ProducesProblem("UnAuthorized request.", StatusCodes.Status401Unauthorized) async Task< Results, UnAuthorizedHttpProblemResult, ValidationProblem> diff --git a/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductByIdEndpoint.cs b/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductByIdEndpoint.cs index 21e5d26..48ff58b 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductByIdEndpoint.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductByIdEndpoint.cs @@ -23,10 +23,10 @@ internal static RouteHandlerBuilder MapGetProductByIdEndpoint(this IEndpointRout .WithDisplayName(nameof(GetProductById).Humanize()) .WithSummaryAndDescription(nameof(GetProductById).Humanize(), nameof(GetProductById).Humanize()) .WithTags(ProductConfigurations.Tag) - // .Produces("Product fetched successfully.", StatusCodes.Status200OK) - // .ProducesValidationProblem("Invalid input for getting product.", StatusCodes.Status400BadRequest) - // .ProducesProblem("Product not found", StatusCodes.Status404NotFound) .MapToApiVersion(1.0); + // .Produces("Product fetched successfully.", StatusCodes.Status200OK) + // .ProducesValidationProblem("Invalid input for getting product.", StatusCodes.Status400BadRequest) + // .ProducesProblem("Product not found", StatusCodes.Status404NotFound) async Task, ValidationProblem, NotFoundHttpProblemResult>> Handle( [AsParameters] GetProductByIdRequestParameters requestParameters diff --git a/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPageEndpoint.cs b/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPageEndpoint.cs index 379222e..9febcb5 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPageEndpoint.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPageEndpoint.cs @@ -22,9 +22,9 @@ internal static RouteHandlerBuilder MapGetProductsByPageEndpoint(this IEndpointR .WithDisplayName(nameof(GetProductsByPage).Humanize()) .WithSummaryAndDescription(nameof(GetProductsByPage).Humanize(), nameof(GetProductsByPage).Humanize()) .WithTags(ProductConfigurations.Tag) - // .Produces("Products fetched successfully.", StatusCodes.Status200OK) - // .ProducesValidationProblem("Invalid input for getting product.", StatusCodes.Status400BadRequest) .MapToApiVersion(1.0); + // .Produces("Products fetched successfully.", StatusCodes.Status200OK) + // .ProducesValidationProblem("Invalid input for getting product.", StatusCodes.Status400BadRequest) async Task, ValidationProblem>> Handle( [AsParameters] GetProductsByPageRequestParameters requestParameters diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 424ee75..04c0a78 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -69,9 +69,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - runtime; build; native; contentfiles; analyzers; buildtransitive - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 5e9ec2a..d190313 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -93,13 +93,12 @@ - - + - + @@ -124,7 +123,6 @@ - diff --git a/src/Shared/EF/Extensions/ServiceCollectionExtensions.cs b/src/Shared/EF/Extensions/ServiceCollectionExtensions.cs index 8eb6681..d77469e 100644 --- a/src/Shared/EF/Extensions/ServiceCollectionExtensions.cs +++ b/src/Shared/EF/Extensions/ServiceCollectionExtensions.cs @@ -49,7 +49,6 @@ params Assembly[] assembliesToScan sqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), null); } ) - // https://github.com/efcore/EFCore.NamingConventions .UseSnakeCaseNamingConvention(); // ref: https://andrewlock.net/series/using-strongly-typed-entity-ids-to-avoid-primitive-obsession/ diff --git a/src/Shared/Logging/Enrichers/BaggageEnricher.cs b/src/Shared/Logging/Enrichers/BaggageEnricher.cs new file mode 100644 index 0000000..b50a954 --- /dev/null +++ b/src/Shared/Logging/Enrichers/BaggageEnricher.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; +using Serilog.Core; +using Serilog.Events; + +namespace Shared.Logging.Enrichers; + +public class BaggageEnricher : ILogEventEnricher +{ + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (Activity.Current == null) + return; + + foreach (var (key, value) in Activity.Current.Baggage) + { + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(key, value)); + } + } +} diff --git a/src/Shared/Logging/Enrichers/LogEnricher.cs b/src/Shared/Logging/Enrichers/LogEnricher.cs new file mode 100644 index 0000000..2d6bf53 --- /dev/null +++ b/src/Shared/Logging/Enrichers/LogEnricher.cs @@ -0,0 +1,73 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Serilog; +using Serilog.Events; + +namespace Shared.Logging.Enrichers; + +// Ref: https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-logging-the-selected-endpoint-name-with-serilog/ +// https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-excluding-health-check-endpoints-from-serilog-request-logging/ +// https://github.com/serilog/serilog-aspnetcore/issues/163 +public static class LogEnricher +{ + /// + /// Enriches the HTTP request log with additional data via the Diagnostic Context. + /// + /// The Serilog diagnostic context. + /// The current HTTP Context. + public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext) + { + var request = httpContext.Request; + + // Set all the common properties available for every request + diagnosticContext.Set("Host", request.Host); + diagnosticContext.Set("Protocol", request.Protocol); + diagnosticContext.Set("Scheme", request.Scheme); + + // Only set it if available. You're not sending sensitive data in a querystring right?! + if (request.QueryString.HasValue) + { + diagnosticContext.Set("QueryString", request.QueryString.Value); + } + + // Set the content-type of the Response at this point + diagnosticContext.Set("ContentType", httpContext.Response.ContentType); + + // Set userId + diagnosticContext.Set("UserId", httpContext.User.FindFirst(x => x.Type == ClaimTypes.NameIdentifier)?.Value); + + // Retrieve the IEndpointFeature selected for the request + var endpoint = httpContext.GetEndpoint(); + if (endpoint is not null) + { + diagnosticContext.Set("EndpointName", endpoint.DisplayName); + } + } + + /// + /// Shows request logs just in Information log level, this means we should set serilog `MinimumLevel:Default` to `Information` (for example ir doesn't show in Warning level) and shows health check logs just in `Debug` log level that is higher than Information (for getting fewer logs in the output). + /// + /// + /// + /// + /// + public static LogEventLevel GetLogLevel(HttpContext ctx, double _, Exception? ex) => + ex != null ? LogEventLevel.Error + : ctx.Response.StatusCode > 499 ? LogEventLevel.Error + : IsHealthCheckEndpoint(ctx) || IsSwagger(ctx) ? LogEventLevel.Debug + : LogEventLevel.Information; + + private static bool IsSwagger(HttpContext ctx) + { + var isHealth = ctx.Request.Path.Value?.Contains("swagger", StringComparison.Ordinal) ?? false; + + return isHealth; + } + + private static bool IsHealthCheckEndpoint(HttpContext ctx) + { + var isHealth = ctx.Request.Path.Value?.Contains("health", StringComparison.Ordinal) ?? false; + + return isHealth; + } +} diff --git a/src/Shared/Logging/Extensions.cs b/src/Shared/Logging/Extensions/DependencyInjectionExtensions.cs similarity index 66% rename from src/Shared/Logging/Extensions.cs rename to src/Shared/Logging/Extensions/DependencyInjectionExtensions.cs index 938f2fd..8bc0602 100644 --- a/src/Shared/Logging/Extensions.cs +++ b/src/Shared/Logging/Extensions/DependencyInjectionExtensions.cs @@ -1,24 +1,34 @@ +using Elastic.Channels; +using Elastic.Ingest.Elasticsearch; +using Elastic.Ingest.Elasticsearch.DataStreams; +using Elastic.Serilog.Sinks; using Microsoft.AspNetCore.Builder; using Serilog; +using Serilog.Enrichers.Span; using Serilog.Exceptions; using Serilog.Exceptions.Core; using Serilog.Exceptions.EntityFrameworkCore.Destructurers; -using Serilog.Formatting.Elasticsearch; -using Serilog.Sinks.Elasticsearch; +using Serilog.Settings.Configuration; using Serilog.Sinks.Grafana.Loki; +using Serilog.Sinks.Spectre; using Shared.Core.Extensions; +using Shared.Core.Extensions.ServiceCollectionsExtensions; -namespace Shared.Logging; +namespace Shared.Logging.Extensions; -public static class RegistrationExtensions +public static class DependencyInjectionExtensions { public static WebApplicationBuilder AddCustomSerilog( this WebApplicationBuilder builder, - string sectionName = "Serilog", - Action? extraConfigure = null + Action? extraConfigure = null, + Action? configurator = null ) { - var serilogOptions = builder.Configuration.BindOptions(sectionName); + var serilogOptions = builder.Configuration.BindOptions(); + configurator?.Invoke(serilogOptions); + + // add option to the dependency injection + builder.Services.AddValidationOptions(opt => configurator?.Invoke(opt)); // https://andrewlock.net/creating-a-rolling-file-logging-provider-for-asp-net-core-2-0/ // https://github.com/serilog/serilog-extensions-hosting @@ -31,14 +41,12 @@ public static WebApplicationBuilder AddCustomSerilog( loggerConfiguration .Enrich.WithProperty("Application", builder.Environment.ApplicationName) - // .Enrich.WithSpan() - // .Enrich.WithBaggage() + .Enrich.WithSpan() + .Enrich.WithBaggage() .Enrich.WithCorrelationIdHeader() .Enrich.FromLogContext() - // https://github.com/serilog/serilog-enrichers-environment .Enrich.WithEnvironmentName() .Enrich.WithMachineName() - // https://rehansaeed.com/logging-with-serilog-exceptions/ .Enrich.WithExceptionDetails( new DestructuringOptionsBuilder() .WithDefaultDestructurers() @@ -46,40 +54,37 @@ public static WebApplicationBuilder AddCustomSerilog( ); // https://github.com/serilog/serilog-settings-configuration - loggerConfiguration.ReadFrom.Configuration(context.Configuration, sectionName: sectionName); + loggerConfiguration.ReadFrom.Configuration( + context.Configuration, + new ConfigurationReaderOptions { SectionName = nameof(SerilogOptions) } + ); if (serilogOptions.UseConsole) { - if (serilogOptions.UseElasticsearchJsonFormatter) - { - // https://github.com/serilog/serilog-sinks-async - // https://github.com/serilog-contrib/serilog-sinks-elasticsearch#elasticsearch-formatters - loggerConfiguration.WriteTo.Async(writeTo => - writeTo.Console(new ExceptionAsObjectJsonFormatter(renderMessage: true)) - ); - } - else - { - // https://github.com/serilog/serilog-sinks-async - loggerConfiguration.WriteTo.Async(writeTo => - writeTo.Console(outputTemplate: serilogOptions.LogTemplate) - ); - } + // https://github.com/serilog/serilog-sinks-async + // https://github.com/lucadecamillis/serilog-sinks-spectre + loggerConfiguration.WriteTo.Async(writeTo => + writeTo.Spectre(outputTemplate: serilogOptions.LogTemplate) + ); } // https://github.com/serilog/serilog-sinks-async if (!string.IsNullOrEmpty(serilogOptions.ElasticSearchUrl)) { // elasticsearch sink internally is async - // https://github.com/serilog-contrib/serilog-sinks-elasticsearch + // https://www.nuget.org/packages/Elastic.Serilog.Sinks loggerConfiguration.WriteTo.Elasticsearch( - new(new Uri(serilogOptions.ElasticSearchUrl)) + new[] { new Uri(serilogOptions.ElasticSearchUrl) }, + opts => { - AutoRegisterTemplate = true, - AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6, - CustomFormatter = new ExceptionAsObjectJsonFormatter(renderMessage: true), - IndexFormat = - $"{builder.Environment.ApplicationName}-{builder.Environment.EnvironmentName}-{DateTime.Now:yyyy-MM}", + opts.DataStream = new DataStreamName( + $"{builder.Environment.ApplicationName}-{builder.Environment.EnvironmentName}-{DateTime.Now:yyyy-MM}" + ); + opts.BootstrapMethod = BootstrapMethod.Failure; + opts.ConfigureChannel = channelOpts => + { + channelOpts.BufferOptions = new BufferOptions { ExportMaxConcurrency = 10 }; + }; } ); } @@ -91,9 +96,9 @@ public static WebApplicationBuilder AddCustomSerilog( serilogOptions.GrafanaLokiUrl, new[] { - new LokiLabel { Key = "service", Value = "vertical-slice-api-template" }, + new LokiLabel { Key = "service", Value = "food-delivery" }, }, - new[] { "app" } + ["app"] ); } diff --git a/src/Shared/Logging/Extensions/HttpContextExtensions.cs b/src/Shared/Logging/Extensions/HttpContextExtensions.cs new file mode 100644 index 0000000..0922298 --- /dev/null +++ b/src/Shared/Logging/Extensions/HttpContextExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; + +namespace Shared.Logging.Extensions; + +public static class HttpContextExtensions +{ + public static string? GetMetricsCurrentResourceName(this HttpContext httpContext) + { + ArgumentNullException.ThrowIfNull(httpContext); + + Endpoint? endpoint = httpContext.Features.Get()?.Endpoint; + + return endpoint?.Metadata.GetMetadata()?.EndpointName; + } +} diff --git a/src/Shared/Logging/Extensions/LoggerEnrichmentConfigurationExtensions.cs b/src/Shared/Logging/Extensions/LoggerEnrichmentConfigurationExtensions.cs new file mode 100644 index 0000000..44205f9 --- /dev/null +++ b/src/Shared/Logging/Extensions/LoggerEnrichmentConfigurationExtensions.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using Serilog; +using Serilog.Configuration; +using Shared.Logging.Enrichers; + +namespace Shared.Logging.Extensions; + +public static class LoggerEnrichmentConfigurationExtensions +{ + /// + /// Enrich logger output with Baggage information from the current . + /// + /// The enrichment configuration. + /// Configuration object allowing method chaining. + public static LoggerConfiguration WithBaggage(this LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) + { + ArgumentNullException.ThrowIfNull(loggerEnrichmentConfiguration); + return loggerEnrichmentConfiguration.With(new BaggageEnricher()); + } +} diff --git a/src/Shared/Logging/LoggingBehavior.cs b/src/Shared/Logging/LoggingBehavior.cs index 3e3f243..68a056d 100644 --- a/src/Shared/Logging/LoggingBehavior.cs +++ b/src/Shared/Logging/LoggingBehavior.cs @@ -4,17 +4,11 @@ namespace Shared.Logging; -public class LoggingBehavior : IPipelineBehavior +public class LoggingBehavior(ILogger> logger) + : IPipelineBehavior where TRequest : IRequest where TResponse : class { - private readonly ILogger> _logger; - - public LoggingBehavior(ILogger> logger) - { - _logger = logger; - } - public async Task Handle( TRequest request, RequestHandlerDelegate next, @@ -23,7 +17,7 @@ CancellationToken cancellationToken { const string prefix = nameof(LoggingBehavior); - _logger.LogInformation( + logger.LogInformation( "[{Prefix}] Handle request '{RequestData}' and response '{ResponseData}'", prefix, typeof(TRequest).Name, @@ -39,7 +33,7 @@ CancellationToken cancellationToken var timeTaken = timer.Elapsed; if (timeTaken.Seconds > 3) { - _logger.LogWarning( + logger.LogWarning( "[{PerfPossible}] The request '{RequestData}' took '{TimeTaken}' seconds", prefix, typeof(TRequest).Name, @@ -48,7 +42,7 @@ CancellationToken cancellationToken } else { - _logger.LogInformation( + logger.LogInformation( "[{PerfPossible}] The request '{RequestData}' took '{TimeTaken}' seconds", prefix, typeof(TRequest).Name, @@ -56,23 +50,17 @@ CancellationToken cancellationToken ); } - _logger.LogInformation("[{Prefix}] Handled '{RequestData}'", prefix, typeof(TRequest).Name); + logger.LogInformation("[{Prefix}] Handled '{RequestData}'", prefix, typeof(TRequest).Name); return response; } } -public class StreamLoggingBehavior : IStreamPipelineBehavior +public class StreamLoggingBehavior(ILogger> logger) + : IStreamPipelineBehavior where TRequest : IStreamRequest where TResponse : class { - private readonly ILogger> _logger; - - public StreamLoggingBehavior(ILogger> logger) - { - _logger = logger; - } - public async IAsyncEnumerable Handle( TRequest request, StreamHandlerDelegate next, @@ -81,7 +69,7 @@ CancellationToken cancellationToken { const string prefix = nameof(StreamLoggingBehavior); - _logger.LogInformation( + logger.LogInformation( "[{Prefix}] Handle request '{RequestData}' and response '{ResponseData}'", prefix, typeof(TRequest).Name, @@ -97,7 +85,7 @@ CancellationToken cancellationToken var timeTaken = timer.Elapsed; if (timeTaken.Seconds > 3) { - _logger.LogWarning( + logger.LogWarning( "[{PerfPossible}] The request '{RequestData}' took '{TimeTaken}' seconds", prefix, typeof(TRequest).Name, @@ -105,7 +93,7 @@ CancellationToken cancellationToken ); } - _logger.LogInformation("[{Prefix}] Handled '{RequestData}'", prefix, typeof(TRequest).Name); + logger.LogInformation("[{Prefix}] Handled '{RequestData}'", prefix, typeof(TRequest).Name); yield return response; } } diff --git a/src/Shared/Logging/SerilogOptions.cs b/src/Shared/Logging/SerilogOptions.cs index 19aebce..19e7e24 100644 --- a/src/Shared/Logging/SerilogOptions.cs +++ b/src/Shared/Logging/SerilogOptions.cs @@ -1,13 +1,12 @@ namespace Shared.Logging; -internal sealed class SerilogOptions +public sealed class SerilogOptions { public string? SeqUrl { get; set; } public bool UseConsole { get; set; } = true; public bool ExportLogsToOpenTelemetry { get; set; } public string? ElasticSearchUrl { get; set; } public string? GrafanaLokiUrl { get; set; } - public bool UseElasticsearchJsonFormatter { get; set; } public string LogTemplate { get; set; } = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Level} - {Message:lj}{NewLine}{Exception}"; public string? LogPath { get; set; } diff --git a/src/Shared/Resiliency/Extensions/HttpClientBuilderExtensions.cs b/src/Shared/Resiliency/Extensions/HttpClientBuilderExtensions.cs index 0421150..7aa1190 100644 --- a/src/Shared/Resiliency/Extensions/HttpClientBuilderExtensions.cs +++ b/src/Shared/Resiliency/Extensions/HttpClientBuilderExtensions.cs @@ -17,45 +17,41 @@ public static IHttpClientBuilder AddRetryHandler(this IHttpClientBuilder httpCli { var policyOptions = serviceProvider.GetRequiredService>().Value; - var retryPolicy = - // HttpPolicyExtensions.HandleTransientHttpError() - Policy - .Handle() - .OrResult(r => !r.IsSuccessStatusCode) - .Or() // thrown by Polly's TimeoutPolicy if the inner call times out - .WaitAndRetryAsync( - policyOptions.RetryPolicyOptions.Count, - retryAttempt => - TimeSpan.FromSeconds( - Math.Pow(policyOptions.RetryPolicyOptions.BackoffPower, retryAttempt) - ), - onRetry: (outcome, timespan, retryAttempt, context) => + var retryPolicy = Policy + .Handle() + .OrResult(r => !r.IsSuccessStatusCode) + .Or() // thrown by Polly's TimeoutPolicy if the inner call times out + .WaitAndRetryAsync( + policyOptions.RetryPolicyOptions.Count, + retryAttempt => + TimeSpan.FromSeconds(Math.Pow(policyOptions.RetryPolicyOptions.BackoffPower, retryAttempt)), + onRetry: (outcome, timespan, retryAttempt, context) => + { + if (outcome.Result != null) { - if (outcome.Result != null) - { - // https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory#configuring-httpclientfactory-policies-to-use-an-iloggert-from-the-call-site - context - .GetLogger() - ?.LogWarning( - "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}", - outcome.Result.StatusCode, - timespan, - retryAttempt - ); - } - else - { - context - .GetLogger() - ?.LogWarning( - "Request failed because network failure. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}", - timespan, - retryAttempt - ); - } + // https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory#configuring-httpclientfactory-policies-to-use-an-iloggert-from-the-call-site + context + .GetLogger() + ?.LogWarning( + "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}", + outcome.Result.StatusCode, + timespan, + retryAttempt + ); } - ) - .WithPolicyKey(PolicyNames.Retry); + else + { + context + .GetLogger() + ?.LogWarning( + "Request failed because network failure. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}", + timespan, + retryAttempt + ); + } + } + ) + .WithPolicyKey(PolicyNames.Retry); return retryPolicy; } diff --git a/src/Shared/Resiliency/Extensions/ServiceCollectionsExtensions.cs b/src/Shared/Resiliency/Extensions/ServiceCollectionsExtensions.cs index 479ade3..ff67162 100644 --- a/src/Shared/Resiliency/Extensions/ServiceCollectionsExtensions.cs +++ b/src/Shared/Resiliency/Extensions/ServiceCollectionsExtensions.cs @@ -23,44 +23,40 @@ public static IServiceCollection AddCustomPolicyRegistry(this IServiceCollection { var policyOptions = sp.GetRequiredService>().Value; - var retryPolicy = - // HttpPolicyExtensions.HandleTransientHttpError() - Policy - .Handle() - .OrResult(r => !r.IsSuccessStatusCode) - .Or() // thrown by Polly's TimeoutPolicy if the inner call times out - .WaitAndRetryAsync( - policyOptions.RetryPolicyOptions.Count, - retryAttempt => - TimeSpan.FromSeconds( - Math.Pow(policyOptions.RetryPolicyOptions.BackoffPower, retryAttempt) - ), - onRetry: (outcome, timespan, retryAttempt, context) => + var retryPolicy = Policy + .Handle() + .OrResult(r => !r.IsSuccessStatusCode) + .Or() // thrown by Polly's TimeoutPolicy if the inner call times out + .WaitAndRetryAsync( + policyOptions.RetryPolicyOptions.Count, + retryAttempt => + TimeSpan.FromSeconds(Math.Pow(policyOptions.RetryPolicyOptions.BackoffPower, retryAttempt)), + onRetry: (outcome, timespan, retryAttempt, context) => + { + if (outcome.Result != null) { - if (outcome.Result != null) - { - // https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory#configuring-httpclientfactory-policies-to-use-an-iloggert-from-the-call-site - context - .GetLogger() - ?.LogWarning( - "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}", - outcome.Result.StatusCode, - timespan, - retryAttempt - ); - } - else - { - context - .GetLogger() - ?.LogWarning( - "Request failed because network failure. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}", - timespan, - retryAttempt - ); - } + // https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory#configuring-httpclientfactory-policies-to-use-an-iloggert-from-the-call-site + context + .GetLogger() + ?.LogWarning( + "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}", + outcome.Result.StatusCode, + timespan, + retryAttempt + ); } - ); + else + { + context + .GetLogger() + ?.LogWarning( + "Request failed because network failure. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}", + timespan, + retryAttempt + ); + } + } + ); var circuitBreakerPolicy = Policy .Handle() diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index f3c09b7..0d71116 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -52,15 +52,14 @@ - + - - + diff --git a/src/Shared/Web/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.Versioning.cs b/src/Shared/Web/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.Versioning.cs index d7684a9..e3a618e 100644 --- a/src/Shared/Web/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.Versioning.cs +++ b/src/Shared/Web/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.Versioning.cs @@ -48,9 +48,7 @@ public static WebApplicationBuilder AddCustomVersioning(this WebApplicationBuild // can also be used to control the format of the API version in route templates options.SubstituteApiVersionInUrl = true; }) - // this enables binding ApiVersion as a endpoint callback parameter. if you don't use it, then - // you should remove this configuration. - .EnableApiVersionBinding(); + .EnableApiVersionBinding(); // this enables binding ApiVersion as a endpoint callback parameter. if you don't use it, then you should remove this configuration. return builder; }