Skip to content

Commit

Permalink
Support {Properties} in output templates (serilog#944)
Browse files Browse the repository at this point in the history
Fixes serilog#825 - output template support for `Properties`
  • Loading branch information
Pliner authored and Twinki14 committed Dec 30, 2023
1 parent e6e97e4 commit 42bddf3
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 4 deletions.
5 changes: 5 additions & 0 deletions src/Serilog/Events/MessageTemplate.cs
Expand Up @@ -28,6 +28,11 @@ namespace Serilog.Events
/// </summary>
public class MessageTemplate
{
/// <summary>
/// Represents the empty message template.
/// </summary>
public static MessageTemplate Empty { get; } = new MessageTemplate(Enumerable.Empty<MessageTemplateToken>());

readonly MessageTemplateToken[] _tokens;

// Optimisation for when the template is bound to
Expand Down
81 changes: 81 additions & 0 deletions src/Serilog/Formatting/Display/LogEventPropertiesValue.cs
@@ -0,0 +1,81 @@
// Copyright 2017 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;

namespace Serilog.Formatting.Display
{
class LogEventPropertiesValue : LogEventPropertyValue
{
readonly MessageTemplate _template;
readonly IReadOnlyDictionary<string, LogEventPropertyValue> _properties;
readonly MessageTemplate _outputTemplate;

public LogEventPropertiesValue(MessageTemplate template, IReadOnlyDictionary<string, LogEventPropertyValue> properties, MessageTemplate outputTemplate)
{
_template = template;
_properties = properties;
_outputTemplate = outputTemplate;
}

public override void Render(TextWriter output, string format = null, IFormatProvider formatProvider = null)
{
output.Write('{');

var delim = "";
foreach (var kvp in _properties)
{
if (TemplateContainsPropertyName(_template, kvp.Key))
{
continue;
}

if (TemplateContainsPropertyName(_outputTemplate, kvp.Key))
{
continue;
}

output.Write(delim);
delim = ", ";
output.Write(kvp.Key);
output.Write(": ");
kvp.Value.Render(output, null, formatProvider);
}

output.Write('}');
}

static bool TemplateContainsPropertyName(MessageTemplate template, string propertyName)
{
if (template.NamedProperties == null)
{
return false;
}

for (var i = 0; i < template.NamedProperties.Length; i++)
{
var namedProperty = template.NamedProperties[i];
if (namedProperty.PropertyName == propertyName)
{
return true;
}
}

return false;
}
}
}
Expand Up @@ -61,8 +61,8 @@ public void Format(LogEvent logEvent, TextWriter output)
// This could be lazier: the output properties include
// everything from the log event, but often we won't need any more than
// just the standard timestamp/message etc.
var outputProperties = OutputProperties.GetOutputProperties(logEvent);
var outputProperties = OutputProperties.GetOutputProperties(logEvent, _outputTemplate);

foreach (var token in _outputTemplate.Tokens)
{
var pt = token as PropertyToken;
Expand Down
22 changes: 20 additions & 2 deletions src/Serilog/Formatting/Display/OutputProperties.cs
Expand Up @@ -52,12 +52,29 @@ public static class OutputProperties
/// </summary>
public const string ExceptionPropertyName = "Exception";

/// <summary>
/// The properties of the log event.
/// </summary>
public const string PropertiesPropertyName = "Properties";

/// <summary>
/// Create properties from the provided log event.
/// </summary>
/// <param name="logEvent">The log event.</param>
/// <returns>A dictionary with properties representing the log event.</returns>
[Obsolete("Pass the full output template using the other overload.")]
public static IReadOnlyDictionary<string, LogEventPropertyValue> GetOutputProperties(LogEvent logEvent)
{
return GetOutputProperties(logEvent, MessageTemplate.Empty);
}

/// <summary>
/// Create properties from the provided log event.
/// </summary>
/// <param name="logEvent">The log event.</param>
/// <param name="outputTemplate">The output template.</param>
/// <returns>A dictionary with properties representing the log event.</returns>
public static IReadOnlyDictionary<string, LogEventPropertyValue> GetOutputProperties(LogEvent logEvent, MessageTemplate outputTemplate)
{
var result = logEvent.Properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

Expand All @@ -69,11 +86,12 @@ public static class OutputProperties
result[TimestampPropertyName] = new ScalarValue(logEvent.Timestamp);
result[LevelPropertyName] = new LogEventLevelValue(logEvent.Level);
result[NewLinePropertyName] = LiteralNewLine;
result[PropertiesPropertyName] = new LogEventPropertiesValue(logEvent.MessageTemplate, logEvent.Properties, outputTemplate);

var exception = logEvent.Exception == null ? "" : (logEvent.Exception + Environment.NewLine);
var exception = logEvent.Exception == null ? "" : logEvent.Exception + Environment.NewLine;
result[ExceptionPropertyName] = new LiteralStringValue(exception);

return result;
}
}
}
}
Expand Up @@ -198,5 +198,25 @@ public void AppliesCustomFormatterToEnums()
formatter.Format(evt, sw);
Assert.Equal("Size Huge", sw.ToString());
}

[Fact]
public void NonMessagePropertiesAreRendered()
{
var formatter = new MessageTemplateTextFormatter("{Properties}", CultureInfo.InvariantCulture);
var evt = DelegatingSink.GetLogEvent(l => l.ForContext("Foo", 42).Information("Hello from {Bar}!", "bar"));
var sw = new StringWriter();
formatter.Format(evt, sw);
Assert.Equal("{Foo: 42}", sw.ToString());
}

[Fact]
public void DoNotDuplicatePropertiesAlreadyRenderedInOutputTemplate()
{
var formatter = new MessageTemplateTextFormatter("{Foo} {Properties}", CultureInfo.InvariantCulture);
var evt = DelegatingSink.GetLogEvent(l => l.ForContext("Foo", 42).ForContext("Bar", 42).Information("Hello from bar!"));
var sw = new StringWriter();
formatter.Format(evt, sw);
Assert.Equal("42 {Bar: 42}", sw.ToString());
}
}
}

0 comments on commit 42bddf3

Please sign in to comment.