Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More robust event formatting when writing to durable sink buffer files #157

Merged
merged 6 commits into from
Nov 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "5.0.202",
"rollForward": "latestPatch"
"version": "5.0.401",
"rollForward": "latestFeature"
}
}
2 changes: 1 addition & 1 deletion sample/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Sample
{
public class Program
public static class Program
{
public static void Main()
{
Expand Down
8 changes: 2 additions & 6 deletions sample/Sample/Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<Description>Sample Console Application</Description>
<Authors>nblumhardt</Authors>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<TargetFrameworks>net4.8;net5.0</TargetFrameworks>
<AssemblyName>Sample</AssemblyName>
<OutputType>Exe</OutputType>
<PackageId>Sample</PackageId>
Expand All @@ -20,16 +20,12 @@
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.0" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net47' ">
<ItemGroup Condition=" '$(TargetFramework)' == 'net4.8' ">
<Reference Include="System.Net.Http" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Serilog.Sinks.Seq\Serilog.Sinks.Seq.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="logs\" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions serilog-sinks-seq.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=aaaaa/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Backoff/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=delim/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=fdelim/@EntryIndexedValue">True</s:Boolean>
Expand Down
6 changes: 3 additions & 3 deletions src/Serilog.Sinks.Seq/Serilog.Sinks.Seq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<VersionPrefix>5.0.2</VersionPrefix>
<Authors>Serilog Contributors</Authors>
<Copyright>Copyright © Serilog Contributors</Copyright>
<TargetFrameworks>netstandard1.1;netstandard1.3;net45;netstandard2.0;netcoreapp3.1;net5.0</TargetFrameworks>
<TargetFrameworks>netstandard1.1;netstandard1.3;netstandard2.0;net4.5;netcoreapp3.1;net5.0</TargetFrameworks>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>Serilog</RootNamespace>
Expand Down Expand Up @@ -38,11 +38,11 @@
<DefineConstants>$(DefineConstants);DURABLE;THREADING_TIMER</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition=" '$(TargetFramework)' == 'net45' ">
<PropertyGroup Condition=" '$(TargetFramework)' == 'net4.5' ">
<DefineConstants>$(DefineConstants);DURABLE;THREADING_TIMER;HRESULTS</DefineConstants>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
<ItemGroup Condition=" '$(TargetFramework)' == 'net4.5' ">
<Reference Include="System.Net.Http" />
</ItemGroup>

Expand Down
1 change: 0 additions & 1 deletion src/Serilog.Sinks.Seq/Sinks/Seq/Audit/SeqAuditSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ async Task EmitAsync(LogEvent logEvent)

var payload = new StringWriter();
CompactJsonFormatter.FormatEvent(logEvent, payload, JsonValueFormatter);
payload.WriteLine();

var content = new StringContent(payload.ToString(), Encoding.UTF8, SeqApi.CompactLogEventFormatMimeType);
if (!string.IsNullOrWhiteSpace(_apiKey))
Expand Down
125 changes: 125 additions & 0 deletions src/Serilog.Sinks.Seq/Sinks/Seq/ConstrainedBufferedFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Serilog.Sinks.Seq Copyright 2014-2019 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.IO;
using System.Text;
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Compact;
using Serilog.Formatting.Json;
using Serilog.Parsing;

namespace Serilog.Sinks.Seq
{
/// <summary>
/// Wraps a <see cref="CompactJsonFormatter" /> to suppress formatting errors and apply the event body size
/// limit, if any. Placeholder events are logged when an event is unable to be written itself.
/// </summary>
class ConstrainedBufferedFormatter : ITextFormatter
{
static readonly int NewLineByteCount = Encoding.UTF8.GetByteCount(Environment.NewLine);

readonly long? _eventBodyLimitBytes;
readonly CompactJsonFormatter _jsonFormatter = new CompactJsonFormatter(new JsonValueFormatter("$type"));

public ConstrainedBufferedFormatter(long? eventBodyLimitBytes)
{
_eventBodyLimitBytes = eventBodyLimitBytes;
}

public void Format(LogEvent logEvent, TextWriter output)
{
Format(logEvent, output, writePlaceholders: true);
}

void Format(LogEvent logEvent, TextWriter output, bool writePlaceholders)
{
var buffer = new StringWriter();

try
{
_jsonFormatter.Format(logEvent, buffer);
}
catch (Exception ex) when (writePlaceholders)
{
SelfLog.WriteLine(
"Event with message template {0} at {1} could not be formatted as JSON and will be dropped: {2}",
logEvent.MessageTemplate.Text, logEvent.Timestamp, ex);

var placeholder = CreateNonFormattableEventPlaceholder(logEvent, ex);
Format(placeholder, output, writePlaceholders: false);
return;
}

var jsonLine = buffer.ToString();
if (CheckEventBodySize(jsonLine, _eventBodyLimitBytes))
{
output.Write(jsonLine);
}
else
{
SelfLog.WriteLine(
"Event JSON representation exceeds the byte size limit of {0} set for this Seq sink and will be dropped; data: {1}",
_eventBodyLimitBytes, jsonLine);

if (writePlaceholders)
{
var placeholder = CreateOversizeEventPlaceholder(logEvent, jsonLine, _eventBodyLimitBytes!.Value);
Format(placeholder, output, writePlaceholders: false);
}
}
}

static LogEvent CreateNonFormattableEventPlaceholder(LogEvent logEvent, Exception ex)
{
return new LogEvent(
logEvent.Timestamp,
LogEventLevel.Error,
ex,
new MessageTemplateParser().Parse("Event with message template {OriginalMessageTemplate} could not be formatted as JSON"),
new[]
{
new LogEventProperty("OriginalMessageTemplate", new ScalarValue(logEvent.MessageTemplate.Text)),
});
}

static bool CheckEventBodySize(string jsonLine, long? eventBodyLimitBytes)
{
if (eventBodyLimitBytes == null)
return true;

var byteCount = Encoding.UTF8.GetByteCount(jsonLine) - NewLineByteCount;
return byteCount <= eventBodyLimitBytes;
}

static LogEvent CreateOversizeEventPlaceholder(LogEvent logEvent, string jsonLine, long eventBodyLimitBytes)
{
// If the limit is so constrained as to disallow sending 512 bytes + packaging, that's okay - we'll just drop
// the placeholder, too.
var sample = jsonLine.Substring(0, Math.Min(jsonLine.Length, 512));
return new LogEvent(
logEvent.Timestamp,
LogEventLevel.Error,
exception: null,
new MessageTemplateParser().Parse("Event JSON representation exceeds the body size limit {EventBodyLimitBytes}; sample: {EventBodySample}"),
new[]
{
new LogEventProperty("EventBodyLimitBytes", new ScalarValue(eventBodyLimitBytes)),
new LogEventProperty("EventBodySample", new ScalarValue(sample)),
});
}
}
}
3 changes: 1 addition & 2 deletions src/Serilog.Sinks.Seq/Sinks/Seq/Durable/DurableSeqSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
using Serilog.Events;
using System.Net.Http;
using System.Text;
using Serilog.Formatting.Compact;

namespace Serilog.Sinks.Seq.Durable
{
Expand Down Expand Up @@ -60,7 +59,7 @@ public DurableSeqSink(
const long individualFileSizeLimitBytes = 100L * 1024 * 1024;
_sink = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.File(new CompactJsonFormatter(),
.WriteTo.File(new ConstrainedBufferedFormatter(eventBodyLimitBytes),
fileSet.RollingFilePathFormat,
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: individualFileSizeLimitBytes,
Expand Down
80 changes: 0 additions & 80 deletions src/Serilog.Sinks.Seq/Sinks/Seq/SeqPayloadFormatter.cs

This file was deleted.

14 changes: 9 additions & 5 deletions src/Serilog.Sinks.Seq/Sinks/Seq/SeqSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
Expand All @@ -33,7 +34,7 @@ class SeqSink : IBatchedLogEventSink, IDisposable
static readonly TimeSpan RequiredLevelCheckInterval = TimeSpan.FromMinutes(2);

readonly string _apiKey;
readonly long? _eventBodyLimitBytes;
readonly ConstrainedBufferedFormatter _formatter;
readonly HttpClient _httpClient;

DateTime _nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval);
Expand All @@ -49,9 +50,9 @@ public SeqSink(
if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl));
_controlledSwitch = controlledSwitch ?? throw new ArgumentNullException(nameof(controlledSwitch));
_apiKey = apiKey;
_eventBodyLimitBytes = eventBodyLimitBytes;
_httpClient = messageHandler != null ? new HttpClient(messageHandler) : new HttpClient();
_httpClient.BaseAddress = new Uri(SeqApi.NormalizeServerBaseAddress(serverUrl));
_formatter = new ConstrainedBufferedFormatter(eventBodyLimitBytes);
}

public void Dispose()
Expand All @@ -74,10 +75,13 @@ public async Task EmitBatchAsync(IEnumerable<LogEvent> events)
{
_nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval);

var payloadContentType = SeqApi.CompactLogEventFormatMimeType;
var payload = SeqPayloadFormatter.FormatCompactPayload(events, _eventBodyLimitBytes);
var payload = new StringWriter();
foreach (var evt in events)
{
_formatter.Format(evt, payload);
}

var content = new StringContent(payload, Encoding.UTF8, payloadContentType);
var content = new StringContent(payload.ToString(), Encoding.UTF8, SeqApi.CompactLogEventFormatMimeType);
if (!string.IsNullOrWhiteSpace(_apiKey))
content.Headers.Add(SeqApi.ApiKeyHeaderName, _apiKey);

Expand Down
2 changes: 1 addition & 1 deletion test/Serilog.Sinks.Seq.Tests/Audit/SeqAuditSinkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void EarlyCommunicationErrorsPropagateToCallerWhenAuditing()
}
}

[Fact]
[Fact] // This test requires an outbound connection in order to execute properly.
public void RemoteCommunicationErrorsPropagateToCallerWhenAuditing()
{
using (var logger = new LoggerConfiguration()
Expand Down
Loading