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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support {Properties} in output templates #944

Merged
merged 8 commits into from Apr 5, 2017
@@ -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;
}
}
}
@@ -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;
@@ -50,12 +50,18 @@ 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>
/// <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)
public static IReadOnlyDictionary<string, LogEventPropertyValue> GetOutputProperties(LogEvent logEvent, MessageTemplate outputTemplate)

This comment has been minimized.

Copy link
@nblumhardt

nblumhardt Apr 5, 2017

Member

Unfortunately, this will break the literate console sink binary:

https://github.com/serilog/serilog-sinks-literate/blob/dev/src/Serilog.Sinks.Literate/Sinks/Literate/LiterateConsoleSink.cs#L88

One obvious option would be to:

  1. Keep this overload
  2. Add back the one without the additional parameter, but make it forward calls to the full version (we could pass MessageTemplate.Empty as outputTemplate, so that the arguments could still be null-checked)
  3. Mark the old overload [Obsolete("Pass the full output template using the other overload.")]

An alternative, which might be worth considering, is to add the new overload as internal, mark the old one obsolete, and stop offering a public GetOutputProperties() at some time in the future. With the literate console being the only consumer, it may be defensible to simply duplicate the code there.

I like the latter alternative because it'd allow us to re-think how the MessageTemplateTextFormatter works in the future - hopefully to improve on the O(N) scans of the message templates - without then needing to break further public API surface.

Any thoughts?

{
var result = logEvent.Properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

@@ -67,11 +73,12 @@ public static class OutputProperties
result[TimestampPropertyName] = new ScalarValue(logEvent.Timestamp);
result[LevelPropertyName] = new LogEventLevelValue(logEvent.Level);
result[NewLinePropertyName] = new LiteralStringValue(Environment.NewLine);
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;
}
}
}
}
@@ -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());
}
}
}
ProTip! Use n and p to navigate between commits in a pull request.
You can鈥檛 perform that action at this time.