diff --git a/README.md b/README.md index 82f7062..ab5f3e8 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ A Serilog sink that writes events to the [Seq](https://datalust.co/seq) structur [Package Logo](https://nuget.org/packages/serilog.sinks.seq) +> [!TIP] +> If you would like to see timing and dependency information in Seq, [SerilogTracing](https://github.com/serilog-tracing/serilog-tracing) is a Serilog extension that supports both logs and traces. + + ### Getting started Install _Serilog.Sinks.Seq_ into your .NET project: diff --git a/sample/BlazorWasm/BlazorWasm.csproj b/sample/BlazorWasm/BlazorWasm.csproj index 4054529..d0fde33 100644 --- a/sample/BlazorWasm/BlazorWasm.csproj +++ b/sample/BlazorWasm/BlazorWasm.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs index 39b746c..414d536 100644 --- a/src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs @@ -19,7 +19,6 @@ using Serilog.Sinks.Seq; using System.Net.Http; using Serilog.Formatting; -using Serilog.Sinks.PeriodicBatching; using Serilog.Sinks.Seq.Batched; using Serilog.Sinks.Seq.Audit; using Serilog.Sinks.Seq.Http; @@ -98,8 +97,6 @@ public static LoggerConfiguration Seq( var formatter = payloadFormatter ?? CreateDefaultFormatter(); var ingestionApi = new SeqIngestionApiClient(serverUrl, apiKey, messageHandler); - ILogEventSink sink; - if (bufferBaseFilename == null) { var batchedSink = new BatchedSeqSink( @@ -108,29 +105,29 @@ public static LoggerConfiguration Seq( eventBodyLimitBytes, controlledSwitch); - var options = new PeriodicBatchingSinkOptions + var options = new BatchingOptions { BatchSizeLimit = batchPostingLimit, - Period = defaultedPeriod, + BufferingTimeLimit = defaultedPeriod, QueueLimit = queueSizeLimit }; - - sink = new PeriodicBatchingSink(batchedSink, options); - } - else - { - sink = new DurableSeqSink( - ingestionApi, - formatter, - bufferBaseFilename, - batchPostingLimit, - defaultedPeriod, - bufferSizeLimitBytes, - eventBodyLimitBytes, - controlledSwitch, - retainedInvalidPayloadsLimitBytes); + + return loggerSinkConfiguration.Conditional( + controlledSwitch.IsIncluded, + wt => wt.Sink(batchedSink, options, restrictedToMinimumLevel, levelSwitch: null)); } - + + var sink = new DurableSeqSink( + ingestionApi, + formatter, + bufferBaseFilename, + batchPostingLimit, + defaultedPeriod, + bufferSizeLimitBytes, + eventBodyLimitBytes, + controlledSwitch, + retainedInvalidPayloadsLimitBytes); + return loggerSinkConfiguration.Conditional( controlledSwitch.IsIncluded, wt => wt.Sink(sink, restrictedToMinimumLevel, levelSwitch: null)); diff --git a/src/Serilog.Sinks.Seq/Serilog.Sinks.Seq.csproj b/src/Serilog.Sinks.Seq/Serilog.Sinks.Seq.csproj index 5b5db9a..917f880 100644 --- a/src/Serilog.Sinks.Seq/Serilog.Sinks.Seq.csproj +++ b/src/Serilog.Sinks.Seq/Serilog.Sinks.Seq.csproj @@ -2,7 +2,7 @@ A Serilog sink that writes events to Seq using newline-delimited JSON and HTTP/HTTPS. - 7.0.1 + 8.0.0 Serilog Contributors;Serilog.Sinks.Seq Contributors;Datalust Pty Ltd Copyright © Serilog Contributors, Serilog.Sinks.Seq Contributors, Datalust Pty Ltd. netstandard2.0;net6.0 @@ -19,7 +19,7 @@ https://github.com/datalust/serilog-sinks-seq git true - 10 + 12 enable README.md @@ -29,8 +29,7 @@ - - + diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/Batched/BatchedSeqSink.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/Batched/BatchedSeqSink.cs index 4a4ef79..85c16de 100644 --- a/src/Serilog.Sinks.Seq/Sinks/Seq/Batched/BatchedSeqSink.cs +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/Batched/BatchedSeqSink.cs @@ -15,17 +15,16 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; +using Serilog.Core; using Serilog.Events; using Serilog.Formatting; -using Serilog.Sinks.PeriodicBatching; using Serilog.Sinks.Seq.Http; namespace Serilog.Sinks.Seq.Batched; /// -/// The default Seq sink, for use in combination with . +/// The default Seq sink. /// sealed class BatchedSeqSink : IBatchedLogEventSink, IDisposable { @@ -60,11 +59,11 @@ public async Task OnEmptyBatchAsync() if (_controlledSwitch.IsActive && _nextRequiredLevelCheckUtc < DateTime.UtcNow) { - await EmitBatchAsync(Enumerable.Empty()); + await EmitBatchAsync([]); } } - public async Task EmitBatchAsync(IEnumerable events) + public async Task EmitBatchAsync(IReadOnlyCollection events) { _nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval); diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/Conventions/PreserveDottedPropertyNames.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/Conventions/PreserveDottedPropertyNames.cs new file mode 100644 index 0000000..f25107c --- /dev/null +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/Conventions/PreserveDottedPropertyNames.cs @@ -0,0 +1,31 @@ +// Copyright © Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using Serilog.Events; + +namespace Serilog.Sinks.Seq.Conventions; + +/// +/// Maintains verbatim processing of property names. A property named "a.b" will be transmitted to Seq as a +/// scalar value with name "a.b". +/// +class PreserveDottedPropertyNames: IDottedPropertyNameConvention +{ + /// + public IReadOnlyDictionary ProcessDottedPropertyNames(IReadOnlyDictionary maybeDotted) + { + return maybeDotted; + } +} diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/Conventions/UnflattenDottedPropertyNames.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/Conventions/UnflattenDottedPropertyNames.cs new file mode 100644 index 0000000..e3406ae --- /dev/null +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/Conventions/UnflattenDottedPropertyNames.cs @@ -0,0 +1,131 @@ +// Copyright © Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Serilog.Events; + +namespace Serilog.Sinks.Seq.Conventions; + +/// +/// Experimental. Unflatten property names. A property with name "a.b" will be transmitted to Seq as +/// a structure with name "a", and one member "b". +/// +/// This behavior is enabled when the Serilog.Parsing.MessageTemplateParser.AcceptDottedPropertyNames +/// switch is set to value . +class UnflattenDottedPropertyNames: IDottedPropertyNameConvention +{ + const int MaxDepth = 10; + + /// + public IReadOnlyDictionary ProcessDottedPropertyNames(IReadOnlyDictionary maybeDotted) + { + return DottedToNestedRecursive(maybeDotted, 0); + } + + static IReadOnlyDictionary DottedToNestedRecursive(IReadOnlyDictionary maybeDotted, int depth) + { + if (depth == MaxDepth) + return maybeDotted; + + // Assume that the majority of entries will be bare or have unique prefixes. + var result = new Dictionary(maybeDotted.Count); + + // Sorted for determinism. + var dotted = new SortedDictionary(StringComparer.Ordinal); + + // First - give priority to bare names, since these would otherwise be claimed by the parents of further nested + // layers and we'd have nowhere to put them when resolving conflicts. (Dotted entries that conflict can keep their dotted keys). + + foreach (var kv in maybeDotted) + { + if (IsDottedIdentifier(kv.Key)) + { + // Stash for processing in the next stage. + dotted.Add(kv.Key, kv.Value); + } + else + { + result.Add(kv.Key, kv.Value); + } + } + + // Then - for dotted keys with a prefix not already present in the result, convert to structured data and add to + // the result. Any set of dotted names that collide with a preexisting key will be left as-is. + + string? prefix = null; + Dictionary? nested = null; + foreach (var kv in dotted) + { + var (newPrefix, rem) = TakeFirstIdentifier(kv.Key); + + if (prefix != null && prefix != newPrefix) + { + result.Add(prefix, MakeStructureValue(DottedToNestedRecursive(nested!, depth + 1))); + prefix = null; + nested = null; + } + + if (nested != null && !nested.ContainsKey(rem)) + { + prefix = newPrefix; + nested.Add(rem, kv.Value); + } + else if (nested == null && !result.ContainsKey(newPrefix)) + { + prefix = newPrefix; + nested = new () { { rem, kv.Value } }; + } + else + { + result.Add(kv.Key, kv.Value); + } + } + + if (prefix != null) + { + result[prefix] = MakeStructureValue(DottedToNestedRecursive(nested!, depth + 1)); + } + + return result; + } + + static LogEventPropertyValue MakeStructureValue(IReadOnlyDictionary properties) + { + return new StructureValue(properties.Select(kv => new LogEventProperty(kv.Key, kv.Value)), typeTag: null); + } + + internal static bool IsDottedIdentifier(string key) => + key.Contains('.') && + !key.StartsWith(".", StringComparison.Ordinal) && + !key.EndsWith(".", StringComparison.Ordinal) && + key.Split('.').All(IsIdentifier); + + static bool IsIdentifier(string s) => s.Length != 0 && + !char.IsDigit(s[0]) && + s.All(ch => char.IsLetter(ch) || char.IsDigit(ch) || ch == '_'); + + static (string, string) TakeFirstIdentifier(string dottedIdentifier) + { + // We can do this simplistically because keys in `dotted` conform to `IsDottedName`. + Debug.Assert(IsDottedIdentifier(dottedIdentifier)); + + var firstDot = dottedIdentifier.IndexOf('.'); + var prefix = dottedIdentifier.Substring(0, firstDot); + var rem = dottedIdentifier.Substring(firstDot + 1); + return (prefix, rem); + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/BookmarkFile.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/BookmarkFile.cs index e026c62..d0e661a 100644 --- a/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/BookmarkFile.cs +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/BookmarkFile.cs @@ -39,7 +39,7 @@ public FileSetPosition TryReadBookmark() if (current != null) { - var parts = current.Split(new[] { ":::" }, StringSplitOptions.RemoveEmptyEntries); + var parts = current.Split([":::"], StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 2) { return new FileSetPosition(long.Parse(parts[0]), parts[1]); diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/ExponentialBackoffConnectionSchedule.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/ExponentialBackoffConnectionSchedule.cs index 3a02311..c5425b6 100644 --- a/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/ExponentialBackoffConnectionSchedule.cs +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/ExponentialBackoffConnectionSchedule.cs @@ -17,7 +17,7 @@ namespace Serilog.Sinks.Seq.Durable; /// -/// Based on the BatchedConnectionStatus class from . +/// Based on the BatchedConnectionStatus class from Serilog.Sinks.PeriodicBatching. /// sealed class ExponentialBackoffConnectionSchedule { @@ -30,7 +30,7 @@ sealed class ExponentialBackoffConnectionSchedule public ExponentialBackoffConnectionSchedule(TimeSpan period) { - if (period < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(period), "The connection retry period must be a positive timespan"); + if (period < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(period), "The connection retry period must be a positive timespan."); _period = period; } @@ -55,7 +55,7 @@ public TimeSpan NextInterval // Second failure, start ramping up the interval - first 2x, then 4x, ... var backoffFactor = Math.Pow(2, (_failuresSinceSuccessfulConnection - 1)); - // If the period is ridiculously short, give it a boost so we get some + // If the period is ridiculously short, give it a boost so that we get some // visible backoff. var backoffPeriod = Math.Max(_period.Ticks, MinimumBackoffPeriod.Ticks); diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/IDottedPropertyNameConvention.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/IDottedPropertyNameConvention.cs new file mode 100644 index 0000000..9cedffc --- /dev/null +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/IDottedPropertyNameConvention.cs @@ -0,0 +1,33 @@ +// Copyright © Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using Serilog.Events; + +namespace Serilog.Sinks.Seq; + +/// +/// Enables switching between the experimental "unflattening" behavior applied to dotted property names, and the +/// regular verbatim property name handling. +/// +interface IDottedPropertyNameConvention +{ + /// + /// Convert the properties in into the form specified by the current property + /// name processing convention. + /// + /// The properties associated with a log event. + /// The processed properties. + IReadOnlyDictionary ProcessDottedPropertyNames(IReadOnlyDictionary maybeDotted); +} diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs index 591ae89..9bdfaca 100644 --- a/src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs @@ -1,4 +1,4 @@ -// Copyright 2016 Serilog Contributors +// Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ using Serilog.Formatting; using Serilog.Formatting.Json; using Serilog.Parsing; +using Serilog.Sinks.Seq.Conventions; + // ReSharper disable MemberCanBePrivate.Global // ReSharper disable PossibleMultipleEnumeration @@ -33,8 +35,13 @@ namespace Serilog.Sinks.Seq; /// implicit SerilogTracing span support. public class SeqCompactJsonFormatter: ITextFormatter { - readonly JsonValueFormatter _valueFormatter = new("$type"); + static readonly IDottedPropertyNameConvention DottedPropertyNameConvention = + AppContext.TryGetSwitch("Serilog.Parsing.MessageTemplateParser.AcceptDottedPropertyNames", out var accept) && accept ? + new UnflattenDottedPropertyNames() : + new PreserveDottedPropertyNames(); + readonly JsonValueFormatter _valueFormatter = new("$type"); + /// /// Format the log event into the output. Subsequent events will be newline-delimited. /// @@ -139,8 +146,9 @@ public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFo output.Write('\"'); } } - - foreach (var property in logEvent.Properties) + + var properties = DottedPropertyNameConvention.ProcessDottedPropertyNames(logEvent.Properties); + foreach (var property in properties) { var name = property.Key; diff --git a/test/Serilog.Sinks.Seq.Tests/Conventions/UnflattenDottedPropertyNamesTests.cs b/test/Serilog.Sinks.Seq.Tests/Conventions/UnflattenDottedPropertyNamesTests.cs new file mode 100644 index 0000000..e6bbf15 --- /dev/null +++ b/test/Serilog.Sinks.Seq.Tests/Conventions/UnflattenDottedPropertyNamesTests.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Linq; +using Serilog.Events; +using Serilog.Sinks.Seq.Conventions; +using Xunit; + +namespace Serilog.Sinks.Seq.Tests.Conventions; + +public class UnflattenDottedPropertyNamesTests +{ + [Fact] + public void DottedToNestedWorks() + { + var someDotted = new Dictionary + { + ["dotnet.ilogger.category"] = new ScalarValue("Test.App"), + ["environment.name"] = new ScalarValue("Production"), + ["environment.region"] = new ScalarValue("us-west-2"), + ["environment.beverage.name"] = new ScalarValue("coffee"), + ["environment.domains"] = new StructureValue( + [ + new LogEventProperty("example.com", new ScalarValue(42)), + new LogEventProperty("datalust.co", new ScalarValue(43)) + ]), + ["scope"] = new StructureValue([]), + ["scope.name"] = new ScalarValue("Gerald"), + ["vegetable"] = new ScalarValue("Potato"), + ["Scope"] = new ScalarValue("Periscope"), + [".gitattributes"] = new ScalarValue("Text") + }; + + var expected = new Dictionary + { + ["dotnet"] = new StructureValue( + [ + new LogEventProperty("ilogger", new StructureValue( + [ + new LogEventProperty("category", new ScalarValue("Test App")) + ])) + ]), + ["environment"] = new StructureValue( + [ + new LogEventProperty("name", new ScalarValue("Production")), + new LogEventProperty("region", new ScalarValue("us-west-2")), + new LogEventProperty("beverage", new StructureValue( + [ + new LogEventProperty("name", new ScalarValue("coffee")) + ])), + new LogEventProperty("domains", new StructureValue( + [ + new LogEventProperty("example.com", new ScalarValue(42)), + new LogEventProperty("datalust.co", new ScalarValue(43)) + ])) + ]), + ["scope"] = new StructureValue([]), + ["scope.name"] = new ScalarValue("Gerald"), + ["vegetable"] = new ScalarValue("Potato"), + ["Scope"] = new ScalarValue("Periscope"), + [".gitattributes"] = new ScalarValue("Text") + }; + + var actual = new UnflattenDottedPropertyNames().ProcessDottedPropertyNames(someDotted); + + Assert.Equal(expected.Count, actual.Count); + foreach (var expectedProperty in expected) + { + Assert.True(actual.TryGetValue(expectedProperty.Key, out var actualProperty)); + AssertEquivalentValue(expectedProperty.Value, expectedProperty.Value); + } + } + + [Theory] + [InlineData("", false)] + [InlineData(".", false)] + [InlineData("..", false)] + [InlineData(".a", false)] + [InlineData("a.", false)] + [InlineData("a..b", false)] + [InlineData("a.b..c", false)] + [InlineData("a.b.", false)] + [InlineData("a. .b", false)] + [InlineData("1.0", false)] + [InlineData("1", false)] + [InlineData("a", false)] + [InlineData("abc", false)] + [InlineData("a.b", true)] + [InlineData("a1.bc._._xd.e_", true)] + public void OnlyProcessesValidDottedNames(string key, bool isValid) + { + var actual = UnflattenDottedPropertyNames.IsDottedIdentifier(key); + Assert.Equal(isValid, actual); + } + + static void AssertEquivalentValue(LogEventPropertyValue expected, LogEventPropertyValue actual) + { + switch (expected, actual) + { + case (ScalarValue expectedScalar, ScalarValue actualScalar): + { + Assert.Equal(expectedScalar.Value, actualScalar.Value); + break; + } + case (StructureValue expectedStructure, StructureValue actualStructure): + { + Assert.Equal(expectedStructure.TypeTag, actualStructure.TypeTag); + Assert.Equal(expectedStructure.Properties.Count, actualStructure.Properties.Count); + var actualProperties = actualStructure.Properties.ToDictionary(p => p.Name, p => p.Value); + foreach (var expectedProperty in expectedStructure.Properties) + { + var actualValue = Assert.Contains(expectedProperty.Name, actualProperties); + AssertEquivalentValue(expectedProperty.Value, actualValue); + } + break; + } + default: + { + Assert.Equal(expected, actual); + break; + } + } + } +} diff --git a/test/Serilog.Sinks.Seq.Tests/Durable/PayloadReaderTests.cs b/test/Serilog.Sinks.Seq.Tests/Durable/PayloadReaderTests.cs index c1487c6..dbbba44 100644 --- a/test/Serilog.Sinks.Seq.Tests/Durable/PayloadReaderTests.cs +++ b/test/Serilog.Sinks.Seq.Tests/Durable/PayloadReaderTests.cs @@ -17,7 +17,9 @@ public void ReadsEventsFromBufferFiles() { using var tmp = new TempFolder(); var fn = tmp.AllocateFilename("clef"); - var lines = IOFile.ReadAllText(Path.Combine("Resources", "ThreeBufferedEvents.clef.txt"), Encoding.UTF8).Split(new [] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries); + var lines = IOFile.ReadAllText(Path.Combine("Resources", "ThreeBufferedEvents.clef.txt"), Encoding.UTF8) + // ReSharper disable once RedundantCast + .Split((char[])['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); using (var f = IOFile.Create(fn)) using (var fw = new StreamWriter(f, Encoding.UTF8)) { @@ -42,7 +44,9 @@ public void ReadsEventsFromRawBufferFiles() { using var tmp = new TempFolder(); var fn = tmp.AllocateFilename("json"); - var lines = IOFile.ReadAllText(Path.Combine("Resources", "ThreeBufferedEvents.json.txt"), Encoding.UTF8).Split(new [] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries); + var lines = IOFile.ReadAllText(Path.Combine("Resources", "ThreeBufferedEvents.json.txt"), Encoding.UTF8) + // ReSharper disable once RedundantCast + .Split((char[])['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); using (var f = IOFile.Create(fn)) using (var fw = new StreamWriter(f, Encoding.UTF8)) { diff --git a/test/Serilog.Sinks.Seq.Tests/Serilog.Sinks.Seq.Tests.csproj b/test/Serilog.Sinks.Seq.Tests/Serilog.Sinks.Seq.Tests.csproj index acb05e2..c429307 100644 --- a/test/Serilog.Sinks.Seq.Tests/Serilog.Sinks.Seq.Tests.csproj +++ b/test/Serilog.Sinks.Seq.Tests/Serilog.Sinks.Seq.Tests.csproj @@ -28,13 +28,13 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - +