diff --git a/Serilog.Logfmt/Directory.Build.props b/Serilog.Logfmt/Directory.Build.props index 1c93e46..e7d771c 100644 --- a/Serilog.Logfmt/Directory.Build.props +++ b/Serilog.Logfmt/Directory.Build.props @@ -4,7 +4,7 @@ https://github.com/eiximenis/Serilog.Logfmt/ https://github.com/eiximenis/Serilog.Logfmt Serilog.Logfmt - 1.0.1$(VersionSuffix) + 1.0.2$(VersionSuffix) Eiximenis Lo Crestià true diff --git a/Serilog.Logfmt/DoubleQuotesAction.cs b/Serilog.Logfmt/DoubleQuotesAction.cs new file mode 100644 index 0000000..b1dabf4 --- /dev/null +++ b/Serilog.Logfmt/DoubleQuotesAction.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Serilog.Logfmt +{ + enum DoubleQuotesAction + { + None, + ConvertToSingle, + Escape, + Remove + } +} diff --git a/Serilog.Logfmt/IDoubleQuotesOptions.cs b/Serilog.Logfmt/IDoubleQuotesOptions.cs new file mode 100644 index 0000000..cf6d6ab --- /dev/null +++ b/Serilog.Logfmt/IDoubleQuotesOptions.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Serilog.Logfmt +{ + public interface IDoubleQuotesOptions + { + void Escape(); + void ConvertToSingle(); + void Preserve(); + void Remove(); + } +} diff --git a/Serilog.Logfmt/LogfmtFormatProvider.cs b/Serilog.Logfmt/LogfmtFormatProvider.cs new file mode 100644 index 0000000..421b721 --- /dev/null +++ b/Serilog.Logfmt/LogfmtFormatProvider.cs @@ -0,0 +1,17 @@ +using System; + +namespace Serilog.Logfmt +{ + internal class LogfmtFormatProvider : IFormatProvider, ICustomFormatter + { + public string Format(string format, object arg, IFormatProvider formatProvider) + { + return arg.ToString(); + } + + public object GetFormat(Type formatType) + { + return this; + } + } +} \ No newline at end of file diff --git a/Serilog.Logfmt/LogfmtFormatter.cs b/Serilog.Logfmt/LogfmtFormatter.cs index f4996f6..7d692bc 100644 --- a/Serilog.Logfmt/LogfmtFormatter.cs +++ b/Serilog.Logfmt/LogfmtFormatter.cs @@ -3,6 +3,7 @@ using Serilog.Formatting; using System; using System.Collections.Generic; +using System.Data.SqlTypes; using System.IO; using System.Linq; using System.Linq.Expressions; @@ -46,8 +47,8 @@ public void Format(LogEvent logEvent, TextWriter output) var msg = ""; using (var sw = new StringWriter()) { - logEvent.RenderMessage(sw); - msg = sw.ToString(); + sw.WriteMessage(logEvent); // Don't use logEvent.RenderMessage(sw) due to extra quotes added. + msg = sw.ToLogfmtQuotedString(_options.DoubleQuotesAction); } output.WriteLine("{1}{0}{1} ", msg, msg.Contains(" ") ? "\"" : ""); diff --git a/Serilog.Logfmt/LogfmtOptions.cs b/Serilog.Logfmt/LogfmtOptions.cs index 5dd696a..8e9660c 100644 --- a/Serilog.Logfmt/LogfmtOptions.cs +++ b/Serilog.Logfmt/LogfmtOptions.cs @@ -6,7 +6,7 @@ namespace Serilog.Logfmt { - public class LogfmtOptions + public class LogfmtOptions : IDoubleQuotesOptions { internal bool NormalizeCase { get; private set; } @@ -14,6 +14,8 @@ public class LogfmtOptions internal LogExceptionOptions ExceptionOptions {get;} + internal DoubleQuotesAction DoubleQuotesAction { get; private set; } + internal Func PropertyKeyFilter { get; private set; } public LogfmtOptions() @@ -22,6 +24,7 @@ public LogfmtOptions() GrafanaLevels = true; PropertyKeyFilter = k => false; ExceptionOptions = new LogExceptionOptions(); + DoubleQuotesAction = DoubleQuotesAction.ConvertToSingle; } public LogfmtOptions PreserveCase() @@ -30,6 +33,12 @@ public LogfmtOptions PreserveCase() return this; } + public LogfmtOptions OnDoubleQuotes(Action optionsAction) + { + optionsAction?.Invoke(this); + return this; + } + public LogfmtOptions PreserveSerilogLogLevels() { GrafanaLevels = false; @@ -46,7 +55,24 @@ public LogfmtOptions OnException(Action optionsAction) { optionsAction?.Invoke(ExceptionOptions); return this; - } + } + void IDoubleQuotesOptions.Escape() + { + DoubleQuotesAction = DoubleQuotesAction.Escape; + } + + void IDoubleQuotesOptions.ConvertToSingle() + { + DoubleQuotesAction = DoubleQuotesAction.ConvertToSingle; + } + void IDoubleQuotesOptions.Preserve() + { + DoubleQuotesAction = DoubleQuotesAction.None; + } + void IDoubleQuotesOptions.Remove() + { + DoubleQuotesAction = DoubleQuotesAction.Remove; + } } } diff --git a/Serilog.Logfmt/LogfmtValueFormatter.cs b/Serilog.Logfmt/LogfmtValueFormatter.cs index 3098575..f3d3149 100644 --- a/Serilog.Logfmt/LogfmtValueFormatter.cs +++ b/Serilog.Logfmt/LogfmtValueFormatter.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel.Design; using System.IO; +using System.Linq.Expressions; using System.Text; namespace Serilog.Logfmt @@ -31,6 +32,22 @@ protected override bool VisitScalarValue(TextWriter state, ScalarValue scalar) { var svalue = scalar.Value?.ToString() ?? ""; var needQuotes = svalue.Contains(" "); + if (needQuotes) + { + switch (_options.DoubleQuotesAction) + { + case DoubleQuotesAction.ConvertToSingle: + svalue = svalue.Replace('"', '\''); + break; + case DoubleQuotesAction.Remove: + svalue = svalue.Replace(@"""", ""); + break; + case DoubleQuotesAction.Escape: + svalue = svalue.Replace(@"""", @"\"""); + break; + default: break; + } + } state.Write("{1}{0}{1}", svalue, needQuotes ? "\"" : ""); return false; } diff --git a/Serilog.Logfmt/StringWriterExtensions.cs b/Serilog.Logfmt/StringWriterExtensions.cs new file mode 100644 index 0000000..d579038 --- /dev/null +++ b/Serilog.Logfmt/StringWriterExtensions.cs @@ -0,0 +1,59 @@ +using Serilog.Events; +using Serilog.Parsing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Serilog.Logfmt +{ + static class StringWriterExtensions + { + public static string ToLogfmtQuotedString(this StringWriter sw, DoubleQuotesAction action) + { + var sb = sw.GetStringBuilder(); + switch (action) + { + case DoubleQuotesAction.ConvertToSingle: + sb.Replace('"', '\''); + break; + case DoubleQuotesAction.Escape: + sb.Replace(@"""", @"\"""); + break; + case DoubleQuotesAction.Remove: + sb.Replace(@"""", ""); + break; + default: // None + break; + } + + return sb.ToString(); + } + + // From https://github.com/serilog/serilog/issues/936 for avoiding extra quotes on strings + public static void WriteMessage(this TextWriter tw, LogEvent logEvent) + { + Predicate isString = pv => + { + var sv = pv as ScalarValue; + return (sv != null) && (sv.Value as string) != null; + }; + foreach (var t in logEvent.MessageTemplate.Tokens) + { + var pt = t as PropertyToken; + LogEventPropertyValue propVal; + if (pt != null && + logEvent.Properties.TryGetValue(pt.PropertyName, out propVal) && + isString(propVal)) + { + tw.Write(((ScalarValue)propVal).Value); + } + else + { + t.Render(logEvent.Properties, tw, null); + } + } + } + } +} diff --git a/TestApi/Program.cs b/TestApi/Program.cs index 65f5dbe..4a9c3a9 100644 --- a/TestApi/Program.cs +++ b/TestApi/Program.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Serilog; using Serilog.Logfmt; +using Serilog.Sinks.SystemConsole.Themes; using System; using System.Collections.Generic; using System.Linq; @@ -25,7 +26,8 @@ public static IHostBuilder CreateHostBuilder(string[] args) => { config.MinimumLevel.Verbose() .Enrich.FromLogContext() - .WriteTo.Console(new LogfmtFormatter()); + // .WriteTo.Console() + .WriteTo.Console(formatter: new LogfmtFormatter()); }) .ConfigureWebHostDefaults(webBuilder => { diff --git a/TestApi/Startup.cs b/TestApi/Startup.cs index e20d8cb..e991efc 100644 --- a/TestApi/Startup.cs +++ b/TestApi/Startup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -19,7 +20,7 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory lf) { if (env.IsDevelopment()) { @@ -32,6 +33,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapGet("/", async context => { + var logger = lf.CreateLogger("Startup"); + logger.LogInformation(@"Message with ""double quotes"" :) "); + await context.Response.WriteAsync("Hello World!"); }); });