diff --git a/src/Serilog.Expressions/Templates/Compilation/CompiledTimestampToken.cs b/src/Serilog.Expressions/Templates/Compilation/CompiledTimestampToken.cs new file mode 100644 index 0000000..c43e78e --- /dev/null +++ b/src/Serilog.Expressions/Templates/Compilation/CompiledTimestampToken.cs @@ -0,0 +1,70 @@ +// 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 Serilog.Expressions; +using Serilog.Parsing; +using Serilog.Templates.Rendering; +using Serilog.Templates.Themes; + +namespace Serilog.Templates.Compilation; + +class CompiledTimestampToken : CompiledTemplate +{ + readonly string? _format; + readonly Alignment? _alignment; + readonly IFormatProvider? _formatProvider; + readonly Style _secondaryText; + + public CompiledTimestampToken(string? format, Alignment? alignment, IFormatProvider? formatProvider, TemplateTheme theme) + { + _format = format; + _alignment = alignment; + _formatProvider = formatProvider; + _secondaryText = theme.GetStyle(TemplateThemeStyle.SecondaryText); + } + + public override void Evaluate(EvaluationContext ctx, TextWriter output) + { + var invisibleCharacterCount = 0; + + if (_alignment == null) + { + EvaluateUnaligned(ctx, output, _formatProvider, ref invisibleCharacterCount); + } + else + { + var writer = new StringWriter(); + EvaluateUnaligned(ctx, writer, _formatProvider, ref invisibleCharacterCount); + Padding.Apply(output, writer.ToString(), _alignment.Value.Widen(invisibleCharacterCount)); + } + } + + void EvaluateUnaligned(EvaluationContext ctx, TextWriter output, IFormatProvider? formatProvider, ref int invisibleCharacterCount) + { + var value = ctx.LogEvent.Timestamp; + + using var style = _secondaryText.Set(output, ref invisibleCharacterCount); + +#if FEATURE_SPAN + Span buffer = stackalloc char[36]; + if (value.TryFormat(buffer, out int charsWritten, _format, _formatProvider)) + { + output.Write(buffer[..charsWritten]); + return; + } +#endif + output.Write(value.ToString(_format, formatProvider)); + + } +} \ No newline at end of file diff --git a/src/Serilog.Expressions/Templates/Compilation/TemplateCompiler.cs b/src/Serilog.Expressions/Templates/Compilation/TemplateCompiler.cs index fc083da..f755be1 100644 --- a/src/Serilog.Expressions/Templates/Compilation/TemplateCompiler.cs +++ b/src/Serilog.Expressions/Templates/Compilation/TemplateCompiler.cs @@ -44,6 +44,10 @@ public static CompiledTemplate Compile(Template template, Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Message }, Format: null } message => encoder.Wrap(new CompiledMessageToken(formatProvider, message.Alignment, theme)), + FormattedExpression + { + Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Timestamp }, + } timestamp => encoder.Wrap(new CompiledTimestampToken(timestamp.Format, timestamp.Alignment, formatProvider, theme)), FormattedExpression expression => encoder.MakeCompiledFormattedExpression( ExpressionCompiler.Compile(expression.Expression, formatProvider, nameResolver), expression.Format, expression.Alignment, formatProvider, theme), TemplateBlock block => new CompiledTemplateBlock(block.Elements.Select(e => Compile(e, formatProvider, nameResolver, theme, encoder)).ToArray()), diff --git a/test/Serilog.Expressions.Tests/Cases/template-evaluation-cases.asv b/test/Serilog.Expressions.Tests/Cases/template-evaluation-cases.asv index 6c30df5..a77c0bf 100644 --- a/test/Serilog.Expressions.Tests/Cases/template-evaluation-cases.asv +++ b/test/Serilog.Expressions.Tests/Cases/template-evaluation-cases.asv @@ -33,3 +33,5 @@ Culture-specific {42.34} ⇶ Culture-specific 42,34 {rest()} ⇶ {"Name":"nblumhardt"} {Name} {rest()} ⇶ nblumhardt {} {rest(true)} ⇶ {} +{@t:yyyy-MM-dd HH:mm:ss.ffff zzz} ⇶ 2000-12-31 23:59:58.1230 +10:00 +{@t:yyyy-MM-dd HH:mm:ss.ffff zzz------------------} ⇶ 2000-12-31 23:59:58.1230 +10:00------------------ diff --git a/test/Serilog.Expressions.Tests/Support/Some.cs b/test/Serilog.Expressions.Tests/Support/Some.cs index aa98d64..6d73a71 100644 --- a/test/Serilog.Expressions.Tests/Support/Some.cs +++ b/test/Serilog.Expressions.Tests/Support/Some.cs @@ -12,12 +12,22 @@ public static LogEvent InformationEvent(string messageTemplate = "Hello, world!" return LogEvent(LogEventLevel.Information, messageTemplate, propertyValues); } + public static LogEvent InformationEvent(DateTimeOffset timestamp, string messageTemplate = "Hello, world!", params object?[] propertyValues) + { + return LogEvent(timestamp, LogEventLevel.Information, messageTemplate, propertyValues); + } + public static LogEvent WarningEvent(string messageTemplate = "Hello, world!", params object?[] propertyValues) { return LogEvent(LogEventLevel.Warning, messageTemplate, propertyValues); } public static LogEvent LogEvent(LogEventLevel level, string messageTemplate = "Hello, world!", params object?[] propertyValues) + { + return LogEvent(DateTimeOffset.Now, level, messageTemplate, propertyValues); + } + + public static LogEvent LogEvent(DateTimeOffset timestamp, LogEventLevel level, string messageTemplate = "Hello, world!", params object?[] propertyValues) { var log = new LoggerConfiguration().CreateLogger(); #pragma warning disable Serilog004 // Constant MessageTemplate verifier @@ -26,7 +36,7 @@ public static LogEvent LogEvent(LogEventLevel level, string messageTemplate = "H { throw new XunitException("Template could not be bound."); } - return new(DateTimeOffset.Now, level, null, template, properties); + return new(timestamp, level, null, template, properties); } public static object AnonymousObject() diff --git a/test/Serilog.Expressions.Tests/TemplateEvaluationTests.cs b/test/Serilog.Expressions.Tests/TemplateEvaluationTests.cs index 95334a1..7fa036c 100644 --- a/test/Serilog.Expressions.Tests/TemplateEvaluationTests.cs +++ b/test/Serilog.Expressions.Tests/TemplateEvaluationTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Serilog.Events; using Serilog.Expressions.Tests.Support; using Serilog.Templates; using Xunit; @@ -7,6 +8,9 @@ namespace Serilog.Expressions.Tests; public class TemplateEvaluationTests { + static readonly DateTimeOffset TestTimestamp = new( + 2000, 12, 31, 23, 59, 58, 123, TimeSpan.FromHours(10)); + public static IEnumerable TemplateEvaluationCases => AsvCases.ReadCases("template-evaluation-cases.asv"); @@ -14,7 +18,7 @@ public class TemplateEvaluationTests [MemberData(nameof(TemplateEvaluationCases))] public void TemplatesAreCorrectlyEvaluated(string template, string expected) { - var evt = Some.InformationEvent("Hello, {Name}!", "nblumhardt"); + var evt = Some.InformationEvent(TestTimestamp, "Hello, {Name}!", "nblumhardt"); var frFr = CultureInfo.GetCultureInfoByIetfLanguageTag("fr-FR"); var compiled = new ExpressionTemplate(template, formatProvider: frFr); var output = new StringWriter();