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

Console styles improvements #17

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,15 @@ Log.Information("Hello, browser!");
```

A more detailed example is available [in this repository](https://github.com/serilog/serilog-sinks-browserconsole/tree/dev/example/ExampleClient).

### Styling your stuff

In your sink's `outputTemplate` parameter, you can leverage [`console.log` styling capabilities](https://developer.mozilla.org/en-US/docs/Web/API/console#styling_console_output) by using the `<<...>>` placeholder. Use `<<_>>` to reset styles to defaults.

Example:

```
<<color: red;>>{Level}<<_>>: {Message}
```

You can also define styles for tokens via the `tokenStyles` dictionary.
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,20 @@ namespace Serilog;
public static class LoggerConfigurationBrowserConsoleExtensions
{
const string SerilogToken =
"%cserilog{_}color:white;background:#8c7574;border-radius:3px;padding:1px 2px;font-weight:600;";
const string DefaultConsoleOutputTemplate = SerilogToken + "{Message}{NewLine}{Exception}";
"<<color:white;background:#8c7574;border-radius:3px;padding:1px 2px;font-weight:600;>>serilog<<_>>";

const string DefaultConsoleOutputTemplate = SerilogToken + "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}";

/// <summary>
/// Writes log events to the browser console.
/// </summary>
/// <param name="sinkConfiguration">Logger sink configuration.</param>
/// <param name="restrictedToMinimumLevel">The minimum level for
/// events passed through the sink. Ignored when <paramref name="levelSwitch"/> is specified.</param>
/// <param name="outputTemplate">A message template describing the format used to write to the sink.
/// The default is <code>"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"</code>.</param>
/// The default is <code>"(serilog) [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"</code>.</param>
/// <param name="formatProvider">Supplies culture-specific formatting information, or null.</param>
/// <param name="tokenStyles">A dictionary of styles to apply to tokens. See <a href="https://developer.mozilla.org/en-US/docs/Web/API/console#styling_console_output">MDN about console styling</a></param>
/// <param name="levelSwitch">A switch allowing the pass-through minimum level
/// to be changed at runtime.</param>
/// <param name="jsRuntime">An instance of <see cref="IJSRuntime"/> to interact with the browser.</param>
Expand All @@ -49,11 +50,12 @@ public static LoggerConfiguration BrowserConsole(
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
string outputTemplate = DefaultConsoleOutputTemplate,
IFormatProvider? formatProvider = null,
IReadOnlyDictionary<string, string>? tokenStyles = null,
LoggingLevelSwitch? levelSwitch = null,
IJSRuntime? jsRuntime = null)
{
ArgumentNullException.ThrowIfNull(sinkConfiguration);
var formatter = new OutputTemplateRenderer(outputTemplate, formatProvider);
var formatter = new OutputTemplateRenderer(outputTemplate, formatProvider, tokenStyles);
return sinkConfiguration.Sink(new BrowserConsoleSink(jsRuntime, formatter), restrictedToMinimumLevel, levelSwitch);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken)
if (!logEvent.Properties.TryGetValue(_token.PropertyName, out var propertyValue))
{
if (_token.Alignment is not null)
emitToken(Padding.Apply(string.Empty, _token.Alignment));
emitToken.Literal(Padding.Apply(string.Empty, _token.Alignment));
return;
}

Expand All @@ -55,8 +55,8 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken)

var str = writer.ToString();
if (_token.Alignment is not null)
emitToken(Padding.Apply(str, _token.Alignment));
emitToken.Text(Padding.Apply(str, _token.Alignment));
else
emitToken(str);
emitToken.Text(str);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ class ExceptionTokenRenderer : OutputTemplateTokenRenderer
public override void Render(LogEvent logEvent, TokenEmitter emitToken)
{
if (logEvent.Exception is not null)
emitToken(logEvent.Exception.ToString());
emitToken.Text(logEvent.Exception.ToString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken)
{
var moniker = LevelOutputFormat.GetLevelMoniker(logEvent.Level, _levelToken.Format);
var alignedOutput = Padding.Apply(moniker, _levelToken.Alignment);
emitToken(alignedOutput);
emitToken.Text(alignedOutput);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken)
switch (token)
{
case TextToken tt:
emitToken(tt.Text);
new TextTokenRenderer(tt.Text).Render(logEvent, emitToken);
break;
case PropertyToken pt:
if (logEvent.Properties.TryGetValue(pt.PropertyName, out var propertyValue))
emitToken(ObjectModelInterop.ToInteropValue(propertyValue));
{
new PropertyTokenRenderer(pt, propertyValue).Render(logEvent, emitToken);
}
break;
default:
throw new InvalidOperationException();
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public NewLineTokenRenderer(Alignment? alignment)
public override void Render(LogEvent logEvent, TokenEmitter emitToken)
{
if (_alignment is not null)
emitToken(Padding.Apply(Environment.NewLine, _alignment.Value.Widen(Environment.NewLine.Length)));
emitToken.Literal(Padding.Apply(Environment.NewLine, _alignment.Value.Widen(Environment.NewLine.Length)));
else
emitToken(Environment.NewLine);
emitToken.Literal(Environment.NewLine);
}
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,65 @@
using Serilog.Events;
using Serilog.Formatting.Display;
using Serilog.Parsing;
using System;

namespace Serilog.Sinks.BrowserConsole.Output;

class OutputTemplateRenderer
{
readonly OutputTemplateTokenRenderer[] _renderers;
private readonly IFormatProvider? _formatProvider;
private readonly IReadOnlyDictionary<string, string>? _tokenStyles;
private readonly MessageTemplate _template;

public OutputTemplateRenderer(string outputTemplate, IFormatProvider? formatProvider)
public OutputTemplateRenderer(string outputTemplate, IFormatProvider? formatProvider, IReadOnlyDictionary<string, string>? tokenStyles = default)
{
ArgumentNullException.ThrowIfNull(outputTemplate);
var template = new MessageTemplateParser().Parse(outputTemplate);

_renderers = template.Tokens
.Select(token => token switch
_formatProvider = formatProvider;
_tokenStyles = tokenStyles;
_template = new MessageTemplateParser().Parse(outputTemplate);

_renderers = _template.Tokens
.SelectMany(token => token switch
{
TextToken tt => new TextTokenRenderer(tt.Text),
PropertyToken pt => pt.PropertyName switch
{
OutputProperties.LevelPropertyName => new LevelTokenRenderer(pt) as OutputTemplateTokenRenderer,
OutputProperties.NewLinePropertyName => new NewLineTokenRenderer(pt.Alignment),
OutputProperties.ExceptionPropertyName => new ExceptionTokenRenderer(),
OutputProperties.MessagePropertyName => new MessageTemplateOutputTokenRenderer(),
OutputProperties.TimestampPropertyName => new TimestampTokenRenderer(pt, formatProvider),
OutputProperties.PropertiesPropertyName => new PropertiesTokenRenderer(pt, template),
_ => new EventPropertyTokenRenderer(pt, formatProvider)
},
TextToken tt => [new TextTokenRenderer(tt.Text)],
PropertyToken pt => WrapStyle(pt),
_ => throw new InvalidOperationException()
})
.ToArray();
}

private OutputTemplateTokenRenderer[] WrapStyle(PropertyToken token)
{
OutputTemplateTokenRenderer renderer = token.PropertyName switch
{
OutputProperties.LevelPropertyName => new LevelTokenRenderer(token),
OutputProperties.NewLinePropertyName => new NewLineTokenRenderer(token.Alignment),
OutputProperties.ExceptionPropertyName => new ExceptionTokenRenderer(),
OutputProperties.MessagePropertyName => new MessageTemplateOutputTokenRenderer(),
OutputProperties.TimestampPropertyName => new TimestampTokenRenderer(token, _formatProvider),
OutputProperties.PropertiesPropertyName => new PropertiesTokenRenderer(token, _template),
_ => new EventPropertyTokenRenderer(token, _formatProvider)
};
if (_tokenStyles?.TryGetValue(token.PropertyName, out var style) ?? false)
{
return [new StyleTokenRenderer(style), renderer, StyleTokenRenderer.Reset];
}
else
{
return [renderer];
}
}

public object?[] Format(LogEvent logEvent)
{
ArgumentNullException.ThrowIfNull(logEvent);

var buffer = new List<object?>(_renderers.Length * 2);
var tokenEmitter = new TokenEmitter();
foreach (var renderer in _renderers)
{
renderer.Render(logEvent, buffer.Add);
renderer.Render(logEvent, tokenEmitter);
}
return buffer.ToArray();
return tokenEmitter.YieldArgs();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,53 @@
// limitations under the License.

using Serilog.Events;
using System.Text;
using static System.Net.Mime.MediaTypeNames;

namespace Serilog.Sinks.BrowserConsole.Output;

delegate void TokenEmitter(object? token);
class TokenEmitter
{
private StringBuilder _template = new();
private List<object?> _args = [];

internal void Literal(string template)
{
_template.Append(template.Replace("%", "%%"));
}

internal void Text(object @string)
{
_template.Append("%s");
_args.Add(@string);
}
internal void Text(string @string) => Text((object)@string);

internal void Object(object? @object)
{
_template.Append("%o");
_args.Add(@object);
}

internal void Integer(object @int)
{
_template.Append("%d");
_args.Add(@int);
}

internal void Float(object @float) {
_template.Append("%f");
_args.Add(@float);
}

internal object?[] YieldArgs() => [_template.ToString(), .. _args];

internal void Style(string styleContent)
{
_template.Append("%c");
_args.Add(styleContent);
}
}

abstract class OutputTemplateTokenRenderer
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,48 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken)

foreach (var property in included)
{
emitToken(ObjectModelInterop.ToInteropValue(property.Value, _token.Format));
new PropertyTokenRenderer(_token, property.Value).Render(logEvent, emitToken);
}
}
private void HandleProperty(LogEventProperty property, TokenEmitter emitToken)
{
if (property.Value is ScalarValue sv)
{
if(sv.Value is null)
{
emitToken.Object(ObjectModelInterop.ToInteropValue(property.Value, _token.Format));
return;
}
switch (Type.GetTypeCode(sv.Value.GetType()))
{
// See https://stackoverflow.com/a/1750024
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
emitToken.Integer(sv.Value);
break;
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Single:
emitToken.Float(sv.Value);
break;
case TypeCode.String:
case TypeCode.Char:
emitToken.Text(sv.Value);
break;
default:
emitToken.Object(ObjectModelInterop.ToInteropValue(property.Value, _token.Format));
break;
}
}
else
emitToken.Object(ObjectModelInterop.ToInteropValue(property.Value, _token.Format));
}

static bool TemplateContainsPropertyName(MessageTemplate template, string propertyName)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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.Linq;
using Serilog.Events;
using Serilog.Parsing;

namespace Serilog.Sinks.BrowserConsole.Output
{
class PropertyTokenRenderer : OutputTemplateTokenRenderer
{
readonly PropertyToken _token;
readonly LogEventPropertyValue _propertyValue;
public PropertyTokenRenderer(PropertyToken token, LogEventPropertyValue propertyValue)
{
_token = token;
_propertyValue = propertyValue;
}

public override void Render(LogEvent logEvent, TokenEmitter emitToken)
{
if (_propertyValue is ScalarValue sv)
{
if (sv.Value is null)
{
emitToken.Object(ObjectModelInterop.ToInteropValue(sv));
return;
}
switch (Type.GetTypeCode(sv.Value.GetType()))
{
// See https://stackoverflow.com/a/1750024
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
emitToken.Integer(sv.Value);
break;
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Single:
emitToken.Float(sv.Value);
break;
case TypeCode.String:
case TypeCode.Char:
emitToken.Text(sv.Value);
break;
default:
emitToken.Object(ObjectModelInterop.ToInteropValue(sv));
break;
}
}
else
emitToken.Object(ObjectModelInterop.ToInteropValue(_propertyValue));
}
}
}
Loading