Skip to content
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
16 changes: 11 additions & 5 deletions src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

using System;
using System.Globalization;
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;
Expand All @@ -34,7 +35,7 @@ public static class SeqLoggerConfigurationExtensions
const int DefaultBatchPostingLimit = 1000;
static readonly TimeSpan DefaultPeriod = TimeSpan.FromSeconds(2);
const int DefaultQueueSizeLimit = 100000;
static ITextFormatter CreateDefaultFormatter() => new SeqCompactJsonFormatter();
static ITextFormatter CreateDefaultFormatter(IFormatProvider? formatProvider) => new SeqCompactJsonFormatter(formatProvider);

/// <summary>
/// Write log events to a <a href="https://datalust.co/seq">Seq</a> server.
Expand Down Expand Up @@ -67,6 +68,8 @@ public static class SeqLoggerConfigurationExtensions
/// durable log shipping.</param>
/// <param name="payloadFormatter">An <see cref="ITextFormatter"/> that will be used to format (newline-delimited CLEF/JSON)
/// payloads. Experimental.</param>
/// <param name="formatProvider">An <see cref="IFormatProvider"/> that will be used to render log event tokens. Does not apply if <paramref name="payloadFormatter"/> is provided.
/// If <paramref name="formatProvider"/> is provided then event messages will be rendered and included in the payload.</param>
/// <returns>Logger configuration, allowing configuration to continue.</returns>
/// <exception cref="ArgumentNullException">A required parameter is null.</exception>
public static LoggerConfiguration Seq(
Expand All @@ -83,7 +86,8 @@ public static LoggerConfiguration Seq(
HttpMessageHandler? messageHandler = null,
long? retainedInvalidPayloadsLimitBytes = null,
int queueSizeLimit = DefaultQueueSizeLimit,
ITextFormatter? payloadFormatter = null)
ITextFormatter? payloadFormatter = null,
IFormatProvider? formatProvider = null)
{
if (loggerSinkConfiguration == null) throw new ArgumentNullException(nameof(loggerSinkConfiguration));
if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl));
Expand All @@ -94,7 +98,7 @@ public static LoggerConfiguration Seq(

var defaultedPeriod = period ?? DefaultPeriod;
var controlledSwitch = new ControlledLevelSwitch(controlLevelSwitch);
var formatter = payloadFormatter ?? CreateDefaultFormatter();
var formatter = payloadFormatter ?? CreateDefaultFormatter(formatProvider);
var ingestionApi = new SeqIngestionApiClient(serverUrl, apiKey, messageHandler);

if (bufferBaseFilename == null)
Expand Down Expand Up @@ -145,6 +149,7 @@ public static LoggerConfiguration Seq(
/// <param name="messageHandler">Used to construct the HttpClient that will send the log messages to Seq.</param>
/// <param name="payloadFormatter">An <see cref="ITextFormatter"/> that will be used to format (newline-delimited CLEF/JSON)
/// payloads. Experimental.</param>
/// <param name="formatProvider">An <see cref="IFormatProvider"/> that will be used to render log event tokens.</param>
/// <returns>Logger configuration, allowing configuration to continue.</returns>
/// <exception cref="ArgumentNullException">A required parameter is null.</exception>
public static LoggerConfiguration Seq(
Expand All @@ -153,13 +158,14 @@ public static LoggerConfiguration Seq(
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
string? apiKey = null,
HttpMessageHandler? messageHandler = null,
ITextFormatter? payloadFormatter = null)
ITextFormatter? payloadFormatter = null,
IFormatProvider? formatProvider = null)
{
if (loggerAuditSinkConfiguration == null) throw new ArgumentNullException(nameof(loggerAuditSinkConfiguration));
if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl));

var ingestionApi = new SeqIngestionApiClient(serverUrl, apiKey, messageHandler);
var sink = new SeqAuditSink(ingestionApi, payloadFormatter ?? CreateDefaultFormatter());
var sink = new SeqAuditSink(ingestionApi, payloadFormatter ?? CreateDefaultFormatter(formatProvider));
return loggerAuditSinkConfiguration.Sink(sink, restrictedToMinimumLevel);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// 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.IO;
using Serilog.Events;
using Serilog.Formatting.Json;
using Serilog.Parsing;

namespace Serilog.Sinks.Seq.Formatting;

/// <summary>
/// Matches the `:lj` clean formatting style now employed by Serilog.Expressions, Serilog.Sinks.Console, and elsewhere.
/// In this mode, strings embedded in message templates are unquoted, and structured data is rendered as JSON.
/// </summary>
/// <remarks>This implementation is derived from the Serilog.Expressions one, sans theming support, and avoiding the
/// extra dependency. In time there should be core Serilog support for this.</remarks>
static class CleanMessageTemplateFormatter
{
static readonly JsonValueFormatter SharedJsonValueFormatter = new("$type");

public static string Format(MessageTemplate messageTemplate, IReadOnlyDictionary<string, LogEventPropertyValue> properties, IFormatProvider? formatProvider)
{
var output = new StringWriter();

foreach (var token in messageTemplate.Tokens)
{
switch (token)
{
case TextToken tt:
{
output.Write(tt.Text);
break;
}
case PropertyToken pt:
{
RenderPropertyToken(properties, pt, output, formatProvider);
break;
}
default:
{
output.Write(token);
break;
}
}
}

return output.ToString();
}

static void RenderPropertyToken(IReadOnlyDictionary<string, LogEventPropertyValue> properties, PropertyToken pt, TextWriter output, IFormatProvider? formatProvider)
{
if (!properties.TryGetValue(pt.PropertyName, out var value))
{
output.Write(pt.ToString());
return;
}

if (pt.Alignment is null)
{
RenderPropertyValueUnaligned(value, output, pt.Format, formatProvider);
return;
}

var buffer = new StringWriter();

RenderPropertyValueUnaligned(value, buffer, pt.Format, formatProvider);

var result = buffer.ToString();

if (result.Length >= pt.Alignment.Value.Width)
output.Write(result);
else
Padding.Apply(output, result, pt.Alignment.Value);
}

static void RenderPropertyValueUnaligned(LogEventPropertyValue propertyValue, TextWriter output, string? format, IFormatProvider? formatProvider)
{
if (propertyValue is not ScalarValue scalar)
{
SharedJsonValueFormatter.Format(propertyValue, output);
return;
}

var value = scalar.Value;

if (value == null)
{
output.Write("null");
return;
}

if (value is string str)
{
output.Write(str);
return;
}

if (value is ValueType)
{
if (value is int or uint or long or ulong or decimal or byte or sbyte or short or ushort)
{
output.Write(((IFormattable)value).ToString(format, formatProvider));
return;
}

if (value is double d)
{
output.Write(d.ToString(format, formatProvider));
return;
}

if (value is float f)
{
output.Write(f.ToString(format, formatProvider));
return;
}

if (value is bool b)
{
output.Write(b);
return;
}
}

if (value is IFormattable formattable)
{
output.Write(formattable.ToString(format, formatProvider));
return;
}

output.Write(value);
}
}
54 changes: 54 additions & 0 deletions src/Serilog.Sinks.Seq/Sinks/Seq/Formatting/Padding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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.IO;
using System.Linq;
using Serilog.Parsing;

namespace Serilog.Sinks.Seq.Formatting
{
static class Padding
{
static readonly char[] PaddingChars = Enumerable.Repeat(' ', 80).ToArray();

/// <summary>
/// Writes the provided value to the output, applying direction-based padding when <paramref name="alignment"/> is provided.
/// </summary>
public static void Apply(TextWriter output, string value, Alignment alignment)
{
if (value.Length >= alignment.Width)
{
output.Write(value);
return;
}

var pad = alignment.Width - value.Length;

if (alignment.Direction == AlignmentDirection.Left)
output.Write(value);

if (pad <= PaddingChars.Length)
{
output.Write(PaddingChars, 0, pad);
}
else
{
output.Write(new string(' ', pad));
}

if (alignment.Direction == AlignmentDirection.Right)
output.Write(value);
}
}
}
26 changes: 23 additions & 3 deletions src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using Serilog.Formatting.Json;
using Serilog.Parsing;
using Serilog.Sinks.Seq.Conventions;
using Serilog.Sinks.Seq.Formatting;

// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable PossibleMultipleEnumeration
Expand All @@ -41,15 +42,22 @@ public class SeqCompactJsonFormatter: ITextFormatter
new PreserveDottedPropertyNames();

readonly JsonValueFormatter _valueFormatter = new("$type");
readonly IFormatProvider _formatProvider;

/// <param name="formatProvider">An <see cref="IFormatProvider"/> that will be used to render log event tokens.</param>
public SeqCompactJsonFormatter(IFormatProvider? formatProvider = null)
{
_formatProvider = formatProvider ?? CultureInfo.InvariantCulture;
}

/// <summary>
/// Format the log event into the output. Subsequent events will be newline-delimited.
/// </summary>
/// <param name="logEvent">The event to format.</param>
/// <param name="output">The output.</param>
public void Format(LogEvent logEvent, TextWriter output)
{
FormatEvent(logEvent, output, _valueFormatter);
FormatEvent(logEvent, output, _valueFormatter, _formatProvider);
output.WriteLine();
}

Expand All @@ -59,17 +67,29 @@ public void Format(LogEvent logEvent, TextWriter output)
/// <param name="logEvent">The event to format.</param>
/// <param name="output">The output.</param>
/// <param name="valueFormatter">A value formatter for <see cref="LogEventPropertyValue"/>s on the event.</param>
public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFormatter valueFormatter)
/// <param name="formatProvider">An <see cref="IFormatProvider"/> that will be used to render log event tokens.</param>
public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFormatter valueFormatter, IFormatProvider formatProvider)
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
if (output == null) throw new ArgumentNullException(nameof(output));
if (valueFormatter == null) throw new ArgumentNullException(nameof(valueFormatter));

output.Write("{\"@t\":\"");
output.Write(logEvent.Timestamp.UtcDateTime.ToString("O"));

output.Write("\",\"@mt\":");
JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output);

if (!formatProvider.Equals(CultureInfo.InvariantCulture))
{
// `@m` is normally created during ingestion, however, it must be sent from the client
// to honour non-default IFormatProviders
output.Write(",\"@m\":");
JsonValueFormatter.WriteQuotedJsonString(
CleanMessageTemplateFormatter.Format(logEvent.MessageTemplate, logEvent.Properties, formatProvider),
output);
}

var tokensWithFormat = logEvent.MessageTemplate.Tokens
.OfType<PropertyToken>()
.Where(pt => pt.Format != null);
Expand All @@ -85,7 +105,7 @@ public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFo
output.Write(delim);
delim = ",";
var space = new StringWriter();
r.Render(logEvent.Properties, space, CultureInfo.InvariantCulture);
r.Render(logEvent.Properties, space, formatProvider);
JsonValueFormatter.WriteQuotedJsonString(space.ToString(), output);
}
output.Write(']');
Expand Down
Loading