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

Added useCompactFormat flag to support posting events using Serilog.Formatting.Compact #25

Merged
merged 1 commit into from
Jul 7, 2016
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
2 changes: 2 additions & 0 deletions sample/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public static void Main(string[] args)
Thread.Sleep(1000);
Log.Debug("Loop iteration done");
}

Log.CloseAndFlush();
}
}
}
9 changes: 7 additions & 2 deletions src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public static class SeqLoggerConfigurationExtensions
/// <param name="retainedInvalidPayloadsLimitBytes">A soft limit for the number of bytes to use for storing failed requests.
/// The limit is soft in that it can be exceeded by any single error payload, but in that case only that single error
/// payload will be retained.</param>
/// <param name="useCompactFormat">Use the compact log event format defined by
/// <a href="https://github.com/serilog/serilog-formatting-compact">Serilog.Formatting.Compact</a>. Has no effect on
/// durable log shipping. Requires Seq 3.3+.</param>
/// <returns>Logger configuration, allowing configuration to continue.</returns>
/// <exception cref="ArgumentNullException">A required parameter is null.</exception>
public static LoggerConfiguration Seq(
Expand All @@ -65,7 +68,8 @@ public static LoggerConfiguration Seq(
long? eventBodyLimitBytes = 256 * 1024,
LoggingLevelSwitch controlLevelSwitch = null,
HttpMessageHandler messageHandler = null,
long? retainedInvalidPayloadsLimitBytes = null)
long? retainedInvalidPayloadsLimitBytes = null,
bool useCompactFormat = false)
{
if (loggerSinkConfiguration == null) throw new ArgumentNullException(nameof(loggerSinkConfiguration));
if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl));
Expand All @@ -84,7 +88,8 @@ public static LoggerConfiguration Seq(
defaultedPeriod,
eventBodyLimitBytes,
controlLevelSwitch,
messageHandler);
messageHandler,
useCompactFormat);
}
else
{
Expand Down
7 changes: 2 additions & 5 deletions src/Serilog.Sinks.Seq/Sinks/Seq/HttpLogShipper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ namespace Serilog.Sinks.Seq
{
class HttpLogShipper : IDisposable
{
const string ApiKeyHeaderName = "X-Seq-ApiKey";
const string BulkUploadResource = "api/events/raw";

static readonly TimeSpan RequiredLevelCheckInterval = TimeSpan.FromMinutes(2);

readonly string _apiKey;
Expand Down Expand Up @@ -206,9 +203,9 @@ void OnTick()

var content = new StringContent(payload, Encoding.UTF8, "application/json");
if (!string.IsNullOrWhiteSpace(_apiKey))
content.Headers.Add(ApiKeyHeaderName, _apiKey);
content.Headers.Add(SeqApi.ApiKeyHeaderName, _apiKey);

var result = _httpClient.PostAsync(BulkUploadResource, content).Result;
var result = _httpClient.PostAsync(SeqApi.BulkUploadResource, content).Result;
if (result.IsSuccessStatusCode)
{
_connectionSchedule.MarkSuccess();
Expand Down
5 changes: 5 additions & 0 deletions src/Serilog.Sinks.Seq/Sinks/Seq/SeqApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ namespace Serilog.Sinks.Seq
{
class SeqApi
{
public const string BulkUploadResource = "api/events/raw";
public const string ApiKeyHeaderName = "X-Seq-ApiKey";
public const string RawEventFormatMimeType = "application/json";
public const string CompactLogEventFormatMimeType = "application/vnd.serilog.clef";

// Why not use a JSON parser here? For a very small case, it's not
// worth taking on the extra payload/dependency management issues that
// a full-fledged parser will entail. If things get more sophisticated
Expand Down
95 changes: 74 additions & 21 deletions src/Serilog.Sinks.Seq/Sinks/Seq/SeqSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
using Serilog.Core;
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Formatting.Compact;
using Serilog.Formatting.Json;
using Serilog.Sinks.PeriodicBatching;

namespace Serilog.Sinks.Seq
Expand All @@ -31,12 +33,13 @@ class SeqSink : PeriodicBatchingSink
readonly string _apiKey;
readonly long? _eventBodyLimitBytes;
readonly HttpClient _httpClient;
const string BulkUploadResource = "api/events/raw";
const string ApiKeyHeaderName = "X-Seq-ApiKey";

static readonly JsonValueFormatter JsonValueFormatter = new JsonValueFormatter();

// If non-null, then background level checks will be performed; set either through the constructor
// or in response to a level specification from the server. Never set to null after being made non-null.
LoggingLevelSwitch _levelControlSwitch;
readonly bool _useCompactFormat;
static readonly TimeSpan RequiredLevelCheckInterval = TimeSpan.FromMinutes(2);
DateTime _nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval);

Expand All @@ -50,13 +53,15 @@ public SeqSink(
TimeSpan period,
long? eventBodyLimitBytes,
LoggingLevelSwitch levelControlSwitch,
HttpMessageHandler messageHandler)
HttpMessageHandler messageHandler,
bool useCompactFormat)
: base(batchPostingLimit, period)
{
if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl));
_apiKey = apiKey;
_eventBodyLimitBytes = eventBodyLimitBytes;
_levelControlSwitch = levelControlSwitch;
_useCompactFormat = useCompactFormat;

var baseUri = serverUrl;
if (!baseUri.EndsWith("/"))
Expand Down Expand Up @@ -89,13 +94,23 @@ protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
{
_nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval);

var payload = FormatPayload(events, _eventBodyLimitBytes);
string payload, payloadContentType;
if (_useCompactFormat)
{
payloadContentType = SeqApi.CompactLogEventFormatMimeType;
payload = FormatCompactPayload(events, _eventBodyLimitBytes);
}
else
{
payloadContentType = SeqApi.RawEventFormatMimeType;
payload = FormatRawPayload(events, _eventBodyLimitBytes);
}

var content = new StringContent(payload, Encoding.UTF8, "application/json");
var content = new StringContent(payload, Encoding.UTF8, payloadContentType);
if (!string.IsNullOrWhiteSpace(_apiKey))
content.Headers.Add(ApiKeyHeaderName, _apiKey);
content.Headers.Add(SeqApi.ApiKeyHeaderName, _apiKey);

var result = await _httpClient.PostAsync(BulkUploadResource, content).ConfigureAwait(false);
var result = await _httpClient.PostAsync(SeqApi.BulkUploadResource, content).ConfigureAwait(false);
if (!result.IsSuccessStatusCode)
throw new LoggingFailedException($"Received failed result {result.StatusCode} when posting events to Seq");

Expand All @@ -115,39 +130,56 @@ protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
}
}

internal static string FormatPayload(IEnumerable<LogEvent> events, long? eventBodyLimitBytes)
internal static string FormatCompactPayload(IEnumerable<LogEvent> events, long? eventBodyLimitBytes)
{
var payload = new StringWriter();
payload.Write("{\"Events\":[");

var delimStart = "";
foreach (var logEvent in events)
{
var buffer = new StringWriter();

try
{
RawJsonFormatter.FormatContent(logEvent, buffer);
CompactJsonFormatter.FormatEvent(logEvent, buffer, JsonValueFormatter);
}
catch (Exception ex)
{
SelfLog.WriteLine(
"Event at {0} with message template {1} could not be formatted into JSON for Seq and will be dropped: {2}",
logEvent.Timestamp.ToString("o"), logEvent.MessageTemplate.Text, ex);

LogNonFormattableEvent(logEvent, ex);
continue;
}

var json = buffer.ToString();
if (CheckEventBodySize(json, eventBodyLimitBytes))
{
payload.WriteLine(json);
}
}

return payload.ToString();
}

if (eventBodyLimitBytes.HasValue &&
Encoding.UTF8.GetByteCount(json) > eventBodyLimitBytes.Value)
internal static string FormatRawPayload(IEnumerable<LogEvent> events, long? eventBodyLimitBytes)
{
var payload = new StringWriter();
payload.Write("{\"Events\":[");

var delimStart = "";
foreach (var logEvent in events)
{
var buffer = new StringWriter();

try
{
SelfLog.WriteLine(
"Event JSON representation exceeds the byte size limit of {0} set for this Seq sink and will be dropped; data: {1}",
eventBodyLimitBytes, json);
RawJsonFormatter.FormatContent(logEvent, buffer);
}
else
catch (Exception ex)
{
LogNonFormattableEvent(logEvent, ex);
continue;
}

var json = buffer.ToString();
if (CheckEventBodySize(json, eventBodyLimitBytes))
{
payload.Write(delimStart);
payload.Write(json);
Expand All @@ -165,5 +197,26 @@ protected override bool CanInclude(LogEvent evt)
return levelControlSwitch == null ||
(int)levelControlSwitch.MinimumLevel <= (int)evt.Level;
}

static bool CheckEventBodySize(string json, long? eventBodyLimitBytes)
{
if (eventBodyLimitBytes.HasValue &&
Encoding.UTF8.GetByteCount(json) > eventBodyLimitBytes.Value)
{
SelfLog.WriteLine(
"Event JSON representation exceeds the byte size limit of {0} set for this Seq sink and will be dropped; data: {1}",
eventBodyLimitBytes, json);
return false;
}

return true;
}

static void LogNonFormattableEvent(LogEvent logEvent, Exception ex)
{
SelfLog.WriteLine(
"Event at {0} with message template {1} could not be formatted into JSON for Seq and will be dropped: {2}",
logEvent.Timestamp.ToString("o"), logEvent.MessageTemplate.Text, ex);
}
}
}
5 changes: 3 additions & 2 deletions src/Serilog.Sinks.Seq/project.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "2.0.1-*",
"version": "3.0.0-*",
"description": "Serilog sink that writes to the Seq event server over HTTP/S.",
"authors": [ "Serilog Contributors" ],
"packOptions": {
Expand All @@ -10,7 +10,8 @@
},
"dependencies": {
"Serilog": "2.0.0",
"Serilog.Sinks.PeriodicBatching": "2.0.0"
"Serilog.Sinks.PeriodicBatching": "2.0.0",
"Serilog.Formatting.Compact": "1.0.0"
},
"buildOptions": {
"keyFile": "../../assets/Serilog.snk"
Expand Down
20 changes: 18 additions & 2 deletions test/Serilog.Sinks.Seq.Tests/SeqSinkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,32 @@ public class SeqSinkTests
public void EventsAreFormattedIntoJsonPayloads()
{
var evt = Some.LogEvent("Hello, {Name}!", "Alice");
var json = SeqSink.FormatPayload(new[] {evt}, null);
var json = SeqSink.FormatRawPayload(new[] {evt}, null);
Assert.Contains("Name\":\"Alice", json);
}

[Fact]
public void EventsAreDroppedWhenJsonRenderingFails()
{
var evt = Some.LogEvent(new NastyException(), "Hello, {Name}!", "Alice");
var json = SeqSink.FormatPayload(new[] { evt }, null);
var json = SeqSink.FormatRawPayload(new[] { evt }, null);
Assert.Contains("[]", json);
}

[Fact]
public void EventsAreFormattedIntoCompactJsonPayloads()
{
var evt = Some.LogEvent("Hello, {Name}!", "Alice");
var json = SeqSink.FormatCompactPayload(new[] { evt }, null);
Assert.Contains("Name\":\"Alice", json);
}

[Fact]
public void EventsAreDroppedWhenCompactJsonRenderingFails()
{
var evt = Some.LogEvent(new NastyException(), "Hello, {Name}!", "Alice");
var json = SeqSink.FormatCompactPayload(new[] { evt }, null);
Assert.Empty(json);
}
}
}