From 619511ee0b2dfe3670693397034d3b6ef0400c46 Mon Sep 17 00:00:00 2001 From: Mykhailo Shevchuk Date: Sat, 2 Apr 2022 00:04:52 +0300 Subject: [PATCH] Release v8 features (#101) * Concept of v8 * Updated Sample to .NET 6 * Fix dup of label at formatter lvl * Enabled ImplicitUsings * Updated Web sample * Fixed bug at props copying * Fixed tests * Added test for global label and level renaming * Deleted obsolete test file * Added link to Discussions page to issue template * Bumped SDK to .NET 6 in CI pipeline * Deleted garbage file * Bumped .NET SDK to 6 in CodeQL pipeline * Bumped build project * Fixed RegExps in tests --- .github/ISSUE_TEMPLATE/config.yml | 6 +- .github/workflows/ci.yaml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- Directory.Build.props | 2 +- build/Build.csproj | 10 +- build/Program.cs | 58 +- build/Targets.cs | 17 +- .../Person.cs | 7 +- .../Program.cs | 74 ++- .../Serilog.Sinks.Grafana.Loki.Sample.csproj | 5 +- .../Controllers/SerilogController.cs | 59 +- .../Program.cs | 36 +- ...log.Sinks.Grafana.Loki.SampleWebApp.csproj | 5 +- .../Startup.cs | 41 -- .../appsettings.json | 9 +- ...efaultReservedPropertyRenamingStrategy.cs} | 21 +- .../DictionaryComparer.cs | 62 +- .../HttpClients/BaseLokiHttpClient.cs | 80 ++- .../HttpClients/LokiGzipHttpClient.cs | 78 ++- .../HttpClients/LokiHttpClient.cs | 42 +- .../ILabelAwareTextFormatter.cs | 33 +- .../ILokiBatchFormatter.cs | 43 +- .../ILokiHttpClient.cs | 45 +- .../IReservedPropertyRenamingStrategy.cs | 29 + .../Infrastructure/BoundedQueue.cs | 73 ++- .../ExponentialBackoffConnectionSchedule.cs | 83 ++- .../Infrastructure/PortableTimer.cs | 113 ++-- .../LoggerConfigurationLokiExtensions.cs | 194 +++--- .../LokiBatchFormatter.cs | 419 ++++++------- .../LokiCredentials.cs | 27 +- .../LokiJsonTextFormatter.cs | 323 +++++----- src/Serilog.Sinks.Grafana.Loki/LokiLabel.cs | 25 +- src/Serilog.Sinks.Grafana.Loki/LokiSink.cs | 235 ++++---- .../Models/LokiBatch.cs | 30 +- .../Models/LokiLogEvent.cs | 60 ++ .../Models/LokiStream.cs | 31 +- .../Serilog.Sinks.Grafana.Loki.csproj | 1 + .../Utils/DateTimeOffsetExtensions.cs | 11 +- .../Utils/Encoding.cs | 11 +- .../Utils/EnumerableExtensions.cs | 48 ++ .../Utils/LogEventExtensions.cs | 45 ++ .../Utils/LogEventLevelExtensions.cs | 31 +- .../Utils/LokiRoutesBuilder.cs | 11 +- .../BaseLokiHttpClientTests.cs | 68 +-- .../InfrastructureTests/BoundedQueueTests.cs | 94 ++- ...ponentialBackoffConnectionScheduleTests.cs | 156 +++-- .../InfrastructureTests/PortableTimerTests.cs | 196 +++--- ...onShouldBeSerializedCorrectly.approved.txt | 2 +- ...ldHavePriorityOverPropertyOne.approved.txt | 1 + ...abelsShouldBeCreatedCorrectly.approved.txt | 1 + ...houldBeCreatedWithParamPrefix.approved.txt | 1 + ....LevelPropertyShouldBeRenamed.approved.txt | 1 + ...houtMessageShouldNotBeCreated.approved.txt | 2 +- ...ernalTimestampShouldBeCreated.approved.txt | 2 +- ...rsShouldBeSerializedCorrectly.approved.txt | 2 +- ...ouldGenerateNewGroupAndStream.approved.txt | 1 + ...ouldGenerateNewGroupAndStream.approved.txt | 0 ...ouldGenerateNewGroupAndStream.received.txt | 1 + ...abelsShouldBeCreatedCorrectly.approved.txt | 1 + ...ervedKeywordShouldBeSanitized.approved.txt | 2 +- ...abelsShouldBeInTheSameStreams.approved.txt | 1 + ...onShouldBeSerializedCorrectly.approved.txt | 2 +- ...erentStreamsWithoutLevelLabel.approved.txt | 1 - ...eredAccordingToOutputTemplate.approved.txt | 1 - ...lsShouldNotBePresentInRequest.approved.txt | 1 - ...obalLabelsShouldNotBeFiltered.approved.txt | 1 - ...houldBeCreatedWithParamPrefix.approved.txt | 1 - ...LabelShouldBeCreatedCorrectly.approved.txt | 1 - ...ouldGenerateNewGroupAndStream.approved.txt | 1 - ...abelsShouldBePresentInRequest.approved.txt | 1 - ...ouldGenerateNewGroupAndStream.approved.txt | 1 - ...estContentShouldMatchApproved.approved.txt | 1 - ...abelsShouldBeInTheSameStreams.approved.txt | 1 - .../IntegrationTests/AuthTests.cs | 75 ++- ...okiJsonTextFormatterRequestPayloadTests.cs | 564 ++++++++++++------ .../IntegrationTests/RequestPayloadTests.cs | 264 -------- .../IntegrationTests/RequestsUriTests.cs | 39 +- .../Serilog.Sinks.Grafana.Loki.Tests.csproj | 5 +- .../TestHelpers/Backoff/CappedBackoff.cs | 27 +- .../TestHelpers/Backoff/ExponentialBackoff.cs | 40 +- .../TestHelpers/Backoff/IBackoff.cs | 9 +- .../TestHelpers/Backoff/LinearBackoff.cs | 50 +- .../TestHelpers/TestLokiHttpClient.cs | 42 +- .../DateTimeOffsetExtensionsTests.cs | 34 +- .../UtilsTests/EncodingTests.cs | 17 +- .../LogEventLevelExtensionsTests.cs | 27 +- .../UtilsTests/LokiRoutesBuilderTests.cs | 23 +- 87 files changed, 2135 insertions(+), 2163 deletions(-) delete mode 100644 sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Startup.cs rename src/Serilog.Sinks.Grafana.Loki/{LokiLabelFiltrationMode.cs => DefaultReservedPropertyRenamingStrategy.cs} (58%) create mode 100644 src/Serilog.Sinks.Grafana.Loki/IReservedPropertyRenamingStrategy.cs create mode 100644 src/Serilog.Sinks.Grafana.Loki/Models/LokiLogEvent.cs create mode 100644 src/Serilog.Sinks.Grafana.Loki/Utils/EnumerableExtensions.cs create mode 100644 src/Serilog.Sinks.Grafana.Loki/Utils/LogEventExtensions.cs create mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.GlobalLabelShouldHavePriorityOverPropertyOne.approved.txt create mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.GlobalLabelsShouldBeCreatedCorrectly.approved.txt create mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix.approved.txt create mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.LevelPropertyShouldBeRenamed.approved.txt create mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterAsLabelShouldGenerateNewGroupAndStream.approved.txt create mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.approved.txt create mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.received.txt create mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.PropertiesAsLabelsShouldBeCreatedCorrectly.approved.txt create mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.SameGroupLabelsShouldBeInTheSameStreams.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.DifferentLevelsShouldNotGenerateDifferentStreamsWithoutLevelLabel.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.EntryShouldBeRenderedAccordingToOutputTemplate.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.ExcludedLabelsShouldNotBePresentInRequest.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.GlobalLabelsShouldNotBeFiltered.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LevelLabelShouldBeCreatedCorrectly.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LevelLabelShouldGenerateNewGroupAndStream.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.OnlyIncludedLabelsShouldBePresentInRequest.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.RequestContentShouldMatchApproved.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.SameGroupLabelsShouldBeInTheSameStreams.approved.txt delete mode 100644 test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/RequestPayloadTests.cs diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a49eab2..8dfb589 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ -blank_issues_enabled: true \ No newline at end of file +blank_issues_enabled: true +contact_links: + - name: Discussions + url: https://github.com/serilog-contrib/serilog-sinks-grafana-loki/discussions + about: The place for questions, support and feature requests \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c543879..9bd2934 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v2 with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x - run: dotnet --info diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 071a2a5..573ca58 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -41,7 +41,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v2 with: - dotnet-version: '5.0.x' + dotnet-version: '6.0.x' - run: dotnet --info diff --git a/Directory.Build.props b/Directory.Build.props index 5c5e6c2..c2fe06d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ Mykhailo Shevchuk, Contributors $(MSBuildThisFileDirectory)StyleCop.ruleset - 9 + 10 v diff --git a/build/Build.csproj b/build/Build.csproj index 45b3334..b07c663 100644 --- a/build/Build.csproj +++ b/build/Build.csproj @@ -2,12 +2,14 @@ Exe - net5.0 + net6.0 + enable + enable - - + + - + \ No newline at end of file diff --git a/build/Program.cs b/build/Program.cs index 9ee1dd7..9cbb476 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -1,44 +1,42 @@ -using System.IO; -using static Bullseye.Targets; +using static Bullseye.Targets; using static SimpleExec.Command; -namespace Build +namespace Build; + +internal static class Program { - internal static class Program + private const string PackOutput = "./artifacts"; + private const string Solution = "Serilog.Sinks.Grafana.Loki.sln"; + + internal static async Task Main(string[] args) { - private const string PackOutput = "./artifacts"; - private const string Solution = "Serilog.Sinks.Grafana.Loki.sln"; + Target(Targets.CleanBuildOutput, () => { Run("dotnet", $"clean {Solution} -c Release -v m --nologo"); }); - internal static void Main(string[] args) + Target(Targets.Build, DependsOn(Targets.CleanBuildOutput), () => { - Target(Targets.CleanBuildOutput, () => { Run("dotnet", $"clean {Solution} -c Release -v m --nologo"); }); + Run("dotnet", $"build {Solution} -c Release --nologo"); + }); - Target(Targets.Build, DependsOn(Targets.CleanBuildOutput), () => - { - Run("dotnet", $"build {Solution} -c Release --nologo"); - }); - - Target(Targets.Test, DependsOn(Targets.Build), () => - { - Run("dotnet", $"test {Solution} -c Release --no-build --nologo"); - }); + Target(Targets.Test, DependsOn(Targets.Build), () => + { + Run("dotnet", $"test {Solution} -c Release --no-build --nologo"); + }); - Target(Targets.CleanPackOutput, () => + Target(Targets.CleanPackOutput, () => + { + if (Directory.Exists(PackOutput)) { - if (Directory.Exists(PackOutput)) - { - Directory.Delete(PackOutput, true); - } - }); + Directory.Delete(PackOutput, true); + } + }); - Target(Targets.Pack, DependsOn(Targets.CleanPackOutput), () => - { - Run("dotnet", $"pack ./src/Serilog.Sinks.Grafana.Loki/Serilog.Sinks.Grafana.Loki.csproj -c Release -o {Directory.CreateDirectory(PackOutput).FullName} --no-build --nologo"); - }); + Target(Targets.Pack, DependsOn(Targets.CleanPackOutput), () => + { + Run("dotnet", $"pack ./src/Serilog.Sinks.Grafana.Loki/Serilog.Sinks.Grafana.Loki.csproj -c Release -o {Directory.CreateDirectory(PackOutput).FullName} --no-build --nologo"); + }); - Target("default", DependsOn(Targets.Test, Targets.Pack)); + Target("default", DependsOn(Targets.Test, Targets.Pack)); - RunTargetsAndExit(args, ex => ex is SimpleExec.NonZeroExitCodeException); - } + await RunTargetsAndExitAsync(args, ex => ex is SimpleExec.ExitCodeException); } } \ No newline at end of file diff --git a/build/Targets.cs b/build/Targets.cs index 8559fdd..75cba30 100644 --- a/build/Targets.cs +++ b/build/Targets.cs @@ -1,11 +1,10 @@ -namespace Build +namespace Build; + +internal static class Targets { - internal static class Targets - { - internal const string Build = "build"; - internal const string CleanBuildOutput = "clean-build-output"; - internal const string CleanPackOutput = "clean-pack-output"; - internal const string Pack = "pack"; - internal const string Test = "test"; - } + internal const string Build = "build"; + internal const string CleanBuildOutput = "clean-build-output"; + internal const string CleanPackOutput = "clean-pack-output"; + internal const string Pack = "pack"; + internal const string Test = "test"; } \ No newline at end of file diff --git a/sample/Serilog.Sinks.Grafana.Loki.Sample/Person.cs b/sample/Serilog.Sinks.Grafana.Loki.Sample/Person.cs index be452be..69387fc 100644 --- a/sample/Serilog.Sinks.Grafana.Loki.Sample/Person.cs +++ b/sample/Serilog.Sinks.Grafana.Loki.Sample/Person.cs @@ -1,4 +1,3 @@ -namespace Serilog.Sinks.Grafana.Loki.Sample -{ - internal record Person(string Name, int Age); -} \ No newline at end of file +namespace Serilog.Sinks.Grafana.Loki.Sample; + +internal record Person(string Name, int Age); \ No newline at end of file diff --git a/sample/Serilog.Sinks.Grafana.Loki.Sample/Program.cs b/sample/Serilog.Sinks.Grafana.Loki.Sample/Program.cs index 1f36e63..6d53235 100644 --- a/sample/Serilog.Sinks.Grafana.Loki.Sample/Program.cs +++ b/sample/Serilog.Sinks.Grafana.Loki.Sample/Program.cs @@ -1,48 +1,42 @@ -using System; -using System.Collections.Generic; -using Serilog.Debugging; +using Serilog.Debugging; -namespace Serilog.Sinks.Grafana.Loki.Sample +namespace Serilog.Sinks.Grafana.Loki.Sample; + +public static class Program { - public static class Program + private const string OutputTemplate = + "{Timestamp:dd-MM-yyyy HH:mm:ss} [{Level:u3}] [{ThreadId}] {Message}{NewLine}{Exception}"; + + public static void Main(string[] args) { - private const string OutputTemplate = - "{Timestamp:dd-MM-yyyy HH:mm:ss} [{Level:u3}] [{ThreadId}] {Message}{NewLine}{Exception}"; + SelfLog.Enable(Console.Error); + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .Enrich.WithThreadId() + .Enrich.WithProperty("meaning_of_life", 42) + .WriteTo.Console(outputTemplate: OutputTemplate) + .WriteTo.GrafanaLoki( + "http://localhost:3100", + new List { new() { Key = "app", Value = "console" } }, + credentials: null) + .CreateLogger(); + + Log.Debug("This is a debug message"); + + var person = new Person("Billy", 42); - public static void Main(string[] args) + Log.Information("Person of the day: {@Person}", person); + + try + { + throw new AccessViolationException("Access denied"); + } + catch (Exception ex) { - SelfLog.Enable(Console.Error); - - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .Enrich.WithThreadId() - .Enrich.WithProperty("meaning_of_life", "42") - .WriteTo.Console(outputTemplate: OutputTemplate) - .WriteTo.GrafanaLoki( - "http://localhost:3100", - new List { new() { Key = "app", Value = "console" } }, - credentials: null, - outputTemplate: OutputTemplate, - createLevelLabel: true, - useInternalTimestamp: false) - .CreateLogger(); - - Log.Debug("This is a debug message"); - - var person = new Person("Billy", 42); - - Log.Information("Person of the day: {@Person}", person); - - try - { - throw new AccessViolationException("Access denied"); - } - catch (Exception ex) - { - Log.Error(ex, "An error occured"); - } - - Log.CloseAndFlush(); + Log.Error(ex, "An error occured"); } + + Log.CloseAndFlush(); } } \ No newline at end of file diff --git a/sample/Serilog.Sinks.Grafana.Loki.Sample/Serilog.Sinks.Grafana.Loki.Sample.csproj b/sample/Serilog.Sinks.Grafana.Loki.Sample/Serilog.Sinks.Grafana.Loki.Sample.csproj index 2d6d619..48d9105 100644 --- a/sample/Serilog.Sinks.Grafana.Loki.Sample/Serilog.Sinks.Grafana.Loki.Sample.csproj +++ b/sample/Serilog.Sinks.Grafana.Loki.Sample/Serilog.Sinks.Grafana.Loki.Sample.csproj @@ -2,8 +2,9 @@ Exe - net5.0 + net6.0 enable + enable @@ -16,4 +17,4 @@ - + \ No newline at end of file diff --git a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Controllers/SerilogController.cs b/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Controllers/SerilogController.cs index db56ed8..4ad60ab 100644 --- a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Controllers/SerilogController.cs +++ b/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Controllers/SerilogController.cs @@ -1,43 +1,40 @@ -using System; -using System.Net; +using System.Net; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -namespace Serilog.Sinks.Grafana.Loki.SampleWebApp.Controllers +namespace Serilog.Sinks.Grafana.Loki.SampleWebApp.Controllers; + +[ApiController] +[Route("serilog")] +public class SerilogController : ControllerBase { - [ApiController] - [Route("serilog")] - public class SerilogController : ControllerBase + private readonly ILogger _logger; + + public SerilogController(ILogger logger) { - private readonly ILogger _logger; + _logger = logger; + } - public SerilogController(ILogger logger) - { - _logger = logger; - } + [HttpGet("info")] + public IActionResult GetInfo() + { + var odin = new {Id = 1, Name = "Odin"}; + _logger.LogInformation("God of the day {@God}", odin); + return Ok(odin); + } - [HttpGet("info")] - public IActionResult GetInfo() + [HttpGet("error")] + public IActionResult GetError(int id = 3) + { + try { - var odin = new {Id = 1, Name = "Odin"}; - _logger.LogInformation("God of the day {@God}", odin); - return Ok(odin); + // ReSharper disable once IntDivisionByZero + var result = id/0; + return Ok(result); } - - [HttpGet("error")] - public IActionResult GetError(int id = 3) + catch (DivideByZeroException ex) { - try - { - // ReSharper disable once IntDivisionByZero - var result = id/0; - return Ok(result); - } - catch (DivideByZeroException ex) - { - _logger.LogError(ex, "An error occured"); - return StatusCode((int) HttpStatusCode.InternalServerError, ex.Message); - } + _logger.LogError(ex, "An error occured"); + return StatusCode((int) HttpStatusCode.InternalServerError, ex.Message); } } } \ No newline at end of file diff --git a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Program.cs b/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Program.cs index de709bd..4dc4bf7 100644 --- a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Program.cs +++ b/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Program.cs @@ -1,24 +1,20 @@ -using System; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Serilog; using Serilog.Debugging; -namespace Serilog.Sinks.Grafana.Loki.SampleWebApp -{ - public static class Program - { - public static void Main(string[] args) - { - SelfLog.Enable(Console.Error); +SelfLog.Enable(Console.Error); - CreateHostBuilder(args).Build().Run(); - } +var builder = WebApplication.CreateBuilder(); - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureLogging((ctx, cfg) => cfg.ClearProviders()) - .UseSerilog((ctx, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration)) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); - } -} \ No newline at end of file +builder.Host + .ConfigureLogging((_, loggingBuilder) => loggingBuilder.ClearProviders()) + .UseSerilog((ctx, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration)); + +// Add services to the container. +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Serilog.Sinks.Grafana.Loki.SampleWebApp.csproj b/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Serilog.Sinks.Grafana.Loki.SampleWebApp.csproj index b776f09..b8abd11 100644 --- a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Serilog.Sinks.Grafana.Loki.SampleWebApp.csproj +++ b/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Serilog.Sinks.Grafana.Loki.SampleWebApp.csproj @@ -1,8 +1,9 @@ - net5.0 + net6.0 enable + enable @@ -16,4 +17,4 @@ - + \ No newline at end of file diff --git a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Startup.cs b/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Startup.cs deleted file mode 100644 index 26d1bbf..0000000 --- a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/Startup.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Serilog.Sinks.Grafana.Loki.SampleWebApp -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); - } - } -} \ No newline at end of file diff --git a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/appsettings.json b/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/appsettings.json index 772b4bb..a40c2c4 100644 --- a/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/appsettings.json +++ b/sample/Serilog.Sinks.Grafana.Loki.SampleWebApp/appsettings.json @@ -31,14 +31,11 @@ "value": "web_app" } ], - "filtrationMode": "Include", - "filtrationLabels": [ + "propertiesAsLabels": [ "app" - ], - "outputTemplate": "{Timestamp:dd-MM-yyyy HH:mm:ss} [{Level:u3}] [{ThreadId}] {Message}{NewLine}{Exception}", - "textFormatter": "Serilog.Sinks.Grafana.Loki.LokiJsonTextFormatter, Serilog.Sinks.Grafana.Loki" + ] } } ] } -} +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/LokiLabelFiltrationMode.cs b/src/Serilog.Sinks.Grafana.Loki/DefaultReservedPropertyRenamingStrategy.cs similarity index 58% rename from src/Serilog.Sinks.Grafana.Loki/LokiLabelFiltrationMode.cs rename to src/Serilog.Sinks.Grafana.Loki/DefaultReservedPropertyRenamingStrategy.cs index 6796c51..52fc907 100644 --- a/src/Serilog.Sinks.Grafana.Loki/LokiLabelFiltrationMode.cs +++ b/src/Serilog.Sinks.Grafana.Loki/DefaultReservedPropertyRenamingStrategy.cs @@ -8,20 +8,15 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -namespace Serilog.Sinks.Grafana.Loki +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// +/// +public class DefaultReservedPropertyRenamingStrategy : IReservedPropertyRenamingStrategy { /// - /// Mode, used for labels filtration + /// /// - public enum LokiLabelFiltrationMode - { - /// - /// By including specific labels - /// - Include = 0, - /// - /// By excluding specific labels - /// - Exclude = 1 - } + public string Rename(string originalName) => $"_{originalName}"; } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/DictionaryComparer.cs b/src/Serilog.Sinks.Grafana.Loki/DictionaryComparer.cs index 33ee976..52934f7 100644 --- a/src/Serilog.Sinks.Grafana.Loki/DictionaryComparer.cs +++ b/src/Serilog.Sinks.Grafana.Loki/DictionaryComparer.cs @@ -8,49 +8,45 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System.Collections.Generic; -using System.Linq; +namespace Serilog.Sinks.Grafana.Loki; -namespace Serilog.Sinks.Grafana.Loki +/// +/// Used to compare if two dictionaries have equal key and values +/// +/// +/// +internal class DictionaryComparer : IEqualityComparer> { - /// - /// Used to compare if two dictionaries have equal key and values - /// - /// - /// - internal class DictionaryComparer : IEqualityComparer> - { - public static DictionaryComparer Instance { get; } = new(); + public static DictionaryComparer Instance { get; } = new(); - public bool Equals(IDictionary x, IDictionary y) + public bool Equals(IDictionary x, IDictionary y) + { + if (ReferenceEquals(x, y)) { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x.GetType() != y.GetType()) - { - return false; - } + return true; + } - return x.Count == y.Count && !x.Except(y).Any(); + if (x.GetType() != y.GetType()) + { + return false; } - public int GetHashCode(IDictionary obj) + return x.Count == y.Count && !x.Except(y).Any(); + } + + public int GetHashCode(IDictionary obj) + { + // Overflow is fine, just wrap + unchecked { - // Overflow is fine, just wrap - unchecked + var hash = 17; + foreach (var kvp in obj.OrderBy(kvp => kvp.Key)) { - var hash = 17; - foreach (var kvp in obj.OrderBy(kvp => kvp.Key)) - { - hash = (hash * 27) + kvp.Key!.GetHashCode(); - hash = (hash * 27) + kvp.Value!.GetHashCode(); - } - - return hash; + hash = (hash * 27) + kvp.Key!.GetHashCode(); + hash = (hash * 27) + kvp.Value!.GetHashCode(); } + + return hash; } } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/HttpClients/BaseLokiHttpClient.cs b/src/Serilog.Sinks.Grafana.Loki/HttpClients/BaseLokiHttpClient.cs index d498b79..bd21e9c 100644 --- a/src/Serilog.Sinks.Grafana.Loki/HttpClients/BaseLokiHttpClient.cs +++ b/src/Serilog.Sinks.Grafana.Loki/HttpClients/BaseLokiHttpClient.cs @@ -8,63 +8,57 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; -using System.IO; -using System.Linq; -using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using System.Threading.Tasks; -namespace Serilog.Sinks.Grafana.Loki.HttpClients +namespace Serilog.Sinks.Grafana.Loki.HttpClients; + +/// +/// Base http client for sending log events to Grafana Loki. +/// Implements method for sending authorization header +/// +public abstract class BaseLokiHttpClient : ILokiHttpClient { /// - /// Base http client for sending log events to Grafana Loki. - /// Implements method for sending authorization header + /// used for requests. + /// + protected readonly HttpClient HttpClient; + + /// + /// Initializes a new instance of the class. /// - public abstract class BaseLokiHttpClient : ILokiHttpClient + /// + /// be used for HTTP requests. + /// + protected BaseLokiHttpClient(HttpClient? httpClient = null) { - /// - /// used for requests. - /// - protected readonly HttpClient HttpClient; + HttpClient = httpClient ?? new HttpClient(); + } + + /// + public abstract Task PostAsync(string requestUri, Stream contentStream); - /// - /// Initializes a new instance of the class. - /// - /// - /// be used for HTTP requests. - /// - protected BaseLokiHttpClient(HttpClient? httpClient = null) + /// + public virtual void SetCredentials(LokiCredentials? credentials) + { + if (credentials == null || credentials.IsEmpty) { - HttpClient = httpClient ?? new HttpClient(); + return; } - /// - public abstract Task PostAsync(string requestUri, Stream contentStream); + var headers = HttpClient.DefaultRequestHeaders; - /// - public virtual void SetCredentials(LokiCredentials? credentials) + if (headers.Any(h => h.Key == "Authorization")) { - if (credentials == null || credentials.IsEmpty) - { - return; - } - - var headers = HttpClient.DefaultRequestHeaders; - - if (headers.Any(h => h.Key == "Authorization")) - { - return; - } - - var token = Base64Encode($"{credentials.Login}:{credentials.Password ?? string.Empty}"); - headers.Authorization = new AuthenticationHeaderValue("Basic", token); + return; } - /// - public virtual void Dispose() => HttpClient.Dispose(); - - private static string Base64Encode(string str) => Convert.ToBase64String(Encoding.UTF8.GetBytes(str)); + var token = Base64Encode($"{credentials.Login}:{credentials.Password ?? string.Empty}"); + headers.Authorization = new AuthenticationHeaderValue("Basic", token); } + + /// + public virtual void Dispose() => HttpClient.Dispose(); + + private static string Base64Encode(string str) => Convert.ToBase64String(Encoding.UTF8.GetBytes(str)); } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/HttpClients/LokiGzipHttpClient.cs b/src/Serilog.Sinks.Grafana.Loki/HttpClients/LokiGzipHttpClient.cs index ce3a10f..8662cee 100644 --- a/src/Serilog.Sinks.Grafana.Loki/HttpClients/LokiGzipHttpClient.cs +++ b/src/Serilog.Sinks.Grafana.Loki/HttpClients/LokiGzipHttpClient.cs @@ -8,59 +8,55 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System.IO; using System.IO.Compression; -using System.Net.Http; using System.Net.Http.Headers; -using System.Threading.Tasks; -namespace Serilog.Sinks.Grafana.Loki.HttpClients +namespace Serilog.Sinks.Grafana.Loki.HttpClients; + +/// +/// Http client with gzip compression used for sending log events to Grafana Loki. +/// +public class LokiGzipHttpClient : BaseLokiHttpClient { /// - /// Http client with gzip compression used for sending log events to Grafana Loki. + /// used for compression. + /// + protected readonly CompressionLevel CompressionLevel; + + /// + /// Initializes a new instance of the class. /// - public class LokiGzipHttpClient : BaseLokiHttpClient + /// + /// be used for HTTP requests. + /// + /// + /// be used for HTTP requests. + /// + public LokiGzipHttpClient( + HttpClient? httpClient = null, + CompressionLevel compressionLevel = CompressionLevel.Fastest) + : base(httpClient) { - /// - /// used for compression. - /// - protected readonly CompressionLevel CompressionLevel; + CompressionLevel = compressionLevel; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// be used for HTTP requests. - /// - /// - /// be used for HTTP requests. - /// - public LokiGzipHttpClient( - HttpClient? httpClient = null, - CompressionLevel compressionLevel = CompressionLevel.Fastest) - : base(httpClient) - { - CompressionLevel = compressionLevel; - } + /// + public override async Task PostAsync(string requestUri, Stream contentStream) + { + using var output = new MemoryStream(); - /// - public override async Task PostAsync(string requestUri, Stream contentStream) + using (var gzipStream = new GZipStream(output, CompressionLevel, true)) { - using var output = new MemoryStream(); - - using (var gzipStream = new GZipStream(output, CompressionLevel, true)) - { - await contentStream.CopyToAsync(gzipStream).ConfigureAwait(false); - } + await contentStream.CopyToAsync(gzipStream).ConfigureAwait(false); + } - output.Position = 0; + output.Position = 0; - using (var content = new StreamContent(output)) - { - content.Headers.ContentEncoding.Add("gzip"); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - return await HttpClient.PostAsync(requestUri, content).ConfigureAwait(false); - } + using (var content = new StreamContent(output)) + { + content.Headers.ContentEncoding.Add("gzip"); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + return await HttpClient.PostAsync(requestUri, content).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/HttpClients/LokiHttpClient.cs b/src/Serilog.Sinks.Grafana.Loki/HttpClients/LokiHttpClient.cs index 7a9d0a5..882102a 100644 --- a/src/Serilog.Sinks.Grafana.Loki/HttpClients/LokiHttpClient.cs +++ b/src/Serilog.Sinks.Grafana.Loki/HttpClients/LokiHttpClient.cs @@ -8,35 +8,31 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System.IO; -using System.Net.Http; using System.Net.Http.Headers; -using System.Threading.Tasks; -namespace Serilog.Sinks.Grafana.Loki.HttpClients +namespace Serilog.Sinks.Grafana.Loki.HttpClients; + +/// +/// Default http client used for sending log events to Grafana Loki. +/// +public class LokiHttpClient : BaseLokiHttpClient { /// - /// Default http client used for sending log events to Grafana Loki. + /// Initializes a new instance of the class. /// - public class LokiHttpClient : BaseLokiHttpClient + /// + /// be used for HTTP requests. + /// + public LokiHttpClient(HttpClient? httpClient = null) + : base(httpClient) { - /// - /// Initializes a new instance of the class. - /// - /// - /// be used for HTTP requests. - /// - public LokiHttpClient(HttpClient? httpClient = null) - : base(httpClient) - { - } + } - /// - public override async Task PostAsync(string requestUri, Stream contentStream) - { - using var content = new StreamContent(contentStream); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - return await HttpClient.PostAsync(requestUri, content).ConfigureAwait(false); - } + /// + public override async Task PostAsync(string requestUri, Stream contentStream) + { + using var content = new StreamContent(contentStream); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + return await HttpClient.PostAsync(requestUri, content).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/ILabelAwareTextFormatter.cs b/src/Serilog.Sinks.Grafana.Loki/ILabelAwareTextFormatter.cs index 52c04ea..0e0605e 100644 --- a/src/Serilog.Sinks.Grafana.Loki/ILabelAwareTextFormatter.cs +++ b/src/Serilog.Sinks.Grafana.Loki/ILabelAwareTextFormatter.cs @@ -8,28 +8,25 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System.Collections.Generic; -using System.IO; using Serilog.Events; -namespace Serilog.Sinks.Grafana.Loki +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// Interface that has a Format method that accepts labels as input +/// +public interface ILabelAwareTextFormatter { /// - /// Interface that has a Format method that accepts labels as input + /// Used to exclude the Level label. /// - public interface ILabelAwareTextFormatter - { - /// - /// Used to exclude the Level label. - /// - public bool ExcludeLevelLabel { get; } + public bool ExcludeLevelLabel { get; } - /// - /// Format the log event into the output. - /// - /// The event to format. - /// The output. - /// List of labels that are attached to this stream - public void Format(LogEvent logEvent, TextWriter output, IEnumerable labels); - } + /// + /// Format the log event into the output. + /// + /// The event to format. + /// The output. + /// List of labels that are attached to this stream + public void Format(LogEvent logEvent, TextWriter output, IEnumerable labels); } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/ILokiBatchFormatter.cs b/src/Serilog.Sinks.Grafana.Loki/ILokiBatchFormatter.cs index d1f7aef..f9546c6 100644 --- a/src/Serilog.Sinks.Grafana.Loki/ILokiBatchFormatter.cs +++ b/src/Serilog.Sinks.Grafana.Loki/ILokiBatchFormatter.cs @@ -8,31 +8,30 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.IO; -using Serilog.Events; using Serilog.Formatting; +using Serilog.Sinks.Grafana.Loki.Models; -namespace Serilog.Sinks.Grafana.Loki +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// Formats batches of log events into payloads that can be sent over the network. +/// +public interface ILokiBatchFormatter { /// - /// Formats batches of log events into payloads that can be sent over the network. + /// Format the log events into a payload. /// - public interface ILokiBatchFormatter - { - /// - /// Format the log events into a payload. - /// - /// - /// The events to format. - /// - /// - /// The formatter turning the log events into a textual representation. - /// - /// - /// The payload to send over the network. - /// - void Format(IReadOnlyCollection<(LogEvent LogEntry, DateTimeOffset Timestamp)> logEvents, ITextFormatter formatter, TextWriter output); - } + /// + /// The events to format wrapped in . + /// + /// + /// The formatter turning the log events into a textual representation. + /// + /// + /// The payload to send over the network. + /// + void Format( + IReadOnlyCollection lokiLogEvents, + ITextFormatter formatter, + TextWriter output); } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/ILokiHttpClient.cs b/src/Serilog.Sinks.Grafana.Loki/ILokiHttpClient.cs index a85b53d..ec0d810 100644 --- a/src/Serilog.Sinks.Grafana.Loki/ILokiHttpClient.cs +++ b/src/Serilog.Sinks.Grafana.Loki/ILokiHttpClient.cs @@ -8,35 +8,30 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; using Serilog.Sinks.Grafana.Loki.HttpClients; -namespace Serilog.Sinks.Grafana.Loki +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// Interface responsible for posting HTTP events +/// and handling authorization for Grafana Loki. +/// +/// +public interface ILokiHttpClient : IDisposable { /// - /// Interface responsible for posting HTTP events - /// and handling authorization for Grafana Loki. + /// Sends a POST request to the specified Uri as an asynchronous operation. /// - /// - public interface ILokiHttpClient : IDisposable - { - /// - /// Sends a POST request to the specified Uri as an asynchronous operation. - /// - /// The Uri the request is sent to. - /// The stream containing the content of the request. - /// The task object representing the asynchronous operation. - Task PostAsync(string requestUri, Stream contentStream); + /// The Uri the request is sent to. + /// The stream containing the content of the request. + /// The task object representing the asynchronous operation. + Task PostAsync(string requestUri, Stream contentStream); - /// - /// Adds authorization header to all requests. - /// - /// - /// used for authorization. - /// - void SetCredentials(LokiCredentials? credentials); - } + /// + /// Adds authorization header to all requests. + /// + /// + /// used for authorization. + /// + void SetCredentials(LokiCredentials? credentials); } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/IReservedPropertyRenamingStrategy.cs b/src/Serilog.Sinks.Grafana.Loki/IReservedPropertyRenamingStrategy.cs new file mode 100644 index 0000000..1b0324c --- /dev/null +++ b/src/Serilog.Sinks.Grafana.Loki/IReservedPropertyRenamingStrategy.cs @@ -0,0 +1,29 @@ +// Copyright 2020-2022 Mykhailo Shevchuk & Contributors +// +// Licensed under the MIT license; +// you may not use this file except in compliance with the License. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See LICENSE file in the project root for full license information. + +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// Defines renaming strategy for properties with names equal to sink's reserved keywords. +/// +public interface IReservedPropertyRenamingStrategy +{ + /// + /// Property rename function + /// By default adds an underscore to the property name. + /// + /// + /// Original name of property. + /// + /// + /// New property name. + /// + string Rename(string originalName); +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Infrastructure/BoundedQueue.cs b/src/Serilog.Sinks.Grafana.Loki/Infrastructure/BoundedQueue.cs index 985bcda..2c7e68d 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Infrastructure/BoundedQueue.cs +++ b/src/Serilog.Sinks.Grafana.Loki/Infrastructure/BoundedQueue.cs @@ -8,60 +8,55 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; -using System.Collections.Generic; -using Serilog.Events; +namespace Serilog.Sinks.Grafana.Loki.Infrastructure; -namespace Serilog.Sinks.Grafana.Loki.Infrastructure +internal class BoundedQueue { - internal class BoundedQueue - { - private const int Unbounded = -1; + private const int Unbounded = -1; - private readonly Queue<(T Event, DateTimeOffset Timestamp)> _queue; - private readonly int _queueLimit; - private readonly object _syncRoot = new(); + private readonly Queue _queue; + private readonly int _queueLimit; + private readonly object _syncRoot = new(); - public BoundedQueue(int? queueLimit) + public BoundedQueue(int? queueLimit) + { + if (queueLimit < 1) { - if (queueLimit < 1) - { - throw new ArgumentOutOfRangeException( - nameof(queueLimit), - "Queue limit must be positive, or `null` to indicate unbounded"); - } - - _queue = new Queue<(T, DateTimeOffset)>(); - _queueLimit = queueLimit ?? Unbounded; + throw new ArgumentOutOfRangeException( + nameof(queueLimit), + "Queue limit must be positive, or `null` to indicate unbounded"); } - public bool TryEnqueue(T item) + _queue = new Queue(); + _queueLimit = queueLimit ?? Unbounded; + } + + public bool TryEnqueue(T item) + { + lock (_syncRoot) { - lock (_syncRoot) + if (_queueLimit != Unbounded && _queueLimit == _queue.Count) { - if (_queueLimit != Unbounded && _queueLimit == _queue.Count) - { - return false; - } - - _queue.Enqueue((item, DateTimeOffset.UtcNow)); - return true; + return false; } + + _queue.Enqueue(item); + return true; } + } - public bool TryDequeue(out (T Event, DateTimeOffset Timestamp)? item) + public bool TryDequeue(out T? item) + { + lock (_syncRoot) { - lock (_syncRoot) + if (_queue.Count == 0) { - if (_queue.Count == 0) - { - item = default; - return false; - } - - item = _queue.Dequeue(); - return true; + item = default; + return false; } + + item = _queue.Dequeue(); + return true; } } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Infrastructure/ExponentialBackoffConnectionSchedule.cs b/src/Serilog.Sinks.Grafana.Loki/Infrastructure/ExponentialBackoffConnectionSchedule.cs index 5b76fee..41f3298 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Infrastructure/ExponentialBackoffConnectionSchedule.cs +++ b/src/Serilog.Sinks.Grafana.Loki/Infrastructure/ExponentialBackoffConnectionSchedule.cs @@ -8,65 +8,62 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; +namespace Serilog.Sinks.Grafana.Loki.Infrastructure; -namespace Serilog.Sinks.Grafana.Loki.Infrastructure +internal class ExponentialBackoffConnectionSchedule { - internal class ExponentialBackoffConnectionSchedule - { - public static readonly TimeSpan MinimumBackoffPeriod = TimeSpan.FromSeconds(5); - public static readonly TimeSpan MaximumBackoffInterval = TimeSpan.FromMinutes(10); + public static readonly TimeSpan MinimumBackoffPeriod = TimeSpan.FromSeconds(5); + public static readonly TimeSpan MaximumBackoffInterval = TimeSpan.FromMinutes(10); - private readonly TimeSpan _period; + private readonly TimeSpan _period; - private int _failuresSinceSuccessfulConnection; + private int _failuresSinceSuccessfulConnection; - public ExponentialBackoffConnectionSchedule(TimeSpan period) + public ExponentialBackoffConnectionSchedule(TimeSpan period) + { + if (period < TimeSpan.Zero) { - if (period < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException( - nameof(period), - "The connection retry period must be a positive timespan"); - } - - _period = period; + throw new ArgumentOutOfRangeException( + nameof(period), + "The connection retry period must be a positive timespan"); } - public TimeSpan NextInterval + _period = period; + } + + public TimeSpan NextInterval + { + get { - get + try { - try + if (_failuresSinceSuccessfulConnection <= 1) { - if (_failuresSinceSuccessfulConnection <= 1) - { - return _period; - } + return _period; + } - var backoffFactor = Math.Pow(2, _failuresSinceSuccessfulConnection - 1); - var backoffPeriod = Math.Max(_period.Ticks, MinimumBackoffPeriod.Ticks); - var backedOff = checked((long)(backoffPeriod * backoffFactor)); - var cappedBackoff = Math.Min(MaximumBackoffInterval.Ticks, backedOff); - var actual = Math.Max(_period.Ticks, cappedBackoff); + var backoffFactor = Math.Pow(2, _failuresSinceSuccessfulConnection - 1); + var backoffPeriod = Math.Max(_period.Ticks, MinimumBackoffPeriod.Ticks); + var backedOff = checked((long)(backoffPeriod * backoffFactor)); + var cappedBackoff = Math.Min(MaximumBackoffInterval.Ticks, backedOff); + var actual = Math.Max(_period.Ticks, cappedBackoff); - return TimeSpan.FromTicks(actual); - } - catch (OverflowException) - { - return MaximumBackoffInterval; - } + return TimeSpan.FromTicks(actual); + } + catch (OverflowException) + { + return MaximumBackoffInterval; } } + } - public void MarkSuccess() - { - _failuresSinceSuccessfulConnection = 0; - } + public void MarkSuccess() + { + _failuresSinceSuccessfulConnection = 0; + } - public void MarkFailure() - { - _failuresSinceSuccessfulConnection++; - } + public void MarkFailure() + { + _failuresSinceSuccessfulConnection++; } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Infrastructure/PortableTimer.cs b/src/Serilog.Sinks.Grafana.Loki/Infrastructure/PortableTimer.cs index 5f18f00..de0d2ab 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Infrastructure/PortableTimer.cs +++ b/src/Serilog.Sinks.Grafana.Loki/Infrastructure/PortableTimer.cs @@ -8,46 +8,64 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; -using System.Threading; -using System.Threading.Tasks; +namespace Serilog.Sinks.Grafana.Loki.Infrastructure; -namespace Serilog.Sinks.Grafana.Loki.Infrastructure +internal class PortableTimer : IDisposable { - internal class PortableTimer : IDisposable - { - private readonly Func _onTick; - private readonly object _syncRoot = new(); - private readonly Timer _timer; + private readonly Func _onTick; + private readonly object _syncRoot = new(); + private readonly Timer _timer; - private bool _isDisposed; - private bool _isRunning; + private bool _isDisposed; + private bool _isRunning; - public PortableTimer(Func onTick) + public PortableTimer(Func onTick) + { + _onTick = onTick ?? throw new ArgumentNullException(nameof(onTick)); + _timer = new Timer(_ => OnTick(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + } + + public void Start(TimeSpan interval) + { + if (interval < TimeSpan.Zero) { - _onTick = onTick ?? throw new ArgumentNullException(nameof(onTick)); - _timer = new Timer(_ => OnTick(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + throw new ArgumentOutOfRangeException(nameof(interval)); } - public void Start(TimeSpan interval) + lock (_syncRoot) { - if (interval < TimeSpan.Zero) + if (_isDisposed) { - throw new ArgumentOutOfRangeException(nameof(interval)); + throw new ObjectDisposedException(nameof(PortableTimer)); } - lock (_syncRoot) + _timer.Change(interval, Timeout.InfiniteTimeSpan); + } + } + + public void Dispose() + { + lock (_syncRoot) + { + if (_isDisposed) { - if (_isDisposed) - { - throw new ObjectDisposedException(nameof(PortableTimer)); - } + return; + } - _timer.Change(interval, Timeout.InfiniteTimeSpan); + while (_isRunning) + { + Monitor.Wait(_syncRoot); } + + _timer.Dispose(); + + _isDisposed = true; } + } - public void Dispose() + private async void OnTick() + { + try { lock (_syncRoot) { @@ -56,52 +74,29 @@ public void Dispose() return; } - while (_isRunning) + // There's a little bit of raciness here, but it's needed to support the + // current API, which allows the tick handler to reenter and set the next interval. + if (_isRunning) { Monitor.Wait(_syncRoot); - } - - _timer.Dispose(); - - _isDisposed = true; - } - } - private async void OnTick() - { - try - { - lock (_syncRoot) - { if (_isDisposed) { return; } - - // There's a little bit of raciness here, but it's needed to support the - // current API, which allows the tick handler to reenter and set the next interval. - if (_isRunning) - { - Monitor.Wait(_syncRoot); - - if (_isDisposed) - { - return; - } - } - - _isRunning = true; } - await _onTick(); + _isRunning = true; } - finally + + await _onTick(); + } + finally + { + lock (_syncRoot) { - lock (_syncRoot) - { - _isRunning = false; - Monitor.PulseAll(_syncRoot); - } + _isRunning = false; + Monitor.PulseAll(_syncRoot); } } } diff --git a/src/Serilog.Sinks.Grafana.Loki/LoggerConfigurationLokiExtensions.cs b/src/Serilog.Sinks.Grafana.Loki/LoggerConfigurationLokiExtensions.cs index 58b66ea..debdd55 100644 --- a/src/Serilog.Sinks.Grafana.Loki/LoggerConfigurationLokiExtensions.cs +++ b/src/Serilog.Sinks.Grafana.Loki/LoggerConfigurationLokiExtensions.cs @@ -8,8 +8,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; using Serilog.Configuration; using Serilog.Events; @@ -20,115 +18,107 @@ [assembly: InternalsVisibleTo("Serilog.Sinks.Grafana.Loki.Tests")] -namespace Serilog.Sinks.Grafana.Loki +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// Class containing extension methods to , configuring sinks +/// sending log events to Grafana Loki using HTTP. +/// +public static class LoggerConfigurationLokiExtensions { + private const string DefaultOutputTemplate = + "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"; + /// - /// Class containing extension methods to , configuring sinks - /// sending log events to Grafana Loki using HTTP. + /// Adds a non-durable sink that will send log events to Grafana Loki. + /// A non-durable sink will lose data after a system or process restart. /// - public static class LoggerConfigurationLokiExtensions + /// + /// The logger configuration. + /// + /// + /// The root URI of Loki. + /// + /// + /// The global log event labels, which will be user for enriching all requests. + /// + /// + /// The list of properties, which would be mapped to the labels. + /// + /// + /// Auth . + /// + /// + /// The minimum level for events passed through the sink. + /// Default value is . + /// + /// + /// The maximum number of events to post in a single batch. Default value is 1000. + /// + /// + /// The maximum number of events stored in the queue in memory, waiting to be posted over + /// the network. Default value is infinitely. + /// + /// + /// The time to wait between checking for event batches. Default value is 2 seconds. + /// + /// + /// The formatter rendering individual log events into text, for example JSON. Default + /// value is . + /// + /// + /// A custom implementation. Default value is + /// . + /// + /// + /// Renaming strategy for properties' names equal to reserved keywords. + /// + /// + /// Should use internal sink timestamp instead of application one to use as log timestamp. + /// + /// Logger configuration, allowing configuration to continue. + public static LoggerConfiguration GrafanaLoki( + this LoggerSinkConfiguration sinkConfiguration, + string uri, + IEnumerable? labels = null, + IEnumerable? propertiesAsLabels = null, + LokiCredentials? credentials = null, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + int batchPostingLimit = 1000, + int? queueLimit = null, + TimeSpan? period = null, + ITextFormatter? textFormatter = null, + ILokiHttpClient? httpClient = null, + IReservedPropertyRenamingStrategy? reservedPropertyRenamingStrategy = null, + bool useInternalTimestamp = false) { - private const string DefaultOutputTemplate = - "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"; - - /// - /// Adds a non-durable sink that will send log events to Grafana Loki. - /// A non-durable sink will lose data after a system or process restart. - /// - /// - /// The logger configuration. - /// - /// - /// The root URI of Loki. - /// - /// - /// The global log event labels, which will be user for enriching all requests. - /// - /// - /// The mode for labels filtration - /// - /// - /// - /// The list of label keys used for filtration - /// - /// - /// Auth . - /// - /// - /// A message template describing the format used to write to the sink. - /// Default value is "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}". - /// - /// - /// The minimum level for events passed through the sink. - /// Default value is . - /// - /// - /// The maximum number of events to post in a single batch. Default value is 1000. - /// - /// - /// The maximum number of events stored in the queue in memory, waiting to be posted over - /// the network. Default value is infinitely. - /// - /// - /// The time to wait between checking for event batches. Default value is 2 seconds. - /// - /// - /// The formatter rendering individual log events into text, for example JSON. Default - /// value is . - /// - /// - /// A custom implementation. Default value is - /// . - /// - /// - /// Should level label be created. Default value is false - /// The level label always won't be created while using - /// - /// - /// Should use internal sink timestamp instead of application one to use as log timestamp. - /// - /// Logger configuration, allowing configuration to continue. - public static LoggerConfiguration GrafanaLoki( - this LoggerSinkConfiguration sinkConfiguration, - string uri, - IEnumerable? labels = null, - LokiLabelFiltrationMode? filtrationMode = null, - IEnumerable? filtrationLabels = null, - LokiCredentials? credentials = null, - string outputTemplate = DefaultOutputTemplate, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - int batchPostingLimit = 1000, - int? queueLimit = null, - TimeSpan? period = null, - ITextFormatter? textFormatter = null, - ILokiHttpClient? httpClient = null, - bool createLevelLabel = false, - bool useInternalTimestamp = false) + if (sinkConfiguration == null) { - if (sinkConfiguration == null) - { - throw new ArgumentNullException(nameof(sinkConfiguration)); - } + throw new ArgumentNullException(nameof(sinkConfiguration)); + } - createLevelLabel = createLevelLabel && textFormatter is not ILabelAwareTextFormatter {ExcludeLevelLabel: true}; - var batchFormatter = new LokiBatchFormatter(labels, filtrationMode, filtrationLabels, createLevelLabel, useInternalTimestamp); + reservedPropertyRenamingStrategy ??= new DefaultReservedPropertyRenamingStrategy(); + period ??= TimeSpan.FromSeconds(1); + textFormatter ??= new LokiJsonTextFormatter(reservedPropertyRenamingStrategy); + httpClient ??= new LokiHttpClient(); - period ??= TimeSpan.FromSeconds(1); - textFormatter ??= new MessageTemplateTextFormatter(outputTemplate); - httpClient ??= new LokiHttpClient(); + httpClient.SetCredentials(credentials); - httpClient.SetCredentials(credentials); + var batchFormatter = new LokiBatchFormatter( + reservedPropertyRenamingStrategy, + labels, + propertiesAsLabels, + useInternalTimestamp); - var sink = new LokiSink( - LokiRoutesBuilder.BuildLogsEntriesRoute(uri), - batchPostingLimit, - queueLimit, - period.Value, - textFormatter, - batchFormatter, - httpClient); + var sink = new LokiSink( + LokiRoutesBuilder.BuildLogsEntriesRoute(uri), + batchPostingLimit, + queueLimit, + period.Value, + textFormatter, + batchFormatter, + httpClient); - return sinkConfiguration.Sink(sink, restrictedToMinimumLevel); - } + return sinkConfiguration.Sink(sink, restrictedToMinimumLevel); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/LokiBatchFormatter.cs b/src/Serilog.Sinks.Grafana.Loki/LokiBatchFormatter.cs index 0c6a773..3b8daf7 100644 --- a/src/Serilog.Sinks.Grafana.Loki/LokiBatchFormatter.cs +++ b/src/Serilog.Sinks.Grafana.Loki/LokiBatchFormatter.cs @@ -1,202 +1,219 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Serilog.Events; -using Serilog.Formatting; -using Serilog.Sinks.Grafana.Loki.Models; -using Serilog.Sinks.Grafana.Loki.Utils; - -namespace Serilog.Sinks.Grafana.Loki -{ - /// - /// Formatter serializing batches of log events into a JSON object in the format, recognized by Grafana Loki. - /// - /// Example: - /// - /// { - /// "streams": [ - /// { - /// "stream": { - /// "label": "value" - /// }, - /// "values": [ - /// [ "unix epoch in nanoseconds", "log line" ], - /// [ "unix epoch in nanoseconds", "log line" ] - /// ] - /// } - /// ] - /// } - /// - /// - internal class LokiBatchFormatter : ILokiBatchFormatter - { - private const int DefaultWriteBufferCapacity = 256; - - private readonly IEnumerable _globalLabels; - private readonly LokiLabelFiltrationMode? _filtrationMode; - private readonly IEnumerable _filtrationLabels; - private readonly bool _createLevelLabel; - private readonly bool _useInternalTimestamp; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The list of global . - /// - /// - /// The mode for labels filtration - /// - /// - /// The list of label keys used for filtration - /// - /// - /// Used to force the level to be created as a label - /// - /// - /// Compute internal timestamp - /// - public LokiBatchFormatter( - IEnumerable? globalLabels = null, - LokiLabelFiltrationMode? filtrationMode = null, - IEnumerable? filtrationLabels = null, - bool createLevelLabel = true, - bool useInternalTimestamp = false) - { - _globalLabels = globalLabels ?? Enumerable.Empty(); - _filtrationMode = filtrationMode; - _filtrationLabels = filtrationLabels ?? Enumerable.Empty(); - _createLevelLabel = createLevelLabel; - _useInternalTimestamp = useInternalTimestamp; - } - - /// - /// Format the log events into a payload. - /// - /// - /// The events to format. - /// - /// - /// The formatter turning the log events into a textual representation. - /// - /// - /// The payload to send over the network. - /// - /// - public void Format(IReadOnlyCollection<(LogEvent LogEntry, DateTimeOffset Timestamp)> logEvents, ITextFormatter formatter, TextWriter output) - { - if (logEvents == null) - { - throw new ArgumentNullException(nameof(logEvents)); - } - - if (output == null) - { - throw new ArgumentNullException(nameof(output)); - } - - if (logEvents.Count == 0) - { - return; - } - - var batch = new LokiBatch(); - - // Group logEvent by labels - var groups = logEvents - .Select(le => new { Labels = GenerateLabels(le), LogEventTuple = le }) - .GroupBy(le => le.Labels, le => le.LogEventTuple, DictionaryComparer.Instance); - - foreach (var group in groups) - { - var labels = group.Key; - var stream = batch.CreateStream(); - - foreach (var label in labels) - { - stream.AddLabel(label.Key, label.Value); - } - - foreach (var logEvent in group.OrderBy(x => _useInternalTimestamp ? x.Timestamp : x.LogEntry.Timestamp)) - { - GenerateEntry(logEvent, formatter, stream, labels.Keys); - } - } - - if (batch.IsNotEmpty) - { - output.Write(batch.Serialize()); - } - } - - private void GenerateEntry((LogEvent LogEntry, DateTimeOffset Timestamp) logEventTuple, ITextFormatter formatter, LokiStream stream, IEnumerable labels) - { - var buffer = new StringWriter(new StringBuilder(DefaultWriteBufferCapacity)); - - DateTimeOffset timestamp = logEventTuple.LogEntry.Timestamp; - if (_useInternalTimestamp) - { - logEventTuple.LogEntry.AddPropertyIfAbsent(new LogEventProperty("Timestamp", new ScalarValue(timestamp))); - timestamp = logEventTuple.Timestamp; - } - - if (formatter is ILabelAwareTextFormatter labelAwareTextFormatter) - { - labelAwareTextFormatter.Format(logEventTuple.LogEntry, buffer, labels); - } - else - { - formatter.Format(logEventTuple.LogEntry, buffer); - } - - stream.AddEntry(timestamp, buffer.ToString().TrimEnd('\r', '\n')); - } - - private Dictionary GenerateLabels((LogEvent LogEntry, DateTimeOffset Timestamp) logEventTuple) - { - var labels = _globalLabels.ToDictionary(label => label.Key, label => label.Value); - - if (_createLevelLabel) - { - labels.Add("level", logEventTuple.LogEntry.Level.ToGrafanaLogLevel()); - } - - foreach (var property in logEventTuple.LogEntry.Properties) - { - var key = property.Key; - - // If a message template is a composite format string that contains indexed placeholders ({0}, {1} etc), - // Serilog turns these placeholders into event properties keyed by numeric strings. - // Loki doesn't accept such strings as label keys. Prefix these numeric strings with "param" - // to turn them into valid label keys and at the same time denote them as ordinal parameters. - if (char.IsDigit(key[0])) - { - key = $"param{key}"; - } - - // Some enrichers generates extra quotes and it breaks the payload - var value = property.Value.ToString().Replace("\"", string.Empty); - - if (IsAllowedByFilter(key)) - { - labels.Add(key, value); - } - } - - return labels; - } - - private bool IsAllowedByFilter(string label) => - _filtrationMode switch - { - LokiLabelFiltrationMode.Include => IsInFilterList(label), - LokiLabelFiltrationMode.Exclude => !IsInFilterList(label), - null => true, - _ => true - }; - - private bool IsInFilterList(string label) => _filtrationLabels.Contains(label); - } +using System.Text; +using Serilog.Debugging; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Sinks.Grafana.Loki.Models; +using Serilog.Sinks.Grafana.Loki.Utils; + +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// Formatter serializing batches of log events into a JSON object in the format, recognized by Grafana Loki. +/// +/// Example: +/// +/// { +/// "streams": [ +/// { +/// "stream": { +/// "label": "value" +/// }, +/// "values": [ +/// [ "unix epoch in nanoseconds", "log line" ], +/// [ "unix epoch in nanoseconds", "log line" ] +/// ] +/// } +/// ] +/// } +/// +/// +internal class LokiBatchFormatter : ILokiBatchFormatter +{ + private const int DefaultWriteBufferCapacity = 256; + + private readonly IEnumerable _globalLabels; + private readonly IReservedPropertyRenamingStrategy _renamingStrategy; + private readonly IEnumerable _propertiesAsLabels; + private readonly bool _useInternalTimestamp; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Renaming strategy for properties' names equal to reserved keywords. + /// + /// + /// + /// The list of global . + /// + /// + /// The list of properties, which would be mapped to the labels. + /// + /// + /// Compute internal timestamp + /// + public LokiBatchFormatter( + IReservedPropertyRenamingStrategy renamingStrategy, + IEnumerable? globalLabels = null, + IEnumerable? propertiesAsLabels = null, + bool useInternalTimestamp = false) + { + _renamingStrategy = renamingStrategy; + _globalLabels = globalLabels ?? Enumerable.Empty(); + _propertiesAsLabels = propertiesAsLabels ?? Enumerable.Empty(); + _useInternalTimestamp = useInternalTimestamp; + } + + /// + /// Format the log events into a payload. + /// + /// + /// The events to format wrapped in . + /// + /// + /// The formatter turning the log events into a textual representation. + /// + /// + /// The payload to send over the network. + /// + /// + /// Thrown if one of params is null. + /// + public void Format( + IReadOnlyCollection lokiLogEvents, + ITextFormatter formatter, + TextWriter output) + { + if (lokiLogEvents == null) + { + throw new ArgumentNullException(nameof(lokiLogEvents)); + } + + if (formatter == null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + if (output == null) + { + throw new ArgumentNullException(nameof(output)); + } + + if (lokiLogEvents.Count == 0) + { + return; + } + + var batch = new LokiBatch(); + + // Group logEvent by labels + var groups = lokiLogEvents + .Select(AddLevelAsPropertySafely) + .Select(GenerateLabels) + .GroupBy( + le => le.Labels, + le => le.LokiLogEvent, + DictionaryComparer.Instance); + + foreach (var group in groups) + { + var labels = group.Key; + var stream = batch.CreateStream(); + + foreach (var (key, value) in labels) + { + stream.AddLabel(key, value); + } + + foreach (var logEvent in group.OrderBy(x => _useInternalTimestamp ? x.InternalTimestamp : x.LogEvent.Timestamp)) + { + GenerateEntry( + logEvent, + formatter, + stream); + } + } + + if (batch.IsNotEmpty) + { + output.Write(batch.Serialize()); + } + + // Current behavior breaks rendering + // Log.Info("Hero's {level}", 42) + // Message: "Hero's \"info\"" + // level: "info" + // _level: 42 + LokiLogEvent AddLevelAsPropertySafely(LokiLogEvent lokiLogEvent) + { + var logEvent = lokiLogEvent.LogEvent; + logEvent.RenamePropertyIfPresent("level", _renamingStrategy.Rename); + logEvent.AddOrUpdateProperty( + new LogEventProperty("level", new ScalarValue(logEvent.Level.ToGrafanaLogLevel()))); + + return lokiLogEvent; + } + } + + private void GenerateEntry( + LokiLogEvent lokiLogEvent, + ITextFormatter formatter, + LokiStream stream) + { + var buffer = new StringWriter(new StringBuilder(DefaultWriteBufferCapacity)); + + var logEvent = lokiLogEvent.LogEvent; + var timestamp = logEvent.Timestamp; + + if (_useInternalTimestamp) + { + logEvent.AddPropertyIfAbsent( + new LogEventProperty("Timestamp", new ScalarValue(timestamp))); + timestamp = lokiLogEvent.InternalTimestamp; + } + + formatter.Format(logEvent, buffer); + + stream.AddEntry(timestamp, buffer.ToString().TrimEnd('\r', '\n')); + } + + private (Dictionary Labels, LokiLogEvent LokiLogEvent) GenerateLabels(LokiLogEvent lokiLogEvent) + { + var labels = _globalLabels.ToDictionary(label => label.Key, label => label.Value); + var properties = lokiLogEvent.Properties; + var (propertiesAsLabels, remainingProperties) = + properties.Partition(kvp => _propertiesAsLabels.Contains(kvp.Key)); + + foreach (var property in propertiesAsLabels) + { + var key = property.Key; + + // If a message template is a composite format string that contains indexed placeholders ({0}, {1} etc), + // Serilog turns these placeholders into event properties keyed by numeric strings. + // Loki doesn't accept such strings as label keys. Prefix these numeric strings with "param" + // to turn them into valid label keys and at the same time denote them as ordinal parameters. + if (char.IsDigit(key[0])) + { + key = $"param{key}"; + } + + // Some enrichers generates extra quotes and it breaks the payload + var value = property.Value.ToString().Replace("\"", string.Empty); + + if (labels.ContainsKey(key)) + { + SelfLog.WriteLine( + "Labels already contains key {0}, added from global labels. Property value ({1}) with the same key is ignored", + key, + value); + + continue; + } + + labels.Add(key, value); + } + + return (labels, + lokiLogEvent.CopyWithProperties(remainingProperties)); + } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/LokiCredentials.cs b/src/Serilog.Sinks.Grafana.Loki/LokiCredentials.cs index c371c6f..9dbed82 100644 --- a/src/Serilog.Sinks.Grafana.Loki/LokiCredentials.cs +++ b/src/Serilog.Sinks.Grafana.Loki/LokiCredentials.cs @@ -8,23 +8,22 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -namespace Serilog.Sinks.Grafana.Loki +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// Credentials used for Grafana Loki authorization +/// +public class LokiCredentials { /// - /// Credentials used for Grafana Loki authorization + /// Email or username /// - public class LokiCredentials - { - /// - /// Email or username - /// - public string Login { get; set; } = null!; + public string Login { get; set; } = null!; - /// - /// Password - /// - public string Password { get; set; } = null!; + /// + /// Password + /// + public string Password { get; set; } = null!; - internal bool IsEmpty => string.IsNullOrEmpty(Login) || string.IsNullOrEmpty(Password); - } + internal bool IsEmpty => string.IsNullOrEmpty(Login) || string.IsNullOrEmpty(Password); } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/LokiJsonTextFormatter.cs b/src/Serilog.Sinks.Grafana.Loki/LokiJsonTextFormatter.cs index 5dd78da..da2855b 100644 --- a/src/Serilog.Sinks.Grafana.Loki/LokiJsonTextFormatter.cs +++ b/src/Serilog.Sinks.Grafana.Loki/LokiJsonTextFormatter.cs @@ -8,216 +8,201 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Json; using Serilog.Parsing; using Serilog.Sinks.Grafana.Loki.Utils; -namespace Serilog.Sinks.Grafana.Loki +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// Used to serialize a log event to a json format that loki 2.0 can parse using the json parser ( | json ), more information can be found here https://grafana.com/blog/2020/10/28/loki-2.0-released-transform-logs-as-youre-querying-them-and-set-up-alerts-within-loki/ +/// +[SuppressMessage( + "ReSharper", + "PossibleMultipleEnumeration", + Justification = "Reviewed")] +public class LokiJsonTextFormatter : ITextFormatter { + private static readonly string[] ReservedKeywords = { "Message", "MessageTemplate", "Renderings", "Exception" }; + + private readonly IReservedPropertyRenamingStrategy _renamingStrategy; + private readonly JsonValueFormatter _valueFormatter; + /// - /// Used to serialize a log event to a json format that loki 2.0 can parse using the json parser ( | json ), more information can be found here https://grafana.com/blog/2020/10/28/loki-2.0-released-transform-logs-as-youre-querying-them-and-set-up-alerts-within-loki/ + /// Initializes a new instance of the class. + /// Uses . /// - [SuppressMessage( - "ReSharper", - "PossibleMultipleEnumeration", - Justification = "Reviewed")] - public class LokiJsonTextFormatter : ITextFormatter, ILabelAwareTextFormatter + public LokiJsonTextFormatter() + : this(new DefaultReservedPropertyRenamingStrategy()) { - private static readonly string[] ReservedKeywords = { "Message", "MessageTemplate", "Renderings", "level", "Exception" }; + } - private readonly JsonValueFormatter _valueFormatter; + /// + /// Initializes a new instance of the class. + /// + /// + /// Renaming strategy for properties names equal to reserved keywords. + /// + /// + public LokiJsonTextFormatter(IReservedPropertyRenamingStrategy renamingStrategy) + { + _renamingStrategy = renamingStrategy; + _valueFormatter = new JsonValueFormatter("$type"); + } - /// - /// Initializes a new instance of the class. - /// - public LokiJsonTextFormatter() + /// + /// Format the log event into the output. + /// + /// + /// The event to format. + /// + /// + /// The output. + /// + public void Format(LogEvent logEvent, TextWriter output) + { + if (logEvent == null) { - _valueFormatter = new JsonValueFormatter("$type"); + throw new ArgumentNullException(nameof(logEvent)); } - /// - public bool ExcludeLevelLabel => true; - - /// - /// Format the log event into the output. - /// - /// - /// The event to format. - /// - /// - /// The output. - /// - /// - /// List of labels that should not be written as json fields. - /// - public void Format( - LogEvent logEvent, - TextWriter output, - IEnumerable labels) + if (output == null) { - if (logEvent == null) - { - throw new ArgumentNullException(nameof(logEvent)); - } - - if (output == null) - { - throw new ArgumentNullException(nameof(output)); - } + throw new ArgumentNullException(nameof(output)); + } - output.Write("{\"Message\":"); - JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Render(logEvent.Properties), output); + output.Write("{\"Message\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Render(logEvent.Properties), output); - output.Write(",\"MessageTemplate\":"); - JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output); + output.Write(",\"MessageTemplate\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output); - var tokensWithFormat = logEvent.MessageTemplate.Tokens - .OfType() - .Where(pt => pt.Format != null); + var tokensWithFormat = logEvent.MessageTemplate.Tokens + .OfType() + .Where(pt => pt.Format != null); - // Better not to allocate an array in the 99.9% of cases where this is false - if (tokensWithFormat.Any()) + // Better not to allocate an array in the 99.9% of cases where this is false + if (tokensWithFormat.Any()) + { + output.Write(",\"Renderings\":["); + var delimiter = string.Empty; + foreach (var r in tokensWithFormat) { - output.Write(",\"Renderings\":["); - var delimiter = string.Empty; - foreach (var r in tokensWithFormat) - { - output.Write(delimiter); - delimiter = ","; - var space = new StringWriter(); - r.Render(logEvent.Properties, space); - JsonValueFormatter.WriteQuotedJsonString(space.ToString(), output); - } - - output.Write(']'); + output.Write(delimiter); + delimiter = ","; + var space = new StringWriter(); + r.Render(logEvent.Properties, space); + JsonValueFormatter.WriteQuotedJsonString(space.ToString(), output); } - output.Write(",\"level\":\""); - output.Write(logEvent.Level.ToGrafanaLogLevel()); - output.Write('\"'); + output.Write(']'); + } - if (logEvent.Exception != null) - { - output.Write(",\"Exception\":"); - SerializeException( - output, - logEvent.Exception, - 1); - } + if (logEvent.Exception != null) + { + output.Write(",\"Exception\":"); + SerializeException( + output, + logEvent.Exception, + 1); + } - foreach (var property in logEvent.Properties) - { - var name = GetSanitizedPropertyName(property.Key); - if (labels.Contains(name)) - { - continue; - } + foreach (var (key, value) in logEvent.Properties) + { + var name = GetSanitizedPropertyName(key); + output.Write(','); + JsonValueFormatter.WriteQuotedJsonString(name, output); + output.Write(':'); + _valueFormatter.Format(value, output); + } - output.Write(','); - JsonValueFormatter.WriteQuotedJsonString(name, output); - output.Write(':'); - _valueFormatter.Format(property.Value, output); - } + output.Write('}'); + } - output.Write('}'); - } + /// + /// Used to sanitize property name to avoid conflict with reserved keywords. + /// Appends _ to the property name if it matches with reserved keyword. + /// + /// + /// Name of property to sanitize + /// + protected virtual string GetSanitizedPropertyName(string propertyName) => + ReservedKeywords.Contains(propertyName) ? _renamingStrategy.Rename(propertyName) : propertyName; - /// - [Obsolete("Use \"Format(LogEvent logEvent, TextWriter output, IEnumerable labels)\" instead!")] - public void Format(LogEvent logEvent, TextWriter output) => Format( - logEvent, - output, - Enumerable.Empty()); - - /// - /// Used to sanitize property name to avoid conflict with reserved keywords. - /// Appends _ to the property name if it matches with reserved keyword. - /// - /// - /// Name of property to sanitize - /// - protected virtual string GetSanitizedPropertyName(string propertyName) => - ReservedKeywords.Contains(propertyName) ? $"_{propertyName}" : propertyName; - - /// - /// Used to serialize exceptions, can be overridden when inheriting to change the format. - /// - /// - /// The output. - /// - /// - /// The exception to format. - /// - /// - /// The current nesting level of the exception. - /// - protected virtual void SerializeException( - TextWriter output, - Exception exception, - int level) + /// + /// Used to serialize exceptions, can be overridden when inheriting to change the format. + /// + /// + /// The output. + /// + /// + /// The exception to format. + /// + /// + /// The current nesting level of the exception. + /// + protected virtual void SerializeException( + TextWriter output, + Exception exception, + int level) + { + if (level == 4) { - if (level == 4) - { - JsonValueFormatter.WriteQuotedJsonString(exception.ToString(), output); - - return; - } + JsonValueFormatter.WriteQuotedJsonString(exception.ToString(), output); - output.Write("{\"Type\":"); - var typeNamespace = exception.GetType().Namespace; - var typeName = typeNamespace != null && typeNamespace.StartsWith("System.") - ? exception.GetType().Name - : exception.GetType().ToString(); - JsonValueFormatter.WriteQuotedJsonString(typeName, output); + return; + } - if (!string.IsNullOrWhiteSpace(exception.Message)) - { - output.Write(",\"Message\":"); - JsonValueFormatter.WriteQuotedJsonString(exception.Message, output); - } + output.Write("{\"Type\":"); + var typeNamespace = exception.GetType().Namespace; + var typeName = typeNamespace != null && typeNamespace.StartsWith("System.") + ? exception.GetType().Name + : exception.GetType().ToString(); + JsonValueFormatter.WriteQuotedJsonString(typeName, output); - if (!string.IsNullOrWhiteSpace(exception.StackTrace)) - { - output.Write(",\"StackTrace\":"); - JsonValueFormatter.WriteQuotedJsonString(exception.StackTrace, output); - } + if (!string.IsNullOrWhiteSpace(exception.Message)) + { + output.Write(",\"Message\":"); + JsonValueFormatter.WriteQuotedJsonString(exception.Message, output); + } - if (exception is AggregateException aggregateException) - { - output.Write(",\"InnerExceptions\":["); - var count = aggregateException.InnerExceptions.Count; - for (var i = 0; i < count; i++) - { - var isLast = i == count - 1; - SerializeException( - output, - aggregateException.InnerExceptions[i], - level + 1); - if (!isLast) - { - output.Write(','); - } - } + if (!string.IsNullOrWhiteSpace(exception.StackTrace)) + { + output.Write(",\"StackTrace\":"); + JsonValueFormatter.WriteQuotedJsonString(exception.StackTrace, output); + } - output.Write("]"); - } - else if (exception.InnerException != null) + if (exception is AggregateException aggregateException) + { + output.Write(",\"InnerExceptions\":["); + var count = aggregateException.InnerExceptions.Count; + for (var i = 0; i < count; i++) { - output.Write(",\"InnerException\":"); + var isLast = i == count - 1; SerializeException( output, - exception.InnerException, + aggregateException.InnerExceptions[i], level + 1); + if (!isLast) + { + output.Write(','); + } } - output.Write('}'); + output.Write("]"); } + else if (exception.InnerException != null) + { + output.Write(",\"InnerException\":"); + SerializeException( + output, + exception.InnerException, + level + 1); + } + + output.Write('}'); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/LokiLabel.cs b/src/Serilog.Sinks.Grafana.Loki/LokiLabel.cs index a4fe26a..c0dd2e2 100644 --- a/src/Serilog.Sinks.Grafana.Loki/LokiLabel.cs +++ b/src/Serilog.Sinks.Grafana.Loki/LokiLabel.cs @@ -8,21 +8,20 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -namespace Serilog.Sinks.Grafana.Loki +namespace Serilog.Sinks.Grafana.Loki; + +/// +/// Label used for enriching log entries in Grafana Loki +/// +public class LokiLabel { /// - /// Label used for enriching log entries in Grafana Loki + /// Label's name /// - public class LokiLabel - { - /// - /// Label's name - /// - public string Key { get; set; } = null!; + public string Key { get; set; } = null!; - /// - /// Label's value - /// - public string Value { get; set; } = null!; - } + /// + /// Label's value + /// + public string Value { get; set; } = null!; } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/LokiSink.cs b/src/Serilog.Sinks.Grafana.Loki/LokiSink.cs index cf9d736..bbf3883 100644 --- a/src/Serilog.Sinks.Grafana.Loki/LokiSink.cs +++ b/src/Serilog.Sinks.Grafana.Loki/LokiSink.cs @@ -8,171 +8,166 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; using Serilog.Core; using Serilog.Debugging; using Serilog.Events; using Serilog.Formatting; using Serilog.Sinks.Grafana.Loki.Infrastructure; +using Serilog.Sinks.Grafana.Loki.Models; using Serilog.Sinks.Grafana.Loki.Utils; -namespace Serilog.Sinks.Grafana.Loki +namespace Serilog.Sinks.Grafana.Loki; + +internal class LokiSink : ILogEventSink, IDisposable { - internal class LokiSink : ILogEventSink, IDisposable + private readonly string _requestUri; + private readonly int _batchPostingLimit; + private readonly ITextFormatter _textFormatter; + private readonly ILokiBatchFormatter _batchFormatter; + private readonly ILokiHttpClient _httpClient; + private readonly ExponentialBackoffConnectionSchedule _connectionSchedule; + private readonly object _syncRoot = new(); + private readonly PortableTimer _timer; + private readonly BoundedQueue _queue; + private readonly Queue _waitingBatch = new(); + + private bool _isDisposed; + + public LokiSink( + string requestUri, + int batchPostingLimit, + int? queueLimit, + TimeSpan period, + ITextFormatter textFormatter, + ILokiBatchFormatter batchFormatter, + ILokiHttpClient httpClient) { - private readonly string _requestUri; - private readonly int _batchPostingLimit; - private readonly ITextFormatter _textFormatter; - private readonly ILokiBatchFormatter _batchFormatter; - private readonly ILokiHttpClient _httpClient; - private readonly ExponentialBackoffConnectionSchedule _connectionSchedule; - private readonly object _syncRoot = new(); - private readonly PortableTimer _timer; - private readonly BoundedQueue _queue; - private readonly Queue<(LogEvent LogEntry, DateTimeOffset Timestamp)> _waitingBatch = new(); - - private bool _isDisposed; - - public LokiSink( - string requestUri, - int batchPostingLimit, - int? queueLimit, - TimeSpan period, - ITextFormatter textFormatter, - ILokiBatchFormatter batchFormatter, - ILokiHttpClient httpClient) - { - _requestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri)); - _batchPostingLimit = batchPostingLimit; - _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); - _batchFormatter = batchFormatter ?? throw new ArgumentNullException(nameof(batchFormatter)); - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _requestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri)); + _batchPostingLimit = batchPostingLimit; + _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter)); + _batchFormatter = batchFormatter ?? throw new ArgumentNullException(nameof(batchFormatter)); + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _connectionSchedule = new ExponentialBackoffConnectionSchedule(period); - _timer = new PortableTimer(OnTick); - _queue = new BoundedQueue(queueLimit); + _connectionSchedule = new ExponentialBackoffConnectionSchedule(period); + _timer = new PortableTimer(OnTick); + _queue = new BoundedQueue(queueLimit); - SetTimer(); - } + SetTimer(); + } - public void Emit(LogEvent logEvent) + public void Emit(LogEvent logEvent) + { + if (logEvent == null) { - if (logEvent == null) - { - throw new ArgumentNullException(nameof(logEvent)); - } + throw new ArgumentNullException(nameof(logEvent)); + } - if (!_queue.TryEnqueue(logEvent)) - { - SelfLog.WriteLine("Queue has reached it's limit and the log event {@Event} will be dropped", logEvent); - } + if (!_queue.TryEnqueue(new LokiLogEvent(logEvent))) + { + SelfLog.WriteLine("Queue has reached it's limit and the log event {@Event} will be dropped", logEvent); } + } - public void Dispose() + public void Dispose() + { + lock (_syncRoot) { - lock (_syncRoot) + if (_isDisposed) { - if (_isDisposed) - { - return; - } - - _isDisposed = true; + return; } - _timer.Dispose(); - OnTick().GetAwaiter().GetResult(); - _httpClient.Dispose(); + _isDisposed = true; } - private async Task OnTick() + _timer.Dispose(); + OnTick().GetAwaiter().GetResult(); + _httpClient.Dispose(); + } + + private async Task OnTick() + { + try { - try - { - bool batchWasFull; + bool batchWasFull; - do + do + { + while (_waitingBatch.Count < _batchPostingLimit && _queue.TryDequeue(out var next)) { - while (_waitingBatch.Count < _batchPostingLimit && _queue.TryDequeue(out var next)) - { - _waitingBatch.Enqueue(((LogEvent LogEntry, DateTimeOffset Timestamp))(next!)); - } + _waitingBatch.Enqueue(next!); + } - batchWasFull = _waitingBatch.Count >= _batchPostingLimit; + batchWasFull = _waitingBatch.Count >= _batchPostingLimit; - if (_waitingBatch.Count > 0) - { - HttpResponseMessage response; + if (_waitingBatch.Count > 0) + { + HttpResponseMessage response; - using (var contentStream = new MemoryStream()) + using (var contentStream = new MemoryStream()) + { + using (var contentWriter = new StreamWriter(contentStream, Encoding.UTF8WithoutBom)) { - using (var contentWriter = new StreamWriter(contentStream, Encoding.UTF8WithoutBom)) + _batchFormatter.Format(_waitingBatch, _textFormatter, contentWriter); + await contentWriter.FlushAsync(); + contentStream.Position = 0; + + if (contentStream.Length == 0) { - _batchFormatter.Format(_waitingBatch, _textFormatter, contentWriter); - await contentWriter.FlushAsync(); - contentStream.Position = 0; - - if (contentStream.Length == 0) - { - continue; - } - - response = await _httpClient - .PostAsync(_requestUri, contentStream) - .ConfigureAwait(false); + continue; } - } - if (response.IsSuccessStatusCode) - { - _connectionSchedule.MarkSuccess(); - _waitingBatch.Clear(); + response = await _httpClient + .PostAsync(_requestUri, contentStream) + .ConfigureAwait(false); } - else - { - SelfLog.WriteLine( - "Received failure on HTTP shipping ({0}): {1}. {2} log events will be dropped", - (int)response.StatusCode, - await response.Content.ReadAsStringAsync().ConfigureAwait(false), - _waitingBatch.Count); - - _connectionSchedule.MarkFailure(); - _waitingBatch.Clear(); + } - break; - } + if (response.IsSuccessStatusCode) + { + _connectionSchedule.MarkSuccess(); + _waitingBatch.Clear(); } else { - _connectionSchedule.MarkSuccess(); + SelfLog.WriteLine( + "Received failure on HTTP shipping ({0}): {1}. {2} log events will be dropped", + (int)response.StatusCode, + await response.Content.ReadAsStringAsync().ConfigureAwait(false), + _waitingBatch.Count); + + _connectionSchedule.MarkFailure(); + _waitingBatch.Clear(); + + break; } } - while (batchWasFull); - } - catch (Exception ex) - { - SelfLog.WriteLine("Exception while emitting periodic batch from {0}: {1}", this, ex); - _connectionSchedule.MarkFailure(); + else + { + _connectionSchedule.MarkSuccess(); + } } - finally + while (batchWasFull); + } + catch (Exception ex) + { + SelfLog.WriteLine("Exception while emitting periodic batch from {0}: {1}", this, ex); + _connectionSchedule.MarkFailure(); + } + finally + { + lock (_syncRoot) { - lock (_syncRoot) + if (!_isDisposed) { - if (!_isDisposed) - { - SetTimer(); - } + SetTimer(); } } } + } - private void SetTimer() - { - _timer.Start(_connectionSchedule.NextInterval); - } + private void SetTimer() + { + _timer.Start(_connectionSchedule.NextInterval); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Models/LokiBatch.cs b/src/Serilog.Sinks.Grafana.Loki/Models/LokiBatch.cs index e57002c..d3435be 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Models/LokiBatch.cs +++ b/src/Serilog.Sinks.Grafana.Loki/Models/LokiBatch.cs @@ -8,27 +8,25 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; -namespace Serilog.Sinks.Grafana.Loki.Models -{ - internal class LokiBatch - { - [JsonPropertyName("streams")] - public IList Streams { get; } = new List(); +namespace Serilog.Sinks.Grafana.Loki.Models; - [JsonIgnore] - public bool IsNotEmpty => Streams.Count > 0; +internal class LokiBatch +{ + [JsonPropertyName("streams")] + public IList Streams { get; } = new List(); - public LokiStream CreateStream() - { - var stream = new LokiStream(); - Streams.Add(stream); - return stream; - } + [JsonIgnore] + public bool IsNotEmpty => Streams.Count > 0; - public string Serialize() => JsonSerializer.Serialize(this); + public LokiStream CreateStream() + { + var stream = new LokiStream(); + Streams.Add(stream); + return stream; } + + public string Serialize() => JsonSerializer.Serialize(this); } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Models/LokiLogEvent.cs b/src/Serilog.Sinks.Grafana.Loki/Models/LokiLogEvent.cs new file mode 100644 index 0000000..c107ee3 --- /dev/null +++ b/src/Serilog.Sinks.Grafana.Loki/Models/LokiLogEvent.cs @@ -0,0 +1,60 @@ +// Copyright 2020-2022 Mykhailo Shevchuk & Contributors +// +// Licensed under the MIT license; +// you may not use this file except in compliance with the License. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See LICENSE file in the project root for full license information. + +using Serilog.Events; + +namespace Serilog.Sinks.Grafana.Loki.Models; + +/// +/// A wrapped log event. +/// Contains and sink's internal timestamp. +/// is created when event is emitted to the sink. +/// +public class LokiLogEvent +{ + /// + /// Creates from . + /// + /// + /// A log event. + /// + public LokiLogEvent(LogEvent logEvent) + { + InternalTimestamp = DateTimeOffset.Now; + LogEvent = logEvent; + } + + /// + /// Internal event timestamp, created when event is emitted to the sink. + /// + public DateTimeOffset InternalTimestamp { get; } + + /// + /// A log event. + /// + public LogEvent LogEvent { get; private set; } + + /// + /// Properties associated with the event. + /// + public IReadOnlyDictionary Properties => LogEvent.Properties; + + internal LokiLogEvent CopyWithProperties(IEnumerable> properties) + { + LogEvent = new LogEvent( + LogEvent.Timestamp, + LogEvent.Level, + LogEvent.Exception, + LogEvent.MessageTemplate, + properties.Select(p => new LogEventProperty(p.Key, p.Value))); + + return this; + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Models/LokiStream.cs b/src/Serilog.Sinks.Grafana.Loki/Models/LokiStream.cs index f016be3..0492910 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Models/LokiStream.cs +++ b/src/Serilog.Sinks.Grafana.Loki/Models/LokiStream.cs @@ -8,29 +8,26 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; -using System.Collections.Generic; using System.Text.Json.Serialization; using Serilog.Sinks.Grafana.Loki.Utils; -namespace Serilog.Sinks.Grafana.Loki.Models +namespace Serilog.Sinks.Grafana.Loki.Models; + +internal class LokiStream { - internal class LokiStream - { - [JsonPropertyName("stream")] - public Dictionary Labels { get; } = new(); + [JsonPropertyName("stream")] + public Dictionary Labels { get; } = new(); - [JsonPropertyName("values")] - public IList> Entries { get; set; } = new List>(); + [JsonPropertyName("values")] + public IList> Entries { get; set; } = new List>(); - public void AddLabel(string key, string value) - { - Labels[key] = value; - } + public void AddLabel(string key, string value) + { + Labels[key] = value; + } - public void AddEntry(DateTimeOffset timestamp, string entry) - { - Entries.Add(new[] {timestamp.ToUnixNanosecondsString(), entry}); - } + public void AddEntry(DateTimeOffset timestamp, string entry) + { + Entries.Add(new[] {timestamp.ToUnixNanosecondsString(), entry}); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Serilog.Sinks.Grafana.Loki.csproj b/src/Serilog.Sinks.Grafana.Loki/Serilog.Sinks.Grafana.Loki.csproj index 2ac9987..22d787b 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Serilog.Sinks.Grafana.Loki.csproj +++ b/src/Serilog.Sinks.Grafana.Loki/Serilog.Sinks.Grafana.Loki.csproj @@ -6,6 +6,7 @@ true netstandard2.0 enable + enable true embedded diff --git a/src/Serilog.Sinks.Grafana.Loki/Utils/DateTimeOffsetExtensions.cs b/src/Serilog.Sinks.Grafana.Loki/Utils/DateTimeOffsetExtensions.cs index d1700cb..bc0fe47 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Utils/DateTimeOffsetExtensions.cs +++ b/src/Serilog.Sinks.Grafana.Loki/Utils/DateTimeOffsetExtensions.cs @@ -8,13 +8,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -using System; +namespace Serilog.Sinks.Grafana.Loki.Utils; -namespace Serilog.Sinks.Grafana.Loki.Utils +internal static class DateTimeOffsetExtensions { - internal static class DateTimeOffsetExtensions - { - internal static string ToUnixNanosecondsString(this DateTimeOffset offset) => - (offset.ToUnixTimeMilliseconds() * 1000000).ToString(); - } + internal static string ToUnixNanosecondsString(this DateTimeOffset offset) => + (offset.ToUnixTimeMilliseconds() * 1000000).ToString(); } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Utils/Encoding.cs b/src/Serilog.Sinks.Grafana.Loki/Utils/Encoding.cs index 125274b..31da406 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Utils/Encoding.cs +++ b/src/Serilog.Sinks.Grafana.Loki/Utils/Encoding.cs @@ -11,11 +11,10 @@ using System.Diagnostics.CodeAnalysis; using System.Text; -namespace Serilog.Sinks.Grafana.Loki.Utils +namespace Serilog.Sinks.Grafana.Loki.Utils; + +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "By design.")] +internal static class Encoding { - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "By design.")] - internal static class Encoding - { - public static readonly System.Text.Encoding UTF8WithoutBom = new UTF8Encoding(false); - } + public static readonly System.Text.Encoding UTF8WithoutBom = new UTF8Encoding(false); } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Utils/EnumerableExtensions.cs b/src/Serilog.Sinks.Grafana.Loki/Utils/EnumerableExtensions.cs new file mode 100644 index 0000000..df8f2b4 --- /dev/null +++ b/src/Serilog.Sinks.Grafana.Loki/Utils/EnumerableExtensions.cs @@ -0,0 +1,48 @@ +// Copyright 2020-2022 Mykhailo Shevchuk & Contributors +// +// Licensed under the MIT license; +// you may not use this file except in compliance with the License. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See LICENSE file in the project root for full license information. + +namespace Serilog.Sinks.Grafana.Loki.Utils; + +internal static class EnumerableExtensions +{ + public static (IEnumerable Matched, IEnumerable Unmatched) Partition( + this IEnumerable source, + Func predicate) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + var matched = new List(); + var unmatched = new List(); + + foreach (var item in source) + { + (predicate(item) ? matched : unmatched).Add(item); + } + + return (matched, unmatched); + } + + public static void Deconstruct( + this KeyValuePair kvp, + out TKey key, + out TValue value) + { + key = kvp.Key; + value = kvp.Value; + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Utils/LogEventExtensions.cs b/src/Serilog.Sinks.Grafana.Loki/Utils/LogEventExtensions.cs new file mode 100644 index 0000000..44c7968 --- /dev/null +++ b/src/Serilog.Sinks.Grafana.Loki/Utils/LogEventExtensions.cs @@ -0,0 +1,45 @@ +// Copyright 2020-2022 Mykhailo Shevchuk & Contributors +// +// Licensed under the MIT license; +// you may not use this file except in compliance with the License. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See LICENSE file in the project root for full license information. + +using Serilog.Events; + +namespace Serilog.Sinks.Grafana.Loki.Utils; + +internal static class LogEventExtensions +{ + /// + /// Renames property in the event, if present. + /// Otherwise no action is performed. + /// Calls itself recursively to check if new name is free. + /// If it is taken by other property - renames it according to strategy. + /// + /// + /// Log Event. + /// + /// + /// Property name to be renamed. + /// + /// + /// Renaming strategy. + /// + internal static void RenamePropertyIfPresent( + this LogEvent logEvent, + string propertyName, + Func renamingStrategy) + { + if (logEvent.Properties.TryGetValue(propertyName, out var value)) + { + var newName = renamingStrategy(propertyName); + logEvent.RemovePropertyIfPresent(propertyName); + logEvent.RenamePropertyIfPresent(newName, renamingStrategy); + logEvent.AddOrUpdateProperty(new LogEventProperty(newName, value)); + } + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Utils/LogEventLevelExtensions.cs b/src/Serilog.Sinks.Grafana.Loki/Utils/LogEventLevelExtensions.cs index 0248d8e..a304a62 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Utils/LogEventLevelExtensions.cs +++ b/src/Serilog.Sinks.Grafana.Loki/Utils/LogEventLevelExtensions.cs @@ -10,21 +10,20 @@ using Serilog.Events; -namespace Serilog.Sinks.Grafana.Loki.Utils +namespace Serilog.Sinks.Grafana.Loki.Utils; + +internal static class LogEventLevelExtensions { - internal static class LogEventLevelExtensions - { - // TODO: After the release 7.0.0 Grafana will determine log level fatal, so mapping for that level will be redundant - internal static string ToGrafanaLogLevel(this LogEventLevel level) => - level switch - { - LogEventLevel.Verbose => "trace", - LogEventLevel.Debug => "debug", - LogEventLevel.Information => "info", - LogEventLevel.Warning => "warning", - LogEventLevel.Error => "error", - LogEventLevel.Fatal => "critical", - _ => "unknown" - }; - } + // TODO: After the release 7.0.0 Grafana will determine log level fatal, so mapping for that level will be redundant + internal static string ToGrafanaLogLevel(this LogEventLevel level) => + level switch + { + LogEventLevel.Verbose => "trace", + LogEventLevel.Debug => "debug", + LogEventLevel.Information => "info", + LogEventLevel.Warning => "warning", + LogEventLevel.Error => "error", + LogEventLevel.Fatal => "critical", + _ => "unknown" + }; } \ No newline at end of file diff --git a/src/Serilog.Sinks.Grafana.Loki/Utils/LokiRoutesBuilder.cs b/src/Serilog.Sinks.Grafana.Loki/Utils/LokiRoutesBuilder.cs index 4a44331..ec78485 100644 --- a/src/Serilog.Sinks.Grafana.Loki/Utils/LokiRoutesBuilder.cs +++ b/src/Serilog.Sinks.Grafana.Loki/Utils/LokiRoutesBuilder.cs @@ -8,12 +8,11 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See LICENSE file in the project root for full license information. -namespace Serilog.Sinks.Grafana.Loki.Utils +namespace Serilog.Sinks.Grafana.Loki.Utils; + +internal static class LokiRoutesBuilder { - internal static class LokiRoutesBuilder - { - private const string LogEntriesEndpoint = "/loki/api/v1/push"; + private const string LogEntriesEndpoint = "/loki/api/v1/push"; - public static string BuildLogsEntriesRoute(string host) => $"{host.TrimEnd('/')}{LogEntriesEndpoint}"; - } + public static string BuildLogsEntriesRoute(string host) => $"{host.TrimEnd('/')}{LogEntriesEndpoint}"; } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/HttpClientsTests/BaseLokiHttpClientTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/HttpClientsTests/BaseLokiHttpClientTests.cs index 45d8ed6..4e2c1ac 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/HttpClientsTests/BaseLokiHttpClientTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/HttpClientsTests/BaseLokiHttpClientTests.cs @@ -1,52 +1,50 @@ -using System.Net.Http; -using Serilog.Sinks.Grafana.Loki.Tests.TestHelpers; +using Serilog.Sinks.Grafana.Loki.Tests.TestHelpers; using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.HttpClientsTests +namespace Serilog.Sinks.Grafana.Loki.Tests.HttpClientsTests; + +public class BaseLokiHttpClientTests { - public class BaseLokiHttpClientTests + [Fact] + public void ProvidedHttpClientShouldBeUsed() { - [Fact] - public void ProvidedHttpClientShouldBeUsed() - { - using var httpClient = new HttpClient(); + using var httpClient = new HttpClient(); - using var client = new TestLokiHttpClient(httpClient); + using var client = new TestLokiHttpClient(httpClient); - client.Client.ShouldBe(httpClient); - } + client.Client.ShouldBe(httpClient); + } - [Fact] - public void HttpClientShouldBeCreatedIfNotProvider() - { - using var client = new TestLokiHttpClient(); + [Fact] + public void HttpClientShouldBeCreatedIfNotProvider() + { + using var client = new TestLokiHttpClient(); - client.Client.ShouldNotBeNull(); - } + client.Client.ShouldNotBeNull(); + } - [Fact] - public void BasicAuthHeaderShouldBeCorrect() - { - var credentials = new LokiCredentials {Login = "Billy", Password = "Herrington"}; - using var client = new TestLokiHttpClient(); + [Fact] + public void BasicAuthHeaderShouldBeCorrect() + { + var credentials = new LokiCredentials {Login = "Billy", Password = "Herrington"}; + using var client = new TestLokiHttpClient(); - client.SetCredentials(credentials); + client.SetCredentials(credentials); - var authorization = client.Client.DefaultRequestHeaders.Authorization; - authorization.ShouldSatisfyAllConditions( - () => authorization!.Scheme.ShouldBe("Basic"), - () => authorization!.Parameter.ShouldBe("QmlsbHk6SGVycmluZ3Rvbg==")); - } + var authorization = client.Client.DefaultRequestHeaders.Authorization; + authorization.ShouldSatisfyAllConditions( + () => authorization!.Scheme.ShouldBe("Basic"), + () => authorization!.Parameter.ShouldBe("QmlsbHk6SGVycmluZ3Rvbg==")); + } - [Fact] - public void AuthorizationHeaderShouldNotBeSetWithoutCredentials() - { - using var client = new TestLokiHttpClient(); + [Fact] + public void AuthorizationHeaderShouldNotBeSetWithoutCredentials() + { + using var client = new TestLokiHttpClient(); - client.SetCredentials(null); + client.SetCredentials(null); - client.Client.DefaultRequestHeaders.Authorization.ShouldBeNull(); - } + client.Client.DefaultRequestHeaders.Authorization.ShouldBeNull(); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/BoundedQueueTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/BoundedQueueTests.cs index bd1652e..232c70f 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/BoundedQueueTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/BoundedQueueTests.cs @@ -1,67 +1,65 @@ -using System; -using Serilog.Sinks.Grafana.Loki.Infrastructure; +using Serilog.Sinks.Grafana.Loki.Infrastructure; using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.InfrastructureTests +namespace Serilog.Sinks.Grafana.Loki.Tests.InfrastructureTests; + +public class BoundedQueueTests { - public class BoundedQueueTests + [Fact] + public void QueueShouldThrowExceptionForNegativeQueueLimit() { - [Fact] - public void QueueShouldThrowExceptionForNegativeQueueLimit() - { - Should - .Throw(() => new BoundedQueue(-42)) - .Message.ShouldBe("Queue limit must be positive, or `null` to indicate unbounded (Parameter 'queueLimit')"); - } + Should + .Throw(() => new BoundedQueue(-42)) + .Message.ShouldBe("Queue limit must be positive, or `null` to indicate unbounded (Parameter 'queueLimit')"); + } - [Fact] - public void QueueShouldUseFifo() - { - var queue = new BoundedQueue(null); + [Fact] + public void QueueShouldUseFifo() + { + var queue = new BoundedQueue(null); - var enqueueResult1 = queue.TryEnqueue(1); - var enqueueResult2 = queue.TryEnqueue(2); - var enqueueResult3 = queue.TryEnqueue(3); + var enqueueResult1 = queue.TryEnqueue(1); + var enqueueResult2 = queue.TryEnqueue(2); + var enqueueResult3 = queue.TryEnqueue(3); - var dequeueResult1 = queue.TryDequeue(out var dequeueItem1); - var dequeueResult2 = queue.TryDequeue(out var dequeueItem2); - var dequeueResult3 = queue.TryDequeue(out var dequeueItem3); + var dequeueResult1 = queue.TryDequeue(out var dequeueItem1); + var dequeueResult2 = queue.TryDequeue(out var dequeueItem2); + var dequeueResult3 = queue.TryDequeue(out var dequeueItem3); - enqueueResult1.ShouldBeTrue(); - enqueueResult2.ShouldBeTrue(); - enqueueResult3.ShouldBeTrue(); + enqueueResult1.ShouldBeTrue(); + enqueueResult2.ShouldBeTrue(); + enqueueResult3.ShouldBeTrue(); - dequeueResult1.ShouldBeTrue(); - dequeueResult2.ShouldBeTrue(); - dequeueResult3.ShouldBeTrue(); + dequeueResult1.ShouldBeTrue(); + dequeueResult2.ShouldBeTrue(); + dequeueResult3.ShouldBeTrue(); - dequeueItem1!.Value.Event.ShouldBe(1); - dequeueItem2!.Value.Event.ShouldBe(2); - dequeueItem3!.Value.Event.ShouldBe(3); - } + dequeueItem1.ShouldBe(1); + dequeueItem2.ShouldBe(2); + dequeueItem3.ShouldBe(3); + } - [Fact] - public void QueueShouldNotEnqueueFullQueue() - { - var queue = new BoundedQueue(1); + [Fact] + public void QueueShouldNotEnqueueFullQueue() + { + var queue = new BoundedQueue(1); - var enqueueResult1 = queue.TryEnqueue(1); - var enqueueResult2 = queue.TryEnqueue(2); + var enqueueResult1 = queue.TryEnqueue(1); + var enqueueResult2 = queue.TryEnqueue(2); - enqueueResult1.ShouldBeTrue(); - enqueueResult2.ShouldBeFalse(); - } + enqueueResult1.ShouldBeTrue(); + enqueueResult2.ShouldBeFalse(); + } - [Fact] - public void QueueShouldNotDequeueEmptyQueue() - { - var queue = new BoundedQueue(null); + [Fact] + public void QueueShouldNotDequeueEmptyQueue() + { + var queue = new BoundedQueue(null); - var result = queue.TryDequeue(out var item); + var result = queue.TryDequeue(out var item); - result.ShouldBeFalse(); - item.ShouldBe(default); - } + result.ShouldBeFalse(); + item.ShouldBe(default); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/ExponentialBackoffConnectionScheduleTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/ExponentialBackoffConnectionScheduleTests.cs index 489aba9..6f6ba41 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/ExponentialBackoffConnectionScheduleTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/ExponentialBackoffConnectionScheduleTests.cs @@ -1,102 +1,100 @@ -using System; -using Serilog.Sinks.Grafana.Loki.Infrastructure; +using Serilog.Sinks.Grafana.Loki.Infrastructure; using Serilog.Sinks.Grafana.Loki.Tests.TestHelpers.Backoff; using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.InfrastructureTests +namespace Serilog.Sinks.Grafana.Loki.Tests.InfrastructureTests; + +public class ExponentialBackoffConnectionScheduleTests { - public class ExponentialBackoffConnectionScheduleTests + [Theory] + [InlineData(1)] // 1s + [InlineData(2)] // 2s + [InlineData(5)] // 5s + [InlineData(10)] // 10s + [InlineData(30)] // 30s + [InlineData(1 * 60)] // 1 min + [InlineData(5 * 60)] // 5 min + [InlineData(10 * 60)] // 10 min + public void SchedulerShouldReturnPeriodOnSuccess(int periodInSeconds) { - [Theory] - [InlineData(1)] // 1s - [InlineData(2)] // 2s - [InlineData(5)] // 5s - [InlineData(10)] // 10s - [InlineData(30)] // 30s - [InlineData(1 * 60)] // 1 min - [InlineData(5 * 60)] // 5 min - [InlineData(10 * 60)] // 10 min - public void SchedulerShouldReturnPeriodOnSuccess(int periodInSeconds) - { - var expected = TimeSpan.FromSeconds(periodInSeconds); - var schedule = new ExponentialBackoffConnectionSchedule(expected); + var expected = TimeSpan.FromSeconds(periodInSeconds); + var schedule = new ExponentialBackoffConnectionSchedule(expected); - var actual = schedule.NextInterval; + var actual = schedule.NextInterval; - actual.ShouldBe(expected); - } + actual.ShouldBe(expected); + } - [Theory] - [InlineData(1)] // 1s - [InlineData(2)] // 2s - [InlineData(5)] // 5s - [InlineData(10)] // 10s - [InlineData(30)] // 30s - [InlineData(1 * 60)] // 1 min - [InlineData(5 * 60)] // 5 min - [InlineData(10 * 60)] // 10 min - public void SchedulerShouldReturnPeriodAfterFirstFailure(int periodInSeconds) - { - var expected = TimeSpan.FromSeconds(periodInSeconds); - var schedule = new ExponentialBackoffConnectionSchedule(expected); + [Theory] + [InlineData(1)] // 1s + [InlineData(2)] // 2s + [InlineData(5)] // 5s + [InlineData(10)] // 10s + [InlineData(30)] // 30s + [InlineData(1 * 60)] // 1 min + [InlineData(5 * 60)] // 5 min + [InlineData(10 * 60)] // 10 min + public void SchedulerShouldReturnPeriodAfterFirstFailure(int periodInSeconds) + { + var expected = TimeSpan.FromSeconds(periodInSeconds); + var schedule = new ExponentialBackoffConnectionSchedule(expected); - schedule.MarkFailure(); + schedule.MarkFailure(); - var actual = schedule.NextInterval; + var actual = schedule.NextInterval; - actual.ShouldBe(expected); - } + actual.ShouldBe(expected); + } - [Theory] - [InlineData(1)] // 1s - [InlineData(2)] // 2s - [InlineData(5)] // 5s - [InlineData(10)] // 10s - [InlineData(30)] // 30s - [InlineData(1 * 60)] // 1 min - [InlineData(5 * 60)] // 5 min - [InlineData(10 * 60)] // 10 min - public void SchedulerShouldBehaveExponentially(int periodInSeconds) + [Theory] + [InlineData(1)] // 1s + [InlineData(2)] // 2s + [InlineData(5)] // 5s + [InlineData(10)] // 10s + [InlineData(30)] // 30s + [InlineData(1 * 60)] // 1 min + [InlineData(5 * 60)] // 5 min + [InlineData(10 * 60)] // 10 min + public void SchedulerShouldBehaveExponentially(int periodInSeconds) + { + var period = TimeSpan.FromSeconds(periodInSeconds); + var schedule = new ExponentialBackoffConnectionSchedule(period); + IBackoff backoff = new LinearBackoff(period); + + while (backoff is not CappedBackoff) { - var period = TimeSpan.FromSeconds(periodInSeconds); - var schedule = new ExponentialBackoffConnectionSchedule(period); - IBackoff backoff = new LinearBackoff(period); + schedule.MarkFailure(); - while (backoff is not CappedBackoff) - { - schedule.MarkFailure(); + backoff = backoff.GetNext(schedule.NextInterval); + } - backoff = backoff.GetNext(schedule.NextInterval); - } + schedule.NextInterval.ShouldBe(ExponentialBackoffConnectionSchedule.MaximumBackoffInterval); + } - schedule.NextInterval.ShouldBe(ExponentialBackoffConnectionSchedule.MaximumBackoffInterval); - } + [Theory] + [InlineData(1)] // 1s + [InlineData(2)] // 2s + [InlineData(5)] // 5s + [InlineData(10)] // 10s + [InlineData(30)] // 30s + [InlineData(1 * 60)] // 1 min + [InlineData(5 * 60)] // 5 min + [InlineData(10 * 60)] // 10 min + public void SchedulerShouldRemainCappedDuringFailures(int periodInSeconds) + { + var period = TimeSpan.FromSeconds(periodInSeconds); + var schedule = new ExponentialBackoffConnectionSchedule(period); - [Theory] - [InlineData(1)] // 1s - [InlineData(2)] // 2s - [InlineData(5)] // 5s - [InlineData(10)] // 10s - [InlineData(30)] // 30s - [InlineData(1 * 60)] // 1 min - [InlineData(5 * 60)] // 5 min - [InlineData(10 * 60)] // 10 min - public void SchedulerShouldRemainCappedDuringFailures(int periodInSeconds) + while (schedule.NextInterval != ExponentialBackoffConnectionSchedule.MaximumBackoffInterval) { - var period = TimeSpan.FromSeconds(periodInSeconds); - var schedule = new ExponentialBackoffConnectionSchedule(period); - - while (schedule.NextInterval != ExponentialBackoffConnectionSchedule.MaximumBackoffInterval) - { - schedule.MarkFailure(); - } + schedule.MarkFailure(); + } - for (var i = 0; i < 10000; i++) - { - schedule.NextInterval.ShouldBe(ExponentialBackoffConnectionSchedule.MaximumBackoffInterval); - schedule.MarkFailure(); - } + for (var i = 0; i < 10000; i++) + { + schedule.NextInterval.ShouldBe(ExponentialBackoffConnectionSchedule.MaximumBackoffInterval); + schedule.MarkFailure(); } } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/PortableTimerTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/PortableTimerTests.cs index e9489bf..000f847 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/PortableTimerTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/InfrastructureTests/PortableTimerTests.cs @@ -1,134 +1,130 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Serilog.Sinks.Grafana.Loki.Infrastructure; +using Serilog.Sinks.Grafana.Loki.Infrastructure; using Shouldly; using Xunit; #pragma warning disable 1998 -namespace Serilog.Sinks.Grafana.Loki.Tests.InfrastructureTests +namespace Serilog.Sinks.Grafana.Loki.Tests.InfrastructureTests; + +public class PortableTimerTests { - public class PortableTimerTests + [Fact] + public void TimerShouldThrowExceptionOnCreatingWithNullOnTick() { - [Fact] - public void TimerShouldThrowExceptionOnCreatingWithNullOnTick() - { - Should.Throw(() => new PortableTimer(null!)) - .ParamName.ShouldBe("onTick"); - } + Should.Throw(() => new PortableTimer(null!)) + .ParamName.ShouldBe("onTick"); + } - [Fact] - public void TimerShouldThrowExceptionOnStartWithNegativeInterval() - { - var wasCalled = false; - using var timer = new PortableTimer(async () => { wasCalled = true; }); + [Fact] + public void TimerShouldThrowExceptionOnStartWithNegativeInterval() + { + var wasCalled = false; + using var timer = new PortableTimer(async () => { wasCalled = true; }); - Should.Throw(() => timer.Start(TimeSpan.MinValue)).ParamName - .ShouldBe("interval"); - wasCalled.ShouldBeFalse(); - } + Should.Throw(() => timer.Start(TimeSpan.MinValue)).ParamName + .ShouldBe("interval"); + wasCalled.ShouldBeFalse(); + } - [Fact] - public void TimerShouldThrowExceptionOnStartWhenDisposed() - { - var wasCalled = false; - var timer = new PortableTimer(async () => { wasCalled = true; }); + [Fact] + public void TimerShouldThrowExceptionOnStartWhenDisposed() + { + var wasCalled = false; + var timer = new PortableTimer(async () => { wasCalled = true; }); - timer.Start(TimeSpan.FromMilliseconds(100)); - timer.Dispose(); + timer.Start(TimeSpan.FromMilliseconds(100)); + timer.Dispose(); - wasCalled.ShouldBeFalse(); - Should.Throw(() => timer.Start(TimeSpan.Zero)).ObjectName - .ShouldBe(nameof(PortableTimer)); - } + wasCalled.ShouldBeFalse(); + Should.Throw(() => timer.Start(TimeSpan.Zero)).ObjectName + .ShouldBe(nameof(PortableTimer)); + } - [Fact] - public void TimerShouldWaitUntilEventHandlerOnDispose() + [Fact] + public void TimerShouldWaitUntilEventHandlerOnDispose() + { + var wasCalled = false; + var barrier = new Barrier(2); + + using (var timer = new PortableTimer(async () => + { + barrier.SignalAndWait(); + await Task.Delay(100); + wasCalled = true; + })) { - var wasCalled = false; - var barrier = new Barrier(2); + timer.Start(TimeSpan.Zero); + barrier.SignalAndWait(); + } - using (var timer = new PortableTimer(async () => - { - barrier.SignalAndWait(); - await Task.Delay(100); - wasCalled = true; - })) - { - timer.Start(TimeSpan.Zero); - barrier.SignalAndWait(); - } + wasCalled.ShouldBeTrue(); + } - wasCalled.ShouldBeTrue(); - } + [Fact] + public void TimerShouldNotProcessEventWhenWaiting() + { + var wasCalled = false; - [Fact] - public void TimerShouldNotProcessEventWhenWaiting() + using (var timer = new PortableTimer(async () => + { + await Task.Delay(50); + wasCalled = true; + })) { - var wasCalled = false; - - using (var timer = new PortableTimer(async () => - { - await Task.Delay(50); - wasCalled = true; - })) - { - timer.Start(TimeSpan.FromMilliseconds(20)); - } + timer.Start(TimeSpan.FromMilliseconds(20)); + } - Thread.Sleep(100); + Thread.Sleep(100); - wasCalled.ShouldBeFalse(); - } + wasCalled.ShouldBeFalse(); + } - [Fact] - public void EventShouldBeProcessedOneAtTimeWhenOverlaps() - { - var userHandlerOverlapped = false; + [Fact] + public void EventShouldBeProcessedOneAtTimeWhenOverlaps() + { + var userHandlerOverlapped = false; - // ReSharper disable AccessToModifiedClosure - PortableTimer timer = null!; - timer = new PortableTimer( - async () => + // ReSharper disable AccessToModifiedClosure + PortableTimer timer = null!; + timer = new PortableTimer( + async () => + { + if (Monitor.TryEnter(timer!)) { - if (Monitor.TryEnter(timer!)) + try { - try - { - // ReSharper disable once PossibleNullReferenceException - timer.Start(TimeSpan.Zero); - Thread.Sleep(20); - } - finally - { - Monitor.Exit(timer); - } + // ReSharper disable once PossibleNullReferenceException + timer.Start(TimeSpan.Zero); + Thread.Sleep(20); } - else + finally { - userHandlerOverlapped = true; + Monitor.Exit(timer); } - }); + } + else + { + userHandlerOverlapped = true; + } + }); - timer.Start(TimeSpan.FromMilliseconds(1)); - Thread.Sleep(50); - timer.Dispose(); + timer.Start(TimeSpan.FromMilliseconds(1)); + Thread.Sleep(50); + timer.Dispose(); - userHandlerOverlapped.ShouldBeFalse(); - } + userHandlerOverlapped.ShouldBeFalse(); + } - [Fact] - public void TimerCanBeDisposedFromMultipleThread() - { - PortableTimer timer = null!; + [Fact] + public void TimerCanBeDisposedFromMultipleThread() + { + PortableTimer timer = null!; - // ReSharper disable once PossibleNullReferenceException - timer = new PortableTimer(async () => timer.Start(TimeSpan.FromMilliseconds(1))); + // ReSharper disable once PossibleNullReferenceException + timer = new PortableTimer(async () => timer.Start(TimeSpan.FromMilliseconds(1))); - timer.Start(TimeSpan.Zero); - Thread.Sleep(50); + timer.Start(TimeSpan.Zero); + Thread.Sleep(50); - Parallel.For(0, Environment.ProcessorCount * 2, _ => timer.Dispose()); - } + Parallel.For(0, Environment.ProcessorCount * 2, _ => timer.Dispose()); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.AggregateExceptionShouldBeSerializedCorrectly.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.AggregateExceptionShouldBeSerializedCorrectly.approved.txt index 335078d..da7c47b 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.AggregateExceptionShouldBeSerializedCorrectly.approved.txt +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.AggregateExceptionShouldBeSerializedCorrectly.approved.txt @@ -1 +1 @@ -{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022An error occured\u0022,\u0022MessageTemplate\u0022:\u0022An error occured\u0022,\u0022level\u0022:\u0022error\u0022,\u0022Exception\u0022:{\u0022Type\u0022:\u0022System.AggregateException\u0022,\u0022Message\u0022:\u0022AggregateException (Exception 0) (Exception 1) (Exception 2) (Exception 3) (Exception 4)\u0022,\u0022StackTrace}}"]]}]} \ No newline at end of file +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022An error occured\u0022,\u0022MessageTemplate\u0022:\u0022An error occured\u0022,\u0022Exception\u0022:{\u0022Type\u0022:\u0022System.AggregateException\u0022,\u0022Message\u0022:\u0022AggregateException (Exception 0) (Exception 1) (Exception 2) (Exception 3) (Exception 4)\u0022,\u0022StackTrace\u0022:\u0022 at Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests.LokiJsonTextFormatterRequestPayloadTests.AggregateExceptionShouldBeSerializedCorrectly() in },\u0022level\u0022:\u0022error\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.GlobalLabelShouldHavePriorityOverPropertyOne.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.GlobalLabelShouldHavePriorityOverPropertyOne.approved.txt new file mode 100644 index 0000000..b1c2c80 --- /dev/null +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.GlobalLabelShouldHavePriorityOverPropertyOne.approved.txt @@ -0,0 +1 @@ +{"streams":[{"stream":{"App":"test"},"values":[["","{\u0022Message\u0022:\u0022Hello {App}\u0022,\u0022MessageTemplate\u0022:\u0022Hello {App}\u0022,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.GlobalLabelsShouldBeCreatedCorrectly.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.GlobalLabelsShouldBeCreatedCorrectly.approved.txt new file mode 100644 index 0000000..162f217 --- /dev/null +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.GlobalLabelsShouldBeCreatedCorrectly.approved.txt @@ -0,0 +1 @@ +{"streams":[{"stream":{"server_ip":"127.0.0.1"},"values":[["","{\u0022Message\u0022:\u0022This is \\\u0022Ukraine!\\\u0022\u0022,\u0022MessageTemplate\u0022:\u0022This is {Country}\u0022,\u0022Country\u0022:\u0022Ukraine!\u0022,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix.approved.txt new file mode 100644 index 0000000..b4ecaa4 --- /dev/null +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix.approved.txt @@ -0,0 +1 @@ +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022An error occured in \\\u0022Namespace.Module.Method\\\u0022\u0022,\u0022MessageTemplate\u0022:\u0022An error occured in {0}\u0022,\u00220\u0022:\u0022Namespace.Module.Method\u0022,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.LevelPropertyShouldBeRenamed.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.LevelPropertyShouldBeRenamed.approved.txt new file mode 100644 index 0000000..616d1b1 --- /dev/null +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.LevelPropertyShouldBeRenamed.approved.txt @@ -0,0 +1 @@ +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022Hero\u0027s \\\u0022info\\\u0022\u0022,\u0022MessageTemplate\u0022:\u0022Hero\u0027s {level}\u0022,\u0022_level\u0022:42,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessagePropertyForExceptionsWithoutMessageShouldNotBeCreated.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessagePropertyForExceptionsWithoutMessageShouldNotBeCreated.approved.txt index 33e9703..9ac4a22 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessagePropertyForExceptionsWithoutMessageShouldNotBeCreated.approved.txt +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessagePropertyForExceptionsWithoutMessageShouldNotBeCreated.approved.txt @@ -1 +1 @@ -{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022An error occured\u0022,\u0022MessageTemplate\u0022:\u0022An error occured\u0022,\u0022level\u0022:\u0022error\u0022,\u0022Exception\u0022:{\u0022Type\u0022:\u0022System.Exception\u0022,\u0022StackTrace}}"]]}]} \ No newline at end of file +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022An error occured\u0022,\u0022MessageTemplate\u0022:\u0022An error occured\u0022,\u0022Exception\u0022:{\u0022Type\u0022:\u0022System.Exception\u0022,\u0022StackTrace\u0022:\u0022 at Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests.LokiJsonTextFormatterRequestPayloadTests.MessagePropertyForExceptionsWithoutMessageShouldNotBeCreated() in },\u0022level\u0022:\u0022error\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessagePropertyForInternalTimestampShouldBeCreated.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessagePropertyForInternalTimestampShouldBeCreated.approved.txt index 99527e0..d09d072 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessagePropertyForInternalTimestampShouldBeCreated.approved.txt +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessagePropertyForInternalTimestampShouldBeCreated.approved.txt @@ -1 +1 @@ -{"streams":[{"stream":{"MeaningOfLife":"42"},"values":[["","{\u0022Message\u0022:\u0022The meaning of life is 42\u0022,\u0022MessageTemplate\u0022:\u0022The meaning of life is {@MeaningOfLife}\u0022,\u0022level\u0022:\u0022info\u0022,\u0022Timestamp\u0022:\u0022\u0022}"]]}]} \ No newline at end of file +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022The meaning of life is 42\u0022,\u0022MessageTemplate\u0022:\u0022The meaning of life is {@MeaningOfLife}\u0022,\u0022MeaningOfLife\u0022:42,\u0022level\u0022:\u0022info\u0022,\u0022Timestamp\u0022:\u0022\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessageWithParametersShouldBeSerializedCorrectly.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessageWithParametersShouldBeSerializedCorrectly.approved.txt index 0f96fdd..dd9b1e3 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessageWithParametersShouldBeSerializedCorrectly.approved.txt +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.MessageWithParametersShouldBeSerializedCorrectly.approved.txt @@ -1 +1 @@ -{"streams":[{"stream":{"MeaningOfLife":"42"},"values":[["","{\u0022Message\u0022:\u0022The meaning of life is 42\u0022,\u0022MessageTemplate\u0022:\u0022The meaning of life is {@MeaningOfLife}\u0022,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022The meaning of life is 42\u0022,\u0022MessageTemplate\u0022:\u0022The meaning of life is {MeaningOfLife}\u0022,\u0022MeaningOfLife\u0022:42,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterAsLabelShouldGenerateNewGroupAndStream.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterAsLabelShouldGenerateNewGroupAndStream.approved.txt new file mode 100644 index 0000000..a0b392d --- /dev/null +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterAsLabelShouldGenerateNewGroupAndStream.approved.txt @@ -0,0 +1 @@ +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022What is the meaning of life?\u0022,\u0022MessageTemplate\u0022:\u0022What is the meaning of life?\u0022,\u0022level\u0022:\u0022info\u0022}"]]},{"stream":{"MeaningOfLife":"42"},"values":[["","{\u0022Message\u0022:\u0022The meaning of life is {MeaningOfLife}\u0022,\u0022MessageTemplate\u0022:\u0022The meaning of life is {MeaningOfLife}\u0022,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.approved.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.received.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.received.txt new file mode 100644 index 0000000..491b17e --- /dev/null +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.received.txt @@ -0,0 +1 @@ +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022What is the meaning of life?\u0022,\u0022MessageTemplate\u0022:\u0022What is the meaning of life?\u0022,\u0022level\u0022:\u0022info\u0022}"],["","{\u0022Message\u0022:\u0022The meaning of life is 42\u0022,\u0022MessageTemplate\u0022:\u0022The meaning of life is {@MeaningOfLife}\u0022,\u0022MeaningOfLife\u0022:42,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.PropertiesAsLabelsShouldBeCreatedCorrectly.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.PropertiesAsLabelsShouldBeCreatedCorrectly.approved.txt new file mode 100644 index 0000000..6ee1ce7 --- /dev/null +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.PropertiesAsLabelsShouldBeCreatedCorrectly.approved.txt @@ -0,0 +1 @@ +{"streams":[{"stream":{"level":"info"},"values":[["","{\u0022Message\u0022:\u0022This is \\\u0022Ukraine!\\\u0022\u0022,\u0022MessageTemplate\u0022:\u0022This is {Country}\u0022,\u0022Country\u0022:\u0022Ukraine!\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.PropertyNameEqualToReservedKeywordShouldBeSanitized.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.PropertyNameEqualToReservedKeywordShouldBeSanitized.approved.txt index a8ae9e3..8f8ad3f 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.PropertyNameEqualToReservedKeywordShouldBeSanitized.approved.txt +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.PropertyNameEqualToReservedKeywordShouldBeSanitized.approved.txt @@ -1 +1 @@ -{"streams":[{"stream":{"Message":"Ukraine!"},"values":[["","{\u0022Message\u0022:\u0022This is \\\u0022Ukraine!\\\u0022\u0022,\u0022MessageTemplate\u0022:\u0022This is {Message}\u0022,\u0022level\u0022:\u0022info\u0022,\u0022_Message\u0022:\u0022Ukraine!\u0022}"]]}]} \ No newline at end of file +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022This is \\\u0022Ukraine!\\\u0022\u0022,\u0022MessageTemplate\u0022:\u0022This is {Message}\u0022,\u0022_Message\u0022:\u0022Ukraine!\u0022,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.SameGroupLabelsShouldBeInTheSameStreams.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.SameGroupLabelsShouldBeInTheSameStreams.approved.txt new file mode 100644 index 0000000..f28beb0 --- /dev/null +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.SameGroupLabelsShouldBeInTheSameStreams.approved.txt @@ -0,0 +1 @@ +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022This is an information without params\u0022,\u0022MessageTemplate\u0022:\u0022This is an information without params\u0022,\u0022level\u0022:\u0022info\u0022}"],["","{\u0022Message\u0022:\u0022This is also an information without params\u0022,\u0022MessageTemplate\u0022:\u0022This is also an information without params\u0022,\u0022level\u0022:\u0022info\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.SimpleExceptionShouldBeSerializedCorrectly.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.SimpleExceptionShouldBeSerializedCorrectly.approved.txt index 9f5d2c4..78e6ef5 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.SimpleExceptionShouldBeSerializedCorrectly.approved.txt +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/LokiJsonTextFormatterRequestPayloadTests.SimpleExceptionShouldBeSerializedCorrectly.approved.txt @@ -1 +1 @@ -{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022An error occured\u0022,\u0022MessageTemplate\u0022:\u0022An error occured\u0022,\u0022level\u0022:\u0022error\u0022,\u0022Exception\u0022:{\u0022Type\u0022:\u0022System.Exception\u0022,\u0022Message\u0022:\u0022Exception message\u0022,\u0022StackTrace}}"]]}]} \ No newline at end of file +{"streams":[{"stream":{},"values":[["","{\u0022Message\u0022:\u0022An error occured\u0022,\u0022MessageTemplate\u0022:\u0022An error occured\u0022,\u0022Exception\u0022:{\u0022Type\u0022:\u0022System.Exception\u0022,\u0022Message\u0022:\u0022Exception message\u0022,\u0022StackTrace\u0022:\u0022 at Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests.LokiJsonTextFormatterRequestPayloadTests.SimpleExceptionShouldBeSerializedCorrectly() in },\u0022level\u0022:\u0022error\u0022}"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.DifferentLevelsShouldNotGenerateDifferentStreamsWithoutLevelLabel.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.DifferentLevelsShouldNotGenerateDifferentStreamsWithoutLevelLabel.approved.txt deleted file mode 100644 index dcfa18b..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.DifferentLevelsShouldNotGenerateDifferentStreamsWithoutLevelLabel.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{},"values":[["","This is an information without params"],["","This is an error without params"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.EntryShouldBeRenderedAccordingToOutputTemplate.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.EntryShouldBeRenderedAccordingToOutputTemplate.approved.txt deleted file mode 100644 index 3a102ab..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.EntryShouldBeRenderedAccordingToOutputTemplate.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{},"values":[["","[ERR] An error occured"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.ExcludedLabelsShouldNotBePresentInRequest.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.ExcludedLabelsShouldNotBePresentInRequest.approved.txt deleted file mode 100644 index f3c6750..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.ExcludedLabelsShouldNotBePresentInRequest.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{"server_name":"loki_test"},"values":[["","An error occured"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.GlobalLabelsShouldNotBeFiltered.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.GlobalLabelsShouldNotBeFiltered.approved.txt deleted file mode 100644 index 2b8f1e5..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.GlobalLabelsShouldNotBeFiltered.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{"server_ip":"127.0.0.1"},"values":[["","An error occured"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix.approved.txt deleted file mode 100644 index d1444df..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{"param0":"Namespace.Module.Method"},"values":[["","An error occured in \u0022Namespace.Module.Method\u0022"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LevelLabelShouldBeCreatedCorrectly.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LevelLabelShouldBeCreatedCorrectly.approved.txt deleted file mode 100644 index 500c3c6..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LevelLabelShouldBeCreatedCorrectly.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{"level":"error"},"values":[["","An error occured"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LevelLabelShouldGenerateNewGroupAndStream.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LevelLabelShouldGenerateNewGroupAndStream.approved.txt deleted file mode 100644 index c9c52b9..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.LevelLabelShouldGenerateNewGroupAndStream.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{"level":"info"},"values":[["","This is an information without params"]]},{"stream":{"level":"error"},"values":[["","This is an error without params"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.OnlyIncludedLabelsShouldBePresentInRequest.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.OnlyIncludedLabelsShouldBePresentInRequest.approved.txt deleted file mode 100644 index 2b8f1e5..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.OnlyIncludedLabelsShouldBePresentInRequest.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{"server_ip":"127.0.0.1"},"values":[["","An error occured"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.approved.txt deleted file mode 100644 index 42895ae..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.ParameterShouldGenerateNewGroupAndStream.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{},"values":[["","What is the meaning of life?"]]},{"stream":{"MeaningOfLife":"42"},"values":[["","The meaning of life is 42"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.RequestContentShouldMatchApproved.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.RequestContentShouldMatchApproved.approved.txt deleted file mode 100644 index ce84356..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.RequestContentShouldMatchApproved.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{},"values":[["","An error occured"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.SameGroupLabelsShouldBeInTheSameStreams.approved.txt b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.SameGroupLabelsShouldBeInTheSameStreams.approved.txt deleted file mode 100644 index 6113751..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/Approvals/RequestPayloadTests.SameGroupLabelsShouldBeInTheSameStreams.approved.txt +++ /dev/null @@ -1 +0,0 @@ -{"streams":[{"stream":{},"values":[["","This is an information without params"],["","This is also an information without params"]]}]} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/AuthTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/AuthTests.cs index 815f0c9..99ead64 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/AuthTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/AuthTests.cs @@ -2,45 +2,44 @@ using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests +namespace Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests; + +public class AuthTests { - public class AuthTests + private readonly TestLokiHttpClient _client; + + public AuthTests() + { + _client = new TestLokiHttpClient(); + } + + [Fact] + public void BasicAuthHeaderShouldBeCorrect() { - private readonly TestLokiHttpClient _client; - - public AuthTests() - { - _client = new TestLokiHttpClient(); - } - - [Fact] - public void BasicAuthHeaderShouldBeCorrect() - { - var credentials = new LokiCredentials {Login = "Billy", Password = "Herrington"}; - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki("https://loki:3100", credentials: credentials, httpClient: _client) - .CreateLogger(); - - logger.Error("An error occured"); - logger.Dispose(); - - var authorization = _client.Client.DefaultRequestHeaders.Authorization; - authorization.ShouldSatisfyAllConditions( - () => authorization!.Scheme.ShouldBe("Basic"), - () => authorization!.Parameter.ShouldBe("QmlsbHk6SGVycmluZ3Rvbg==")); - } - - [Fact] - public void NoAuthHeaderShouldBeCorrect() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki("https://loki:3100", httpClient: _client) - .CreateLogger(); - - logger.Error("An error occured"); - logger.Dispose(); - - _client.Client.DefaultRequestHeaders.Authorization.ShouldBeNull(); - } + var credentials = new LokiCredentials {Login = "Billy", Password = "Herrington"}; + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki("https://loki:3100", credentials: credentials, httpClient: _client) + .CreateLogger(); + + logger.Error("An error occured"); + logger.Dispose(); + + var authorization = _client.Client.DefaultRequestHeaders.Authorization; + authorization.ShouldSatisfyAllConditions( + () => authorization!.Scheme.ShouldBe("Basic"), + () => authorization!.Parameter.ShouldBe("QmlsbHk6SGVycmluZ3Rvbg==")); + } + + [Fact] + public void NoAuthHeaderShouldBeCorrect() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki("https://loki:3100", httpClient: _client) + .CreateLogger(); + + logger.Error("An error occured"); + logger.Dispose(); + + _client.Client.DefaultRequestHeaders.Authorization.ShouldBeNull(); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/LokiJsonTextFormatterRequestPayloadTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/LokiJsonTextFormatterRequestPayloadTests.cs index 6559c22..ca88251 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/LokiJsonTextFormatterRequestPayloadTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/LokiJsonTextFormatterRequestPayloadTests.cs @@ -1,236 +1,446 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Serilog.Sinks.Grafana.Loki.Tests.TestHelpers; using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests +namespace Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests; + +public class LokiJsonTextFormatterRequestPayloadTests { - public class LokiJsonTextFormatterRequestPayloadTests + private const string ApprovalsFolderName = "Approvals"; + private const string ExceptionStackTraceRegEx = @"(?<= in)(.*?)(?=},\\)"; + private const string ExceptionStackTraceReplacement = " "; + private const string TimeStampRegEx = "\"[0-9]{19}\""; + private const string TimeStampReplacement = "\"\""; + + private readonly TestLokiHttpClient _client; + + public LokiJsonTextFormatterRequestPayloadTests() { - private const string ApprovalsFolderName = "Approvals"; - private const string OutputTemplate = "{Message}"; + _client = new TestLokiHttpClient(); + } - private readonly TestLokiHttpClient _client; + [Fact] + public void MessageWithoutParametersShouldBeSerializedCorrectly() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + httpClient: _client) + .CreateLogger(); - public LokiJsonTextFormatterRequestPayloadTests() + logger.Information("This is an information without params"); + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => + { + c.SubFolder(ApprovalsFolderName); + c.WithScrubber( + s => Regex.Replace( + s, + TimeStampRegEx, + TimeStampReplacement)); + }); + } + + [Fact] + public void MessageWithParametersShouldBeSerializedCorrectly() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + httpClient: _client) + .CreateLogger(); + + logger.Information("The meaning of life is {MeaningOfLife}", 42); + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => + { + c.SubFolder(ApprovalsFolderName); + c.WithScrubber( + s => Regex.Replace( + s, + TimeStampRegEx, + TimeStampReplacement)); + }); + } + + [Fact] + public void SimpleExceptionShouldBeSerializedCorrectly() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + httpClient: _client) + .CreateLogger(); + + try { - _client = new TestLokiHttpClient(); + throw new Exception("Exception message"); } - - [Fact] - public void MessageWithoutParametersShouldBeSerializedCorrectly() + catch (Exception ex) { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - textFormatter: new LokiJsonTextFormatter(), - httpClient: _client) - .CreateLogger(); - - logger.Information("This is an information without params"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => + logger.Error(ex, "An error occured"); + } + + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); + c.WithScrubber( + s => + { + s = Regex + .Replace( + s, + TimeStampRegEx, + TimeStampReplacement); + + return Regex.Replace( + s, + ExceptionStackTraceRegEx, + ExceptionStackTraceReplacement); + }); }); - } + } + + [Fact] + public void MessagePropertyForExceptionsWithoutMessageShouldNotBeCreated() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + textFormatter: new LokiJsonTextFormatter(), + httpClient: _client) + .CreateLogger(); - [Fact] - public void MessageWithParametersShouldBeSerializedCorrectly() + try { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - textFormatter: new LokiJsonTextFormatter(), - httpClient: _client) - .CreateLogger(); - - logger.Information("The meaning of life is {@MeaningOfLife}", 42); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => + throw new Exception(string.Empty); + } + catch (Exception ex) + { + logger.Error(ex, "An error occured"); + } + + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); + c.WithScrubber( + s => + { + s = Regex + .Replace( + s, + TimeStampRegEx, + TimeStampReplacement); + + return Regex.Replace( + s, + ExceptionStackTraceRegEx, + ExceptionStackTraceReplacement); + }); }); - } + } - [Fact] - public void SimpleExceptionShouldBeSerializedCorrectly() + [Fact] + public void AggregateExceptionShouldBeSerializedCorrectly() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + textFormatter: new LokiJsonTextFormatter(), + httpClient: _client) + .CreateLogger(); + + try { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - textFormatter: new LokiJsonTextFormatter(), - httpClient: _client) - .CreateLogger(); - - try + var exceptions = new List(); + + for (var i = 0; i < 5; i++) { - throw new Exception("Exception message"); + try + { + throw new Exception($"Exception {i}"); + } + catch (Exception ex) + { + exceptions.Add(ex); + } } - catch (Exception ex) + + throw new AggregateException("AggregateException", exceptions); + } + catch (Exception ex) + { + logger.Error(ex, "An error occured"); + } + + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { - logger.Error(ex, "An error occured"); - } + c.SubFolder(ApprovalsFolderName); + c.WithScrubber( + s => + { + s = Regex + .Replace( + s, + TimeStampRegEx, + TimeStampReplacement); - logger.Dispose(); + return Regex.Replace( + s, + ExceptionStackTraceRegEx, + ExceptionStackTraceReplacement); + }); + }); + } + + [Fact] + public void MessagePropertyForInternalTimestampShouldBeCreated() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + textFormatter: new LokiJsonTextFormatter(), + httpClient: _client, + useInternalTimestamp: true) + .CreateLogger(); - _client.Content.ShouldMatchApproved(c => + logger.Information("The meaning of life is {@MeaningOfLife}", 42); + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => - { - s = Regex - .Replace(s, "\"[0-9]{19}\"", "\"\""); - return Regex.Replace( - s, - @"(?<=\\u0022StackTrace)(.*?)(?=}})", - @""); - }); + c.WithScrubber( + s => + { + var replaced = Regex.Replace( + s, + TimeStampRegEx, + TimeStampReplacement); + + return Regex.Replace( + replaced, + "[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]*(\\-|\\+|\\\\u002B)[0-9]{2}:[0-9]{2}", + ""); + }); }); - } + } - [Fact] - public void MessagePropertyForExceptionsWithoutMessageShouldNotBeCreated() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - textFormatter: new LokiJsonTextFormatter(), - httpClient: _client) - .CreateLogger(); - - try + [Fact] + public void PropertyNameEqualToReservedKeywordShouldBeSanitized() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + textFormatter: new LokiJsonTextFormatter(), + httpClient: _client) + .CreateLogger(); + + logger.Information("This is {Message}", "Ukraine!"); + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { - throw new Exception(string.Empty); - } - catch (Exception ex) + c.SubFolder(ApprovalsFolderName); + c.WithScrubber( + s => Regex.Replace( + s, + TimeStampRegEx, + TimeStampReplacement)); + }); + } + + [Fact] + public void PropertiesAsLabelsShouldBeCreatedCorrectly() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + propertiesAsLabels: new[] { "level" }, + textFormatter: new LokiJsonTextFormatter(), + httpClient: _client) + .CreateLogger(); + + logger.Information("This is {Country}", "Ukraine!"); + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { - logger.Error(ex, "An error occured"); - } + c.SubFolder(ApprovalsFolderName); + c.WithScrubber( + s => Regex.Replace( + s, + TimeStampRegEx, + TimeStampReplacement)); + }); + } - logger.Dispose(); + [Fact] + public void GlobalLabelsShouldBeCreatedCorrectly() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + new[] { new LokiLabel { Key = "server_ip", Value = "127.0.0.1" } }, + textFormatter: new LokiJsonTextFormatter(), + httpClient: _client) + .CreateLogger(); + + logger.Information("This is {Country}", "Ukraine!"); + logger.Dispose(); - _client.Content.ShouldMatchApproved(c => + _client.Content.ShouldMatchApproved( + c => { c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => - { - s = Regex - .Replace(s, "\"[0-9]{19}\"", "\"\""); - return Regex.Replace( + c.WithScrubber( + s => Regex.Replace( s, - @"(?<=\\u0022StackTrace)(.*?)(?=}})", - @""); - }); + TimeStampRegEx, + TimeStampReplacement)); }); - } + } - [Fact] - public void AggregateExceptionShouldBeSerializedCorrectly() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - textFormatter: new LokiJsonTextFormatter(), - httpClient: _client) - .CreateLogger(); - - try + [Fact] + public void SameGroupLabelsShouldBeInTheSameStreams() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + httpClient: _client) + .CreateLogger(); + + logger.Information("This is an information without params"); + logger.Information("This is also an information without params"); + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { - var exceptions = new List(); + c.SubFolder(ApprovalsFolderName); + c.WithScrubber( + s => Regex.Replace( + s, + TimeStampRegEx, + TimeStampReplacement)); + }); + } - for (var i = 0; i < 5; i++) - { - try - { - throw new Exception($"Exception {i}"); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - } + [Fact] + public void ParameterAsLabelShouldGenerateNewGroupAndStream() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + propertiesAsLabels: new[] { "MeaningOfLife" }, + httpClient: _client) + .CreateLogger(); - throw new AggregateException("AggregateException", exceptions); - } - catch (Exception ex) + logger.Information("What is the meaning of life?"); + logger.Information("The meaning of life is {MeaningOfLife}", 42); + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { - logger.Error(ex, "An error occured"); - } + c.SubFolder(ApprovalsFolderName); + c.WithScrubber( + s => Regex.Replace( + s, + TimeStampRegEx, + TimeStampReplacement)); + }); + } + + [Fact] + public void LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + httpClient: _client) + .CreateLogger(); - logger.Dispose(); + logger.Information("An error occured in {0}", "Namespace.Module.Method"); + logger.Dispose(); - _client.Content.ShouldMatchApproved(c => + _client.Content.ShouldMatchApproved( + c => { c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => - { - s = Regex - .Replace(s, "\"[0-9]{19}\"", "\"\""); - return Regex.Replace( + c.WithScrubber( + s => Regex.Replace( s, - @"(?<=\\u0022StackTrace)(.*?)}\](?=}})", - @""); - }); + TimeStampRegEx, + TimeStampReplacement)); }); - } + } - [Fact] - public void MessagePropertyForInternalTimestampShouldBeCreated() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - textFormatter: new LokiJsonTextFormatter(), - httpClient: _client, - useInternalTimestamp: true) - .CreateLogger(); - - logger.Information("The meaning of life is {@MeaningOfLife}", 42); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => + [Fact] + public void GlobalLabelShouldHavePriorityOverPropertyOne() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + labels: new[] { new LokiLabel { Key = "App", Value = "test" } }, + propertiesAsLabels: new[] { "App" }, + httpClient: _client) + .CreateLogger(); + + logger.Information("Hello {App}", "not test"); + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => - { - var replaced = Regex.Replace(s, "\"[0-9]{19}\"", "\"\""); - return Regex.Replace(replaced, "[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]*(\\-|\\+|\\\\u002B)[0-9]{2}:[0-9]{2}", ""); - }); + c.WithScrubber( + s => Regex.Replace( + s, + TimeStampRegEx, + TimeStampReplacement)); }); - } + } - [Fact] - public void PropertyNameEqualToReservedKeywordShouldBeSanitized() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - textFormatter: new LokiJsonTextFormatter(), - httpClient: _client) - .CreateLogger(); - - logger.Information("This is {Message}", "Ukraine!"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => + [Fact] + public void LevelPropertyShouldBeRenamed() + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki( + "https://loki:3100", + httpClient: _client) + .CreateLogger(); + + // ReSharper disable once InconsistentLogPropertyNaming + logger.Information("Hero's {level}", 42); + logger.Dispose(); + + _client.Content.ShouldMatchApproved( + c => { c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); + c.WithScrubber( + s => Regex.Replace( + s, + TimeStampRegEx, + TimeStampReplacement)); }); - } } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/RequestPayloadTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/RequestPayloadTests.cs deleted file mode 100644 index 53fbe85..0000000 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/RequestPayloadTests.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using Serilog.Sinks.Grafana.Loki.Tests.TestHelpers; -using Shouldly; -using Xunit; - -namespace Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests -{ - public class RequestPayloadTests - { - private const string ApprovalsFolderName = "Approvals"; - private const string OutputTemplate = "{Message}"; - - private static readonly TimeSpan BatchPeriod = TimeSpan.FromHours(1); - - private readonly TestLokiHttpClient _client; - - public RequestPayloadTests() - { - _client = new TestLokiHttpClient(); - } - - [Fact] - public void RequestContentShouldMatchApproved() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - httpClient: _client) - .CreateLogger(); - - logger.Error("An error occured"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void LevelLabelShouldBeCreatedCorrectly() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - httpClient: _client, - createLevelLabel: true) - .CreateLogger(); - - logger.Error("An error occured"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void OnlyIncludedLabelsShouldBePresentInRequest() - { - var logger = new LoggerConfiguration() - .Enrich.WithProperty("server_name", "loki_test") - .Enrich.WithProperty("server_ip", "127.0.0.1") - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - filtrationMode: LokiLabelFiltrationMode.Include, - filtrationLabels: new[] {"server_ip"}, - httpClient: _client) - .CreateLogger(); - - logger.Error("An error occured"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void ExcludedLabelsShouldNotBePresentInRequest() - { - var logger = new LoggerConfiguration() - .Enrich.WithProperty("server_name", "loki_test") - .Enrich.WithProperty("server_ip", "127.0.0.1") - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - filtrationMode: LokiLabelFiltrationMode.Exclude, - filtrationLabels: new[] {"server_ip"}, - httpClient: _client) - .CreateLogger(); - - logger.Error("An error occured"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void EntryShouldBeRenderedAccordingToOutputTemplate() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: "[{Level:u3}] {Message}", - httpClient: _client) - .CreateLogger(); - - logger.Error("An error occured"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void GlobalLabelsShouldNotBeFiltered() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - filtrationMode: LokiLabelFiltrationMode.Exclude, - filtrationLabels: new[] {"server_ip"}, - labels: new[] {new LokiLabel {Key = "server_ip", Value = "127.0.0.1"}}, - httpClient: _client) - .CreateLogger(); - - logger.Error("An error occured"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void SameGroupLabelsShouldBeInTheSameStreams() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - period: BatchPeriod, - httpClient: _client) - .CreateLogger(); - - logger.Information("This is an information without params"); - logger.Information("This is also an information without params"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void DifferentLevelsShouldNotGenerateDifferentStreamsWithoutLevelLabel() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - period: BatchPeriod, - httpClient: _client) - .CreateLogger(); - - logger.Information("This is an information without params"); - logger.Error("This is an error without params"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void LevelLabelShouldGenerateNewGroupAndStream() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - period: BatchPeriod, - httpClient: _client, - createLevelLabel: true) - .CreateLogger(); - - logger.Information("This is an information without params"); - logger.Error("This is an error without params"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void ParameterShouldGenerateNewGroupAndStream() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - period: BatchPeriod, - httpClient: _client) - .CreateLogger(); - - logger.Information("What is the meaning of life?"); - logger.Information("The meaning of life is {@MeaningOfLife}", 42); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - - [Fact] - public void LabelsForIndexedPlaceholdersShouldBeCreatedWithParamPrefix() - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki( - "https://loki:3100", - outputTemplate: OutputTemplate, - httpClient: _client) - .CreateLogger(); - - logger.Information("An error occured in {0}", "Namespace.Module.Method"); - logger.Dispose(); - - _client.Content.ShouldMatchApproved(c => - { - c.SubFolder(ApprovalsFolderName); - c.WithScrubber(s => Regex.Replace(s, "\"[0-9]{19}\"", "\"\"")); - }); - } - } -} \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/RequestsUriTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/RequestsUriTests.cs index cbf81ba..7979571 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/RequestsUriTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/IntegrationTests/RequestsUriTests.cs @@ -3,30 +3,29 @@ using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests +namespace Serilog.Sinks.Grafana.Loki.Tests.IntegrationTests; + +public class RequestsUriTests { - public class RequestsUriTests - { - private readonly TestLokiHttpClient _client; + private readonly TestLokiHttpClient _client; - public RequestsUriTests() - { - _client = new TestLokiHttpClient(); - } + public RequestsUriTests() + { + _client = new TestLokiHttpClient(); + } - [Theory] - [InlineData("https://loki:3100")] - [InlineData("https://loki:3100/")] - public void RequestUriShouldBeCorrect(string uri) - { - var logger = new LoggerConfiguration() - .WriteTo.GrafanaLoki("https://loki:3100", httpClient: _client) - .CreateLogger(); + [Theory] + [InlineData("https://loki:3100")] + [InlineData("https://loki:3100/")] + public void RequestUriShouldBeCorrect(string uri) + { + var logger = new LoggerConfiguration() + .WriteTo.GrafanaLoki("https://loki:3100", httpClient: _client) + .CreateLogger(); - logger.Error("An error occured"); - logger.Dispose(); + logger.Error("An error occured"); + logger.Dispose(); - _client.RequestUri.ShouldBe(LokiRoutesBuilder.BuildLogsEntriesRoute(uri)); - } + _client.RequestUri.ShouldBe(LokiRoutesBuilder.BuildLogsEntriesRoute(uri)); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/Serilog.Sinks.Grafana.Loki.Tests.csproj b/test/Serilog.Sinks.Grafana.Loki.Tests/Serilog.Sinks.Grafana.Loki.Tests.csproj index a1cd868..32d5e67 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/Serilog.Sinks.Grafana.Loki.Tests.csproj +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/Serilog.Sinks.Grafana.Loki.Tests.csproj @@ -1,9 +1,10 @@ - net5.0 + net6.0 false enable + enable @@ -20,4 +21,4 @@ - + \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/CappedBackoff.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/CappedBackoff.cs index 2bf17e2..8077b8c 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/CappedBackoff.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/CappedBackoff.cs @@ -1,24 +1,21 @@ -using System; +namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers.Backoff; -namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers.Backoff +internal class CappedBackoff : IBackoff { - internal class CappedBackoff : IBackoff + private readonly TimeSpan _currentInterval; + + public CappedBackoff(TimeSpan currentInterval) { - private readonly TimeSpan _currentInterval; + _currentInterval = currentInterval; + } - public CappedBackoff(TimeSpan currentInterval) + public IBackoff GetNext(TimeSpan nextInterval) + { + if (nextInterval != _currentInterval) { - _currentInterval = currentInterval; + throw new Exception("Once backoff implementation is capped, it should remain capped"); } - public IBackoff GetNext(TimeSpan nextInterval) - { - if (nextInterval != _currentInterval) - { - throw new Exception("Once backoff implementation is capped, it should remain capped"); - } - - return this; - } + return this; } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/ExponentialBackoff.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/ExponentialBackoff.cs index 970c2dc..da89a41 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/ExponentialBackoff.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/ExponentialBackoff.cs @@ -1,32 +1,30 @@ -using System; -using Serilog.Sinks.Grafana.Loki.Infrastructure; +using Serilog.Sinks.Grafana.Loki.Infrastructure; -namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers.Backoff +namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers.Backoff; + +internal class ExponentialBackoff : IBackoff { - internal class ExponentialBackoff : IBackoff + private readonly TimeSpan _currentInterval; + + public ExponentialBackoff(TimeSpan currentInterval) { - private readonly TimeSpan _currentInterval; + _currentInterval = currentInterval; + } - public ExponentialBackoff(TimeSpan currentInterval) + public IBackoff GetNext(TimeSpan nextInterval) + { + // From the state of being exponential, the implementation can become capped + if (nextInterval == ExponentialBackoffConnectionSchedule.MaximumBackoffInterval) { - _currentInterval = currentInterval; + return new CappedBackoff(nextInterval); } - public IBackoff GetNext(TimeSpan nextInterval) + // From the state of being exponential, the implementation can remain exponential + if (nextInterval > _currentInterval) { - // From the state of being exponential, the implementation can become capped - if (nextInterval == ExponentialBackoffConnectionSchedule.MaximumBackoffInterval) - { - return new CappedBackoff(nextInterval); - } - - // From the state of being exponential, the implementation can remain exponential - if (nextInterval > _currentInterval) - { - return new ExponentialBackoff(nextInterval); - } - - throw new Exception("Next interval can't be lower then current interval"); + return new ExponentialBackoff(nextInterval); } + + throw new Exception("Next interval can't be lower then current interval"); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/IBackoff.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/IBackoff.cs index d72a902..be71d30 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/IBackoff.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/IBackoff.cs @@ -1,9 +1,6 @@ -using System; +namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers.Backoff; -namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers.Backoff +internal interface IBackoff { - internal interface IBackoff - { - public IBackoff GetNext(TimeSpan nextInterval); - } + public IBackoff GetNext(TimeSpan nextInterval); } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/LinearBackoff.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/LinearBackoff.cs index fb14c05..333db5f 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/LinearBackoff.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/Backoff/LinearBackoff.cs @@ -1,38 +1,36 @@ -using System; -using Serilog.Sinks.Grafana.Loki.Infrastructure; +using Serilog.Sinks.Grafana.Loki.Infrastructure; -namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers.Backoff +namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers.Backoff; + +internal class LinearBackoff : IBackoff { - internal class LinearBackoff : IBackoff + private readonly TimeSpan _currentInterval; + + public LinearBackoff(TimeSpan currentInterval) { - private readonly TimeSpan _currentInterval; + _currentInterval = currentInterval; + } - public LinearBackoff(TimeSpan currentInterval) + IBackoff IBackoff.GetNext(TimeSpan nextInterval) + { + // From the state of being linear, the implementation can become capped + if (nextInterval == ExponentialBackoffConnectionSchedule.MaximumBackoffInterval) { - _currentInterval = currentInterval; + return new CappedBackoff(nextInterval); } - IBackoff IBackoff.GetNext(TimeSpan nextInterval) + // From the state of being linear, the implementation can become exponential + if (nextInterval > _currentInterval) { - // From the state of being linear, the implementation can become capped - if (nextInterval == ExponentialBackoffConnectionSchedule.MaximumBackoffInterval) - { - return new CappedBackoff(nextInterval); - } - - // From the state of being linear, the implementation can become exponential - if (nextInterval > _currentInterval) - { - return new ExponentialBackoff(nextInterval); - } - - // From the state of being linear, the implementation can remain linear - if (nextInterval == _currentInterval) - { - return this; - } + return new ExponentialBackoff(nextInterval); + } - throw new Exception("The implementation from being linear must remain linear or become exponential"); + // From the state of being linear, the implementation can remain linear + if (nextInterval == _currentInterval) + { + return this; } + + throw new Exception("The implementation from being linear must remain linear or become exponential"); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/TestLokiHttpClient.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/TestLokiHttpClient.cs index c84fee2..7db3d67 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/TestLokiHttpClient.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/TestHelpers/TestLokiHttpClient.cs @@ -1,35 +1,31 @@ -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using Serilog.Sinks.Grafana.Loki.HttpClients; +using Serilog.Sinks.Grafana.Loki.HttpClients; using Serilog.Sinks.Grafana.Loki.Utils; -namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers +namespace Serilog.Sinks.Grafana.Loki.Tests.TestHelpers; + +internal class TestLokiHttpClient : LokiHttpClient { - internal class TestLokiHttpClient : LokiHttpClient + internal TestLokiHttpClient() { - internal TestLokiHttpClient() - { - } + } - internal TestLokiHttpClient(HttpClient httpClient) - : base(httpClient) - { - } + internal TestLokiHttpClient(HttpClient httpClient) + : base(httpClient) + { + } - public HttpClient Client => HttpClient; + public HttpClient Client => HttpClient; - public string Content { get; private set; } = null!; + public string Content { get; private set; } = null!; - public string RequestUri { get; private set; } = null!; + public string RequestUri { get; private set; } = null!; - public override async Task PostAsync(string requestUri, Stream contentStream) - { - using var streamReader = new StreamReader(contentStream, Encoding.UTF8WithoutBom); - Content = await streamReader.ReadToEndAsync(); - RequestUri = requestUri; + public override async Task PostAsync(string requestUri, Stream contentStream) + { + using var streamReader = new StreamReader(contentStream, Encoding.UTF8WithoutBom); + Content = await streamReader.ReadToEndAsync(); + RequestUri = requestUri; - return new HttpResponseMessage(); - } + return new HttpResponseMessage(); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/DateTimeOffsetExtensionsTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/DateTimeOffsetExtensionsTests.cs index 5c12507..9fd1db7 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/DateTimeOffsetExtensionsTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/DateTimeOffsetExtensionsTests.cs @@ -1,30 +1,28 @@ -using System; -using Serilog.Sinks.Grafana.Loki.Utils; +using Serilog.Sinks.Grafana.Loki.Utils; using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.UtilsTests +namespace Serilog.Sinks.Grafana.Loki.Tests.UtilsTests; + +public class DateTimeOffsetExtensionsTests { - public class DateTimeOffsetExtensionsTests + [Fact] + public void UnixEpochShouldBeConvertedCorrectly() { - [Fact] - public void UnixEpochShouldBeConvertedCorrectly() - { - var epoch = DateTimeOffset.UnixEpoch; + var epoch = DateTimeOffset.UnixEpoch; - var result = epoch.ToUnixNanosecondsString(); + var result = epoch.ToUnixNanosecondsString(); - result.ShouldBe("0"); - } + result.ShouldBe("0"); + } - [Fact] - public void DateTimeOffsetShouldBeConvertedCorrectly() - { - var dateTimeOffset = new DateTimeOffset(2021, 05, 25, 12, 00, 00, TimeSpan.Zero); + [Fact] + public void DateTimeOffsetShouldBeConvertedCorrectly() + { + var dateTimeOffset = new DateTimeOffset(2021, 05, 25, 12, 00, 00, TimeSpan.Zero); - var result = dateTimeOffset.ToUnixNanosecondsString(); + var result = dateTimeOffset.ToUnixNanosecondsString(); - result.ShouldBe("1621944000000000000"); - } + result.ShouldBe("1621944000000000000"); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/EncodingTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/EncodingTests.cs index 3bac1dc..9776f17 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/EncodingTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/EncodingTests.cs @@ -2,18 +2,17 @@ using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.UtilsTests +namespace Serilog.Sinks.Grafana.Loki.Tests.UtilsTests; + +public class EncodingTests { - public class EncodingTests + [Fact] + public void Utf8WithoutBomShouldNotHasPreamble() { - [Fact] - public void Utf8WithoutBomShouldNotHasPreamble() - { - var encoding = Encoding.UTF8WithoutBom; + var encoding = Encoding.UTF8WithoutBom; - var preamble = encoding.GetPreamble(); + var preamble = encoding.GetPreamble(); - preamble.Length.ShouldBe(0); - } + preamble.Length.ShouldBe(0); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/LogEventLevelExtensionsTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/LogEventLevelExtensionsTests.cs index 470ee00..b9d2c11 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/LogEventLevelExtensionsTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/LogEventLevelExtensionsTests.cs @@ -3,22 +3,21 @@ using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.UtilsTests +namespace Serilog.Sinks.Grafana.Loki.Tests.UtilsTests; + +public class LogEventLevelExtensionsTests { - public class LogEventLevelExtensionsTests + [Theory] + [InlineData(LogEventLevel.Verbose, "trace")] + [InlineData(LogEventLevel.Debug, "debug")] + [InlineData(LogEventLevel.Information, "info")] + [InlineData(LogEventLevel.Warning, "warning")] + [InlineData(LogEventLevel.Error, "error")] + [InlineData(LogEventLevel.Fatal, "critical")] + public void LogEventLevelShouldMapToCorrectGrafanaLevel(LogEventLevel logEventLevel, string expected) { - [Theory] - [InlineData(LogEventLevel.Verbose, "trace")] - [InlineData(LogEventLevel.Debug, "debug")] - [InlineData(LogEventLevel.Information, "info")] - [InlineData(LogEventLevel.Warning, "warning")] - [InlineData(LogEventLevel.Error, "error")] - [InlineData(LogEventLevel.Fatal, "critical")] - public void LogEventLevelShouldMapToCorrectGrafanaLevel(LogEventLevel logEventLevel, string expected) - { - var grafanaLogLevel = logEventLevel.ToGrafanaLogLevel(); + var grafanaLogLevel = logEventLevel.ToGrafanaLogLevel(); - grafanaLogLevel.ShouldBe(expected); - } + grafanaLogLevel.ShouldBe(expected); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/LokiRoutesBuilderTests.cs b/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/LokiRoutesBuilderTests.cs index f7659a8..3ff6f73 100644 --- a/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/LokiRoutesBuilderTests.cs +++ b/test/Serilog.Sinks.Grafana.Loki.Tests/UtilsTests/LokiRoutesBuilderTests.cs @@ -2,20 +2,19 @@ using Shouldly; using Xunit; -namespace Serilog.Sinks.Grafana.Loki.Tests.UtilsTests +namespace Serilog.Sinks.Grafana.Loki.Tests.UtilsTests; + +public class LokiRoutesBuilderTests { - public class LokiRoutesBuilderTests + [Theory] + [InlineData("https://loki", "https://loki/loki/api/v1/push")] + [InlineData("https://loki/", "https://loki/loki/api/v1/push")] + [InlineData("https://loki:3100", "https://loki:3100/loki/api/v1/push")] + [InlineData("https://loki:3100/", "https://loki:3100/loki/api/v1/push")] + public void CorrectRoutesShouldBeBuilt(string host, string expected) { - [Theory] - [InlineData("https://loki", "https://loki/loki/api/v1/push")] - [InlineData("https://loki/", "https://loki/loki/api/v1/push")] - [InlineData("https://loki:3100", "https://loki:3100/loki/api/v1/push")] - [InlineData("https://loki:3100/", "https://loki:3100/loki/api/v1/push")] - public void CorrectRoutesShouldBeBuilt(string host, string expected) - { - var route = LokiRoutesBuilder.BuildLogsEntriesRoute(host); + var route = LokiRoutesBuilder.BuildLogsEntriesRoute(host); - route.ShouldBe(expected); - } + route.ShouldBe(expected); } } \ No newline at end of file