diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.cs b/src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.cs index 3012421b0901b5..d70c3227cae4ec 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.cs @@ -11,10 +11,23 @@ public static partial class ConsoleLoggerExtensions { public static Microsoft.Extensions.Logging.ILoggingBuilder AddConsole(this Microsoft.Extensions.Logging.ILoggingBuilder builder) { throw null; } public static Microsoft.Extensions.Logging.ILoggingBuilder AddConsole(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action configure) { throw null; } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddConsole(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string formatterName) { throw null; } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddConsoleLogFormatter(this Microsoft.Extensions.Logging.ILoggingBuilder builder) where TFormatter : class, Microsoft.Extensions.Logging.Console.IConsoleLogFormatter where TOptions : class { throw null; } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddConsoleLogFormatter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action configure) where TFormatter : class, Microsoft.Extensions.Logging.Console.IConsoleLogFormatter where TOptions : class { throw null; } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddDefaultConsoleLogFormatter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action configure) { throw null; } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddJsonConsoleLogFormatter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action configure) { throw null; } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddSystemdConsoleLogFormatter(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action configure) { throw null; } } } namespace Microsoft.Extensions.Logging.Console { + public static partial class ConsoleLogFormatterNames + { + public const string Default = "default"; + public const string Json = "json"; + public const string Systemd = "systemd"; + } + [System.ObsoleteAttribute("ConsoleLoggerFormat has been deprecated.", false)] public enum ConsoleLoggerFormat { Default = 0, @@ -23,19 +36,64 @@ public enum ConsoleLoggerFormat public partial class ConsoleLoggerOptions { public ConsoleLoggerOptions() { } + [System.ObsoleteAttribute("ConsoleLoggerOptions.DisableColors has been deprecated. Please use ColoredConsoleLogFormatterOptions.DisableColors instead.", false)] public bool DisableColors { get { throw null; } set { } } + [System.ObsoleteAttribute("ConsoleLoggerOptions.Format has been deprecated. Please use ConsoleLoggerOptions.FormatterName instead.", false)] public Microsoft.Extensions.Logging.Console.ConsoleLoggerFormat Format { get { throw null; } set { } } + public string FormatterName { get { throw null; } set { } } + [System.ObsoleteAttribute("ConsoleLoggerOptions.IncludeScopes has been deprecated..", false)] public bool IncludeScopes { get { throw null; } set { } } + [System.ObsoleteAttribute("ConsoleLoggerOptions.LogToStandardErrorThreshold has been deprecated..", false)] public Microsoft.Extensions.Logging.LogLevel LogToStandardErrorThreshold { get { throw null; } set { } } + [System.ObsoleteAttribute("ConsoleLoggerOptions.TimestampFormat has been deprecated..", false)] public string TimestampFormat { get { throw null; } set { } } + [System.ObsoleteAttribute("ConsoleLoggerOptions.UseUtcTimestamp has been deprecated..", false)] public bool UseUtcTimestamp { get { throw null; } set { } } } [Microsoft.Extensions.Logging.ProviderAliasAttribute("Console")] public partial class ConsoleLoggerProvider : Microsoft.Extensions.Logging.ILoggerProvider, Microsoft.Extensions.Logging.ISupportExternalScope, System.IDisposable { public ConsoleLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor options) { } + public ConsoleLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor options, System.Collections.Generic.IEnumerable formatters) { } public Microsoft.Extensions.Logging.ILogger CreateLogger(string name) { throw null; } public void Dispose() { } public void SetScopeProvider(Microsoft.Extensions.Logging.IExternalScopeProvider scopeProvider) { } } + public readonly partial struct ConsoleMessage + { + public readonly System.ConsoleColor? Background; + public readonly System.ConsoleColor? Foreground; + public readonly string Message; + public ConsoleMessage(string message, System.ConsoleColor? background = default(System.ConsoleColor?), System.ConsoleColor? foreground = default(System.ConsoleColor?)) { throw null; } + } + public partial class DefaultConsoleLogFormatterOptions : Microsoft.Extensions.Logging.Console.SystemdConsoleLogFormatterOptions + { + public DefaultConsoleLogFormatterOptions() { } + public bool DisableColors { get { throw null; } set { } } + public bool MultiLine { get { throw null; } set { } } + } + public partial interface IConsoleLogFormatter + { + string Name { get; } + Microsoft.Extensions.Logging.Console.LogMessageEntry Format(Microsoft.Extensions.Logging.LogLevel logLevel, string logName, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception exception, System.Func formatter, Microsoft.Extensions.Logging.IExternalScopeProvider scopeProvider); + } + public partial class JsonConsoleLogFormatterOptions : Microsoft.Extensions.Logging.Console.SystemdConsoleLogFormatterOptions + { + public JsonConsoleLogFormatterOptions() { } + public System.Text.Json.JsonWriterOptions JsonWriterOptions { get { throw null; } set { } } + } + public readonly partial struct LogMessageEntry + { + public readonly bool LogAsError; + public readonly Microsoft.Extensions.Logging.Console.ConsoleMessage[] Messages; + public LogMessageEntry(Microsoft.Extensions.Logging.Console.ConsoleMessage[] messages, bool logAsError = false) { throw null; } + } + public partial class SystemdConsoleLogFormatterOptions + { + public SystemdConsoleLogFormatterOptions() { } + public bool IncludeScopes { get { throw null; } set { } } + public Microsoft.Extensions.Logging.LogLevel LogToStandardErrorThreshold { get { throw null; } set { } } + public string TimestampFormat { get { throw null; } set { } } + public bool UseUtcTimestamp { get { throw null; } set { } } + } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.csproj b/src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.csproj index 17f4ef6d337e9a..0756d569fde5c5 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.csproj +++ b/src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.csproj @@ -5,6 +5,7 @@ + diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs index 46271360b19fea..14620aadf64867 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs @@ -3,33 +3,19 @@ // See the LICENSE file in the project root for more information. using System; +using System.CodeDom; +using System.Collections.Generic; using System.Diagnostics; -using System.Text; +using System.Linq; namespace Microsoft.Extensions.Logging.Console { internal class ConsoleLogger : ILogger { - private static readonly string _loglevelPadding = ": "; - private static readonly string _messagePadding; - private static readonly string _newLineWithMessagePadding; - - // ConsoleColor does not have a value to specify the 'Default' color - private readonly ConsoleColor? DefaultConsoleColor = null; private readonly string _name; private readonly ConsoleLoggerProcessor _queueProcessor; - [ThreadStatic] - private static StringBuilder _logBuilder; - - static ConsoleLogger() - { - var logLevelString = GetLogLevelString(LogLevel.Information); - _messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length); - _newLineWithMessagePadding = Environment.NewLine + _messagePadding; - } - internal ConsoleLogger(string name, ConsoleLoggerProcessor loggerProcessor) { if (name == null) @@ -43,7 +29,7 @@ internal ConsoleLogger(string name, ConsoleLoggerProcessor loggerProcessor) internal IExternalScopeProvider ScopeProvider { get; set; } - internal ConsoleLoggerOptions Options { get; set; } + internal IConsoleLogFormatter Formatter { get; set; } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { @@ -58,289 +44,18 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } var message = formatter(state, exception); - if (!string.IsNullOrEmpty(message) || exception != null) { - WriteMessage(logLevel, _name, eventId.Id, message, exception); - } - } - - public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string message, Exception exception) - { - var format = Options.Format; - Debug.Assert(format >= ConsoleLoggerFormat.Default && format <= ConsoleLoggerFormat.Systemd); - - var logBuilder = _logBuilder; - _logBuilder = null; - - if (logBuilder == null) - { - logBuilder = new StringBuilder(); - } - - LogMessageEntry entry; - if (format == ConsoleLoggerFormat.Default) - { - entry = CreateDefaultLogMessage(logBuilder, logLevel, logName, eventId, message, exception); - } - else if (format == ConsoleLoggerFormat.Systemd) - { - entry = CreateSystemdLogMessage(logBuilder, logLevel, logName, eventId, message, exception); - } - else - { - entry = default; - } - _queueProcessor.EnqueueMessage(entry); - - logBuilder.Clear(); - if (logBuilder.Capacity > 1024) - { - logBuilder.Capacity = 1024; - } - _logBuilder = logBuilder; - } - - private LogMessageEntry CreateDefaultLogMessage(StringBuilder logBuilder, LogLevel logLevel, string logName, int eventId, string message, Exception exception) - { - // Example: - // INFO: ConsoleApp.Program[10] - // Request received - - var logLevelColors = GetLogLevelConsoleColors(logLevel); - var logLevelString = GetLogLevelString(logLevel); - // category and event id - logBuilder.Append(_loglevelPadding); - logBuilder.Append(logName); - logBuilder.Append('['); - logBuilder.Append(eventId); - logBuilder.AppendLine("]"); - - // scope information - GetScopeInformation(logBuilder, multiLine: true); - - if (!string.IsNullOrEmpty(message)) - { - // message - logBuilder.Append(_messagePadding); - - var len = logBuilder.Length; - logBuilder.AppendLine(message); - logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length); - } - - // Example: - // System.InvalidOperationException - // at Namespace.Class.Function() in File:line X - if (exception != null) - { - // exception message - logBuilder.AppendLine(exception.ToString()); - } - - string timestamp = null; - var timestampFormat = Options.TimestampFormat; - if (timestampFormat != null) - { - var dateTime = GetCurrentDateTime(); - timestamp = dateTime.ToString(timestampFormat); - } - - return new LogMessageEntry( - message: logBuilder.ToString(), - timeStamp: timestamp, - levelString: logLevelString, - levelBackground: logLevelColors.Background, - levelForeground: logLevelColors.Foreground, - messageColor: DefaultConsoleColor, - logAsError: logLevel >= Options.LogToStandardErrorThreshold - ); - } - - private LogMessageEntry CreateSystemdLogMessage(StringBuilder logBuilder, LogLevel logLevel, string logName, int eventId, string message, Exception exception) - { - // systemd reads messages from standard out line-by-line in a 'message' format. - // newline characters are treated as message delimiters, so we must replace them. - // Messages longer than the journal LineMax setting (default: 48KB) are cropped. - // Example: - // <6>ConsoleApp.Program[10] Request received - - // loglevel - var logLevelString = GetSyslogSeverityString(logLevel); - logBuilder.Append(logLevelString); - - // timestamp - var timestampFormat = Options.TimestampFormat; - if (timestampFormat != null) - { - var dateTime = GetCurrentDateTime(); - logBuilder.Append(dateTime.ToString(timestampFormat)); - } - - // category and event id - logBuilder.Append(logName); - logBuilder.Append('['); - logBuilder.Append(eventId); - logBuilder.Append(']'); - - // scope information - GetScopeInformation(logBuilder, multiLine: false); - - // message - if (!string.IsNullOrEmpty(message)) - { - logBuilder.Append(' '); - // message - AppendAndReplaceNewLine(logBuilder, message); - } - - // exception - // System.InvalidOperationException at Namespace.Class.Function() in File:line X - if (exception != null) - { - logBuilder.Append(' '); - AppendAndReplaceNewLine(logBuilder, exception.ToString()); - } - - // newline delimiter - logBuilder.Append(Environment.NewLine); - - return new LogMessageEntry( - message: logBuilder.ToString(), - logAsError: logLevel >= Options.LogToStandardErrorThreshold - ); - - static void AppendAndReplaceNewLine(StringBuilder sb, string message) - { - var len = sb.Length; - sb.Append(message); - sb.Replace(Environment.NewLine, " ", len, message.Length); + var entry = Formatter.Format(logLevel, _name, eventId.Id, state, exception, formatter, ScopeProvider); + _queueProcessor.EnqueueMessage(entry); } } - private DateTime GetCurrentDateTime() - { - return Options.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now; - } - public bool IsEnabled(LogLevel logLevel) { return logLevel != LogLevel.None; } public IDisposable BeginScope(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance; - - private static string GetLogLevelString(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Trace: - return "trce"; - case LogLevel.Debug: - return "dbug"; - case LogLevel.Information: - return "info"; - case LogLevel.Warning: - return "warn"; - case LogLevel.Error: - return "fail"; - case LogLevel.Critical: - return "crit"; - default: - throw new ArgumentOutOfRangeException(nameof(logLevel)); - } - } - - private static string GetSyslogSeverityString(LogLevel logLevel) - { - // 'Syslog Message Severities' from https://tools.ietf.org/html/rfc5424. - switch (logLevel) - { - case LogLevel.Trace: - case LogLevel.Debug: - return "<7>"; // debug-level messages - case LogLevel.Information: - return "<6>"; // informational messages - case LogLevel.Warning: - return "<4>"; // warning conditions - case LogLevel.Error: - return "<3>"; // error conditions - case LogLevel.Critical: - return "<2>"; // critical conditions - default: - throw new ArgumentOutOfRangeException(nameof(logLevel)); - } - } - - private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel) - { - if (Options.DisableColors) - { - return new ConsoleColors(null, null); - } - - // We must explicitly set the background color if we are setting the foreground color, - // since just setting one can look bad on the users console. - switch (logLevel) - { - case LogLevel.Critical: - return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red); - case LogLevel.Error: - return new ConsoleColors(ConsoleColor.Black, ConsoleColor.Red); - case LogLevel.Warning: - return new ConsoleColors(ConsoleColor.Yellow, ConsoleColor.Black); - case LogLevel.Information: - return new ConsoleColors(ConsoleColor.DarkGreen, ConsoleColor.Black); - case LogLevel.Debug: - return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black); - case LogLevel.Trace: - return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black); - default: - return new ConsoleColors(DefaultConsoleColor, DefaultConsoleColor); - } - } - - private void GetScopeInformation(StringBuilder stringBuilder, bool multiLine) - { - var scopeProvider = ScopeProvider; - if (Options.IncludeScopes && scopeProvider != null) - { - var initialLength = stringBuilder.Length; - - scopeProvider.ForEachScope((scope, state) => - { - var (builder, paddAt) = state; - var padd = paddAt == builder.Length; - if (padd) - { - builder.Append(_messagePadding); - builder.Append("=> "); - } - else - { - builder.Append(" => "); - } - builder.Append(scope); - }, (stringBuilder, multiLine ? initialLength : -1)); - - if (stringBuilder.Length > initialLength && multiLine) - { - stringBuilder.AppendLine(); - } - } - } - - private readonly struct ConsoleColors - { - public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background) - { - Foreground = foreground; - Background = background; - } - - public ConsoleColor? Foreground { get; } - - public ConsoleColor? Background { get; } - } } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFactoryExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFactoryExtensions.cs index 689912523105a5..75d52700c8a529 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFactoryExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFactoryExtensions.cs @@ -5,8 +5,17 @@ using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Configuration; using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace Microsoft.Extensions.Logging { @@ -20,8 +29,13 @@ public static ILoggingBuilder AddConsole(this ILoggingBuilder builder) { builder.AddConfiguration(); + builder.AddConsoleLogFormatter(); + builder.AddConsoleLogFormatter(); + builder.AddConsoleLogFormatter(); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); LoggerProviderOptions.RegisterProviderOptions(builder.Services); + return builder; } @@ -42,5 +56,140 @@ public static ILoggingBuilder AddConsole(this ILoggingBuilder builder, Action + /// Adds a console logger named 'Console' to the factory. + /// + /// The to use. + /// Formatter name selected for the . + public static ILoggingBuilder AddConsole(this ILoggingBuilder builder, string formatterName) + { + if (formatterName == null) + { + throw new ArgumentNullException(nameof(formatterName)); + } + + Action configure = (options) => { options.FormatterName = formatterName; }; + return builder.AddConsole(configure); + } + + /// + /// Add and configure a console log formatter named 'json' to the factory. + /// + /// The to use. + /// A delegate to configure the options for the built-in default log formatter. + public static ILoggingBuilder AddDefaultConsoleLogFormatter(this ILoggingBuilder builder, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + builder.AddConsole(); + builder.Services.Configure(configure); + + Action configureFormatter = (options) => { options.FormatterName = ConsoleLogFormatterNames.Default; }; + builder.Services.Configure(configureFormatter); + + return builder; + } + + /// + /// Add and configure a console log formatter named 'json' to the factory. + /// + /// The to use. + /// A delegate to configure the options for the built-in json log formatter. + public static ILoggingBuilder AddJsonConsoleLogFormatter(this ILoggingBuilder builder, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + builder.AddConsole(); + builder.Services.Configure(configure); + + Action configureFormatter = (options) => { options.FormatterName = ConsoleLogFormatterNames.Json; }; + builder.Services.Configure(configureFormatter); + + return builder; + } + + /// + /// Add and configure a console log formatter named 'systemd' to the factory. + /// + /// The to use. + /// A delegate to configure the options for the built-in systemd log formatter. + public static ILoggingBuilder AddSystemdConsoleLogFormatter(this ILoggingBuilder builder, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + builder.AddConsole(); + builder.Services.Configure(configure); + + Action configureFormatter = (options) => { options.FormatterName = ConsoleLogFormatterNames.Systemd; }; + builder.Services.Configure(configureFormatter); + + return builder; + } + + /// + /// Adds a custom console logger formatter 'TFormatter' to be configured with options 'TOptions'. + /// + /// The to use. + public static ILoggingBuilder AddConsoleLogFormatter(this ILoggingBuilder builder) + where TOptions : class + where TFormatter : class, IConsoleLogFormatter + { + builder.AddConfiguration(); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.AddSingleton, LogFormatterOptionsSetup>(); + builder.Services.AddSingleton, LoggerProviderOptionsChangeTokenSource>(); + + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + builder.Services.AddOptions().Bind(configuration.GetSection("Logging:Console:FormatterOptions")); + builder.Services.Configure(configuration.GetSection("Logging:Console:FormatterOptions")); + + // todo: configure and bind Console:Loggimg:FormatterName + + return builder; + } + + /// + /// Adds a custom console logger formatter 'TFormatter' to be configured with options 'TOptions'. + /// + /// The to use. + /// A delegate to configure options 'TOptions' for custom formatter 'TFormatter'. + public static ILoggingBuilder AddConsoleLogFormatter(this ILoggingBuilder builder, Action configure) + where TOptions : class + where TFormatter : class, IConsoleLogFormatter + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + builder.AddConsoleLogFormatter(); + + builder.Services.Configure(configure); + + return builder; + } + } + + internal class LogFormatterOptionsSetup : ConfigureFromConfigurationOptions + where TOptions : class + where TFormatter : class, IConsoleLogFormatter + { + public LogFormatterOptionsSetup(ILoggerProviderConfiguration providerConfiguration) + : base(providerConfiguration.Configuration) + { + } } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFormat.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFormat.cs index 5ab4e865c3bfd5..39306a773e9dae 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFormat.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFormat.cs @@ -7,6 +7,7 @@ namespace Microsoft.Extensions.Logging.Console /// /// Format of messages. /// + [System.ObsoleteAttribute("ConsoleLoggerFormat has been deprecated.", false)] public enum ConsoleLoggerFormat { /// diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerOptions.cs index a2fb5967231b6c..b5ea0d9b39f531 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerOptions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerOptions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Text.Json; namespace Microsoft.Extensions.Logging.Console { @@ -11,47 +12,49 @@ namespace Microsoft.Extensions.Logging.Console /// public class ConsoleLoggerOptions { - private ConsoleLoggerFormat _format = ConsoleLoggerFormat.Default; - - /// - /// Includes scopes when . - /// - public bool IncludeScopes { get; set; } - - /// - /// Disables colors when . - /// + [System.ObsoleteAttribute("ConsoleLoggerOptions.DisableColors has been deprecated. Please use ColoredConsoleLogFormatterOptions.DisableColors instead.", false)] public bool DisableColors { get; set; } - + /// /// Gets or sets log message format. Defaults to . /// + [System.ObsoleteAttribute("ConsoleLoggerOptions.Format has been deprecated. Please use ConsoleLoggerOptions.FormatterName instead.", false)] public ConsoleLoggerFormat Format { - get => _format; + get + { + if (FormatterName != null && FormatterName.Equals(ConsoleLogFormatterNames.Systemd, StringComparison.OrdinalIgnoreCase)) + return ConsoleLoggerFormat.Systemd; + return ConsoleLoggerFormat.Default; + } set { - if (value < ConsoleLoggerFormat.Default || value > ConsoleLoggerFormat.Systemd) + if (value == ConsoleLoggerFormat.Systemd) + { + FormatterName = ConsoleLogFormatterNames.Systemd; + } + else { - throw new ArgumentOutOfRangeException(nameof(value)); + FormatterName = ConsoleLogFormatterNames.Default; } - _format = value; } } /// - /// Gets or sets value indicating the minimum level of messages that would get written to Console.Error. + /// /// - public LogLevel LogToStandardErrorThreshold { get; set; } = LogLevel.None; + public string FormatterName { get; set; } + + [System.ObsoleteAttribute("ConsoleLoggerOptions.IncludeScopes has been deprecated..", false)] + public bool IncludeScopes { get; set; } - /// - /// Gets or sets format string used to format timestamp in logging messages. Defaults to null. - /// + [System.ObsoleteAttribute("ConsoleLoggerOptions.LogToStandardErrorThreshold has been deprecated..", false)] + public Microsoft.Extensions.Logging.LogLevel LogToStandardErrorThreshold { get; set; } + + [System.ObsoleteAttribute("ConsoleLoggerOptions.TimestampFormat has been deprecated..", false)] public string TimestampFormat { get; set; } - /// - /// Gets or sets indication whether or not UTC timezone should be used to for timestamps in logging messages. Defaults to false. - /// + [System.ObsoleteAttribute("ConsoleLoggerOptions.UseUtcTimestamp has been deprecated..", false)] public bool UseUtcTimestamp { get; set; } } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProcessor.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProcessor.cs index c7d370e9c30364..94cefe724c47e7 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProcessor.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProcessor.cs @@ -50,21 +50,13 @@ public virtual void EnqueueMessage(LogMessageEntry message) } // for testing - internal virtual void WriteMessage(LogMessageEntry message) + internal virtual void WriteMessage(LogMessageEntry entry) { - var console = message.LogAsError ? ErrorConsole : Console; - - if (message.TimeStamp != null) + var console = entry.LogAsError ? ErrorConsole : Console; + foreach (var message in entry.Messages) { - console.Write(message.TimeStamp, message.MessageColor, message.MessageColor); + console.Write(message.Message, message.Background, message.Foreground); } - - if (message.LevelString != null) - { - console.Write(message.LevelString, message.LevelBackground, message.LevelForeground); - } - - console.Write(message.Message, message.MessageColor, message.MessageColor); console.Flush(); } diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs index e103217bfe6070..1a9e1bb1559a31 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using Microsoft.Extensions.Options; @@ -17,6 +19,7 @@ public class ConsoleLoggerProvider : ILoggerProvider, ISupportExternalScope { private readonly IOptionsMonitor _options; private readonly ConcurrentDictionary _loggers; + private readonly ConcurrentDictionary _formatters; private readonly ConsoleLoggerProcessor _messageQueue; private IDisposable _optionsReloadToken; @@ -26,10 +29,24 @@ public class ConsoleLoggerProvider : ILoggerProvider, ISupportExternalScope /// Creates an instance of . /// /// The options to create instances with. - public ConsoleLoggerProvider(IOptionsMonitor options) + public ConsoleLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor options) + : this(options, Enumerable.Empty()) + { + ; // todo: check workflow. maybe we should always have 4 formatters prepped instead? + // current implementation wont work well with empty formatters. + // use IServiceLocator or IServiceCollection? to locate Default formatter and add as formatters? + } + + /// + /// Creates an instance of . + /// + /// The options to create instances with. + /// Log formatters added for insteaces. + public ConsoleLoggerProvider(IOptionsMonitor options, IEnumerable formatters) { _options = options; _loggers = new ConcurrentDictionary(); + _formatters = new ConcurrentDictionary(formatters.ToDictionary(f => f.Name)); ReloadLoggerOptions(options.CurrentValue); _optionsReloadToken = _options.OnChange(ReloadLoggerOptions); @@ -47,24 +64,84 @@ public ConsoleLoggerProvider(IOptionsMonitor options) } } + // warning: ReloadLoggerOptions can be called before the ctor completed,... before registering all of the state used in this method need to be initialized private void ReloadLoggerOptions(ConsoleLoggerOptions options) { + if ( + options.FormatterName == null || + !_formatters.TryGetValue(options.FormatterName, out IConsoleLogFormatter logFormatter) || + !_formatters.TryGetValue(options.FormatterName?.ToLower(), out logFormatter) + ) + { + switch (options.Format) + { + case ConsoleLoggerFormat.Systemd: + logFormatter = _formatters[ConsoleLogFormatterNames.Systemd]; + break; + default: + logFormatter = _formatters[ConsoleLogFormatterNames.Default]; + break; + } + } + UpdateFormatterOptions(logFormatter, options); + foreach (var logger in _loggers) { - logger.Value.Options = options; + logger.Value.Formatter = logFormatter; } } /// public ILogger CreateLogger(string name) { + if ( + _options.CurrentValue.FormatterName == null || + !_formatters.TryGetValue(_options.CurrentValue.FormatterName, out IConsoleLogFormatter logFormatter) || + !_formatters.TryGetValue(_options.CurrentValue.FormatterName?.ToLower(), out logFormatter) + ) + { + switch (_options.CurrentValue.Format) + { + case ConsoleLoggerFormat.Systemd: + logFormatter = _formatters[ConsoleLogFormatterNames.Systemd]; + break; + default: + logFormatter = _formatters[ConsoleLogFormatterNames.Default]; + break; + } + } + UpdateFormatterOptions(logFormatter, _options.CurrentValue); + return _loggers.GetOrAdd(name, loggerName => new ConsoleLogger(name, _messageQueue) { - Options = _options.CurrentValue, - ScopeProvider = _scopeProvider + ScopeProvider = _scopeProvider, + Formatter = logFormatter }); } + private void UpdateFormatterOptions(IConsoleLogFormatter formatter, ConsoleLoggerOptions deprecatedFromOptions) + { + if (deprecatedFromOptions.FormatterName != null) + return; + // kept for deprecated apis: + if (formatter is DefaultConsoleLogFormatter defaultFormatter) + { + defaultFormatter.FormatterOptions.DisableColors = deprecatedFromOptions.DisableColors; + defaultFormatter.FormatterOptions.IncludeScopes = deprecatedFromOptions.IncludeScopes; + defaultFormatter.FormatterOptions.LogToStandardErrorThreshold = deprecatedFromOptions.LogToStandardErrorThreshold; + defaultFormatter.FormatterOptions.TimestampFormat = deprecatedFromOptions.TimestampFormat; + defaultFormatter.FormatterOptions.UseUtcTimestamp = deprecatedFromOptions.UseUtcTimestamp; + } + else + if (formatter is SystemdConsoleLogFormatter systemdFormatter) + { + systemdFormatter.FormatterOptions.IncludeScopes = deprecatedFromOptions.IncludeScopes; + systemdFormatter.FormatterOptions.LogToStandardErrorThreshold = deprecatedFromOptions.LogToStandardErrorThreshold; + systemdFormatter.FormatterOptions.TimestampFormat = deprecatedFromOptions.TimestampFormat; + systemdFormatter.FormatterOptions.UseUtcTimestamp = deprecatedFromOptions.UseUtcTimestamp; + } + } + /// public void Dispose() { diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/ConsoleLogFormatterNames.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/ConsoleLogFormatterNames.cs new file mode 100644 index 00000000000000..d07d4dd46d8a9d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/ConsoleLogFormatterNames.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.Logging.Console +{ + public static class ConsoleLogFormatterNames + { + public const string Default = "default"; + public const string Json = "json"; + public const string Systemd = "systemd"; + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/DefaultConsoleLogFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/DefaultConsoleLogFormatter.cs new file mode 100644 index 00000000000000..5cf56f19261637 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/DefaultConsoleLogFormatter.cs @@ -0,0 +1,394 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.Console +{ + internal class DefaultConsoleLogFormatter : IConsoleLogFormatter, IDisposable + { + private static readonly string _loglevelPadding = ": "; + private static readonly string _messagePadding; + private static readonly string _newLineWithMessagePadding; + private IDisposable _optionsReloadToken; + + // ConsoleColor does not have a value to specify the 'Default' color + private readonly ConsoleColor? DefaultConsoleColor = null; + + [ThreadStatic] + private static StringBuilder _logBuilder; + + static DefaultConsoleLogFormatter() + { + var logLevelString = GetLogLevelString(LogLevel.Information); + _messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length); + _newLineWithMessagePadding = Environment.NewLine + _messagePadding; + } + + public DefaultConsoleLogFormatter(IOptionsMonitor options) + { + FormatterOptions = options.CurrentValue; + ReloadLoggerOptions(options.CurrentValue); + _optionsReloadToken = options.OnChange(ReloadLoggerOptions); + } + + private void ReloadLoggerOptions(DefaultConsoleLogFormatterOptions options) + { + FormatterOptions = options; + } + + public void Dispose() + { + _optionsReloadToken?.Dispose(); + } + + public string Name => ConsoleLogFormatterNames.Default; + + internal DefaultConsoleLogFormatterOptions FormatterOptions { get; set; } + + public LogMessageEntry Format(LogLevel logLevel, string logName, EventId eventId, TState state, Exception exception, Func formatter, IExternalScopeProvider scopeProvider) + { + var message = formatter(state, exception); + if (!string.IsNullOrEmpty(message) || exception != null) + { + if (!FormatterOptions.MultiLine) + { + return FormatHelperCompact(logLevel, logName, eventId.Id, message, exception, scopeProvider, state); + } + return Format(logLevel, logName, eventId.Id, message, exception, scopeProvider); + } + // TODO: test use case: + return default; + } + + private LogMessageEntry FormatHelperCompact(LogLevel logLevel, string logName, int eventId, string message, Exception exception, IExternalScopeProvider scopeProvider, TState scope) + { + var messages = new List(); + + var logLevelColors = GetLogLevelConsoleColors(logLevel); + var logLevelString = GetLogLevelString(logLevel); + + string timestamp = null; + var timestampFormat = FormatterOptions.TimestampFormat; + if (timestampFormat != null) + { + var dateTime = GetCurrentDateTime(); + timestamp = dateTime.ToString(timestampFormat); + } + if (timestamp != null) + { + messages.Add(new ConsoleMessage(timestamp + " ", null, null)); + } + if (logLevelString != null) + { + messages.Add(new ConsoleMessage(logLevelString + " ", logLevelColors.Background, logLevelColors.Foreground)); + } + + messages.Add(new ConsoleMessage($"{logName}[{eventId}] ", null, null)); + string originalFormat = null; + int count = 0; + + if (scope != null) + { + if (scope is IReadOnlyList> kvpsx) + { + var strings = new List>(); + foreach (var kvp in kvpsx) + { + if (kvp.Key.Contains("{OriginalFormat}")) + { + originalFormat = kvp.Value.ToString(); + } + else + { + strings.Add(kvp); + } + } + int prevIndex = 0; + if (originalFormat != null) + { + foreach (var kvp in kvpsx) + { + if (!kvp.Key.Contains("{OriginalFormat}")) + { + var curIndex = originalFormat.IndexOf("{" + strings.ElementAt(count).Key + "}"); + if (curIndex != -1) + { + var curString = originalFormat.Substring(prevIndex, curIndex - prevIndex); + messages.Add(new ConsoleMessage(curString, null, null)); + // TODO: when DisableColors is true, also uncolor the inner var colors + messages.Add(new ConsoleMessage(strings.ElementAt(count).Value.ToString(), null, ConsoleColor.Cyan)); + prevIndex += curIndex + strings.ElementAt(count).Key.Length + 2; + count++; + } + } + } + } + } + else if (scope is IReadOnlyList> kvps) + { + var strings = new List>(); + foreach (var kvp in kvps) + { + if (kvp.Key.Contains("{OriginalFormat}")) + { + originalFormat = kvp.Value; + } + else + { + strings.Add(kvp); + } + } + int prevIndex = 0; + if (originalFormat != null) + { + foreach (var kvp in kvps) + { + if (!kvp.Key.Contains("{OriginalFormat}")) + { + var curIndex = originalFormat.IndexOf("{" + strings.ElementAt(count).Key + "}"); + if (curIndex != -1) + { + var curString = originalFormat.Substring(prevIndex, curIndex - prevIndex); + messages.Add(new ConsoleMessage(curString, null, null)); + messages.Add(new ConsoleMessage(strings.ElementAt(count).Value, null, ConsoleColor.Cyan)); + prevIndex += curIndex + strings.ElementAt(count).Key.Length + 2; + count++; + } + } + } + } + } + } + + if (!string.IsNullOrEmpty(message)) + { + if (originalFormat == null) + { + messages.Add(new ConsoleMessage(message, null, null)); + } + else if (count == 0) + { + messages.Add(new ConsoleMessage(originalFormat, null, null)); + } + } + + messages.Add(new ConsoleMessage(" ", null, null)); + GetScopeInformation(scopeProvider, messages); + + if (exception != null) + { + // exception message + messages.Add(new ConsoleMessage(" ", null, null)); + messages.Add(new ConsoleMessage(exception.ToString().Replace(Environment.NewLine, " "), null, null)); + // TODO: try to improve readability for exception message. + // TODO: maybe use Compact as default? + } + messages.Add(new ConsoleMessage(Environment.NewLine, null, null)); + + return new LogMessageEntry( + messages: messages.ToArray(), + logAsError: logLevel >= FormatterOptions.LogToStandardErrorThreshold + ); + } + + // IConsoleMessageBuilder // allocates a string + // Append(string messagee) + // SetColor(xx) + // ToString() + + private LogMessageEntry Format(LogLevel logLevel, string logName, int eventId, string message, Exception exception, IExternalScopeProvider scopeProvider) + { + var logBuilder = _logBuilder; + _logBuilder = null; + + if (logBuilder == null) + { + logBuilder = new StringBuilder(); + } + + // Example: + // INFO: ConsoleApp.Program[10] + // Request received + + var logLevelColors = GetLogLevelConsoleColors(logLevel); + var logLevelString = GetLogLevelString(logLevel); + // category and event id + logBuilder.Append(_loglevelPadding); + logBuilder.Append(logName); + logBuilder.Append("["); + logBuilder.Append(eventId); + logBuilder.AppendLine("]"); + + // scope information + GetScopeInformation(logBuilder, scopeProvider); + + if (!string.IsNullOrEmpty(message)) + { + // message + logBuilder.Append(_messagePadding); + + var len = logBuilder.Length; + logBuilder.AppendLine(message); + logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length); + } + + // Example: + // System.InvalidOperationException + // at Namespace.Class.Function() in File:line X + if (exception != null) + { + // exception message + logBuilder.AppendLine(exception.ToString()); + } + + string timestamp = null; + var timestampFormat = FormatterOptions.TimestampFormat; + if (timestampFormat != null) + { + var dateTime = GetCurrentDateTime(); + timestamp = dateTime.ToString(timestampFormat); + } + + var formattedMessage = logBuilder.ToString(); + logBuilder.Clear(); + if (logBuilder.Capacity > 1024) + { + logBuilder.Capacity = 1024; + } + _logBuilder = logBuilder; + + var messages = new List(); + if (timestamp != null) + { + messages.Add(new ConsoleMessage(timestamp, DefaultConsoleColor, DefaultConsoleColor)); + } + if (logLevelString != null) + { + messages.Add(new ConsoleMessage(logLevelString, logLevelColors.Background, logLevelColors.Foreground)); + } + messages.Add(new ConsoleMessage(formattedMessage, DefaultConsoleColor, DefaultConsoleColor)); + + return new LogMessageEntry( + messages: messages.ToArray(), + logAsError: logLevel >= FormatterOptions.LogToStandardErrorThreshold + ); + } + + private DateTime GetCurrentDateTime() + { + return FormatterOptions.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now; + } + + private static string GetLogLevelString(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return "trce"; + case LogLevel.Debug: + return "dbug"; + case LogLevel.Information: + return "info"; + case LogLevel.Warning: + return "warn"; + case LogLevel.Error: + return "fail"; + case LogLevel.Critical: + return "crit"; + default: + throw new ArgumentOutOfRangeException(nameof(logLevel)); + } + } + + private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel) + { + if (FormatterOptions.DisableColors) + { + return new ConsoleColors(null, null); + } + + // We must explicitly set the background color if we are setting the foreground color, + // since just setting one can look bad on the users console. + switch (logLevel) + { + case LogLevel.Critical: + return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red); + case LogLevel.Error: + return new ConsoleColors(ConsoleColor.Black, ConsoleColor.Red); + case LogLevel.Warning: + return new ConsoleColors(ConsoleColor.Yellow, ConsoleColor.Black); + case LogLevel.Information: + return new ConsoleColors(ConsoleColor.DarkGreen, ConsoleColor.Black); + case LogLevel.Debug: + return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black); + case LogLevel.Trace: + return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black); + default: + return new ConsoleColors(DefaultConsoleColor, DefaultConsoleColor); + } + } + + private void GetScopeInformation(IExternalScopeProvider scopeProvider, List messages) + { + if (FormatterOptions.IncludeScopes && scopeProvider != null) + { + scopeProvider.ForEachScope((scope, state) => + { + state.Add(new ConsoleMessage("=> ", null, null)); + state.Add(new ConsoleMessage(scope.ToString(), null, ConsoleColor.DarkGray)); + state.Add(new ConsoleMessage(" ", null, null)); + + }, messages); + } + } + + private void GetScopeInformation(StringBuilder stringBuilder, IExternalScopeProvider scopeProvider) + { + if (FormatterOptions.IncludeScopes && scopeProvider != null) + { + var initialLength = stringBuilder.Length; + + scopeProvider.ForEachScope((scope, state) => + { + var (builder, paddAt) = state; + var padd = paddAt == builder.Length; + if (padd) + { + builder.Append(_messagePadding); + builder.Append("=> "); + } + else + { + builder.Append(" => "); + } + builder.Append(scope); + }, (stringBuilder, initialLength)); + + if (stringBuilder.Length > initialLength) + { + stringBuilder.AppendLine(); + } + } + } + + private readonly struct ConsoleColors + { + public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background) + { + Foreground = foreground; + Background = background; + } + + public ConsoleColor? Foreground { get; } + + public ConsoleColor? Background { get; } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/DefaultConsoleLogFormatterOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/DefaultConsoleLogFormatterOptions.cs new file mode 100644 index 00000000000000..01db9947a58a2d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/DefaultConsoleLogFormatterOptions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Extensions.Logging.Console +{ + /// + /// Options for the built-in default console log formatter. + /// + public class DefaultConsoleLogFormatterOptions : SystemdConsoleLogFormatterOptions + { + public DefaultConsoleLogFormatterOptions() { } + + /// + /// Disables colors when . + /// + public bool DisableColors { get; set; } + + /// + /// When , the entire message gets logged in a single line. + /// + public bool MultiLine { get; set; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/IConsoleLogFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/IConsoleLogFormatter.cs new file mode 100644 index 00000000000000..72ba5a1dbd3999 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/IConsoleLogFormatter.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.Console +{ + public interface IConsoleLogFormatter + { + + /// + /// Gets the name associated with the console log formatter. + /// + string Name { get; } + + /// + /// Formats a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The log name. + /// The event id associated with the log. + /// The entry to be written. Can be also an object. + /// The exception related to this entry. + /// Function to create a message of the and . + /// The provider of scope data. + /// The type of the object to be written. + LogMessageEntry Format(LogLevel logLevel, string logName, EventId eventId, TState state, Exception exception, Func formatter, IExternalScopeProvider scopeProvider); + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/JsonConsoleLogFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/JsonConsoleLogFormatter.cs new file mode 100644 index 00000000000000..90ced0711cfad5 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/JsonConsoleLogFormatter.cs @@ -0,0 +1,205 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.Console +{ + internal class JsonConsoleLogFormatter : IConsoleLogFormatter, IDisposable + { + private IDisposable _optionsReloadToken; + + [ThreadStatic] + private static StringBuilder _logBuilder; + + public JsonConsoleLogFormatter(IOptionsMonitor options) + { + FormatterOptions = options.CurrentValue; + ReloadLoggerOptions(options.CurrentValue); + _optionsReloadToken = options.OnChange(ReloadLoggerOptions); + } + + public string Name => ConsoleLogFormatterNames.Json; + + private string WriteJson(LogLevel logLevel, string logName, int eventId, string message, Exception exception, IExternalScopeProvider scopeProvider)//long[] extraData) + { + const int DefaultBufferSize = 1024; + var output = new ArrayBufferWriter(DefaultBufferSize); + using (var writer = new Utf8JsonWriter(output, FormatterOptions.JsonWriterOptions)) + { + writer.WriteStartObject(); + + + string timestamp = null; + var timestampFormat = FormatterOptions.TimestampFormat; + if (timestampFormat != null) + { + var dateTime = FormatterOptions.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now; + timestamp = dateTime.ToString(timestampFormat); + } + writer.WriteString("timestamp", timestamp); + writer.WriteNumber("eventId", eventId); + writer.WriteString("logLevel", GetLogLevelString(logLevel)); + writer.WriteString("category", logName); + writer.WriteString("message", message); + + if (exception != null) + { + writer.WriteStartObject("exception"); + writer.WriteString("message", exception.Message.ToString()); + writer.WriteString("type", exception.GetType().ToString()); + writer.WriteStartArray("stackTrace"); + if (exception?.StackTrace != null) + { + foreach (var xx in exception?.StackTrace?.Split(Environment.NewLine)) + { + JsonSerializer.Serialize(writer, xx); + } + } + writer.WriteEndArray(); + writer.WriteNumber("hResult", exception.HResult); + writer.WriteEndObject(); + } + + GetScopeInformation(writer, scopeProvider); + + writer.WriteEndObject(); + + writer.Flush(); + } + return Encoding.UTF8.GetString(output.WrittenMemory.Span); + } + + public LogMessageEntry Format(LogLevel logLevel, string logName, EventId eventId, TState state, Exception exception, Func formatter, IExternalScopeProvider scopeProvider) + { + var message = formatter(state, exception); + if (!string.IsNullOrEmpty(message) || exception != null) + { + return Format(logLevel, logName, eventId.Id, message, exception, scopeProvider); + } + return default; + } + + private LogMessageEntry Format(LogLevel logLevel, string logName, int eventId, string message, Exception exception, IExternalScopeProvider scopeProvider) + { + var logBuilder = _logBuilder; + _logBuilder = null; + + if (logBuilder == null) + { + logBuilder = new StringBuilder(); + } + + logBuilder.Append(WriteJson(logLevel, logName, eventId, message, exception, scopeProvider)); + + var formattedMessage = logBuilder.ToString(); + logBuilder.Clear(); + if (logBuilder.Capacity > 1024) + { + logBuilder.Capacity = 1024; + } + _logBuilder = logBuilder; + + var messages = new ConsoleMessage[2] { + new ConsoleMessage(formattedMessage, null, null), + new ConsoleMessage(Environment.NewLine, null, null) + }; + + return new LogMessageEntry( + messages: messages, + logAsError: logLevel >= FormatterOptions.LogToStandardErrorThreshold + ); + } + + private static string GetLogLevelString(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return "json_trace"; + case LogLevel.Debug: + return "json_debug"; + case LogLevel.Information: + return "json_info"; + case LogLevel.Warning: + return "json_warn"; + case LogLevel.Error: + return "json_fail"; + case LogLevel.Critical: + return "json_critical"; + default: + throw new ArgumentOutOfRangeException(nameof(logLevel)); + } + } + + private void GetScopeInformation(Utf8JsonWriter writer, IExternalScopeProvider scopeProvider) + { + try + { + if (FormatterOptions.IncludeScopes && scopeProvider != null) + { + writer.WriteStartObject("scopes"); + scopeProvider.ForEachScope((scope, state) => + { + if (scope is IReadOnlyList> kvps) + { + foreach (var kvp in kvps) + { + if (kvp.Value is string ss) + state.WriteString(kvp.Key, ss); + else + if (kvp.Value is int ii) + state.WriteNumber(kvp.Key, ii); + else + { + // check how this work + state.WritePropertyName(kvp.Key); + JsonSerializer.Serialize(state, kvp.Value); + } + } + //state is the writer + //JsonSerializer.Serialize(state, scope); + } + else + { + state.WriteString("noName", scope.ToString()); + } + }, (writer)); + writer.WriteEndObject(); + } + } + catch (Exception ex) + { + System.Console.WriteLine("Something went wrong" + ex.Message); + } + } + + internal JsonConsoleLogFormatterOptions FormatterOptions { get; set; } + + private void ReloadLoggerOptions(JsonConsoleLogFormatterOptions options) + { + FormatterOptions = options; + } + + public void Dispose() + { + _optionsReloadToken?.Dispose(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/JsonConsoleLogFormatterOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/JsonConsoleLogFormatterOptions.cs new file mode 100644 index 00000000000000..47e3b61673aac2 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/JsonConsoleLogFormatterOptions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; +using System.Text.Json; + +namespace Microsoft.Extensions.Logging.Console +{ + /// + /// Options for the built-in json console log formatter. + /// + public class JsonConsoleLogFormatterOptions : SystemdConsoleLogFormatterOptions + { + public JsonConsoleLogFormatterOptions() { } + + /// + /// Gets or sets JsonWriterOptions. + /// + public JsonWriterOptions JsonWriterOptions { get; set; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/SystemdConsoleLogFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/SystemdConsoleLogFormatter.cs new file mode 100644 index 00000000000000..f4949d7ec39043 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/SystemdConsoleLogFormatter.cs @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.Console +{ + internal class SystemdConsoleLogFormatter : IConsoleLogFormatter, IDisposable + { + private IDisposable _optionsReloadToken; + + private static readonly string _loglevelPadding = ": "; + private static readonly string _messagePadding; + + [ThreadStatic] + private static StringBuilder _logBuilder; + + static SystemdConsoleLogFormatter() + { + var logLevelString = GetSyslogSeverityString(LogLevel.Information); + _messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length); + } + + public SystemdConsoleLogFormatter(IOptionsMonitor options) + { + FormatterOptions = options.CurrentValue; + ReloadLoggerOptions(options.CurrentValue); + _optionsReloadToken = options.OnChange(ReloadLoggerOptions); + } + + private void ReloadLoggerOptions(SystemdConsoleLogFormatterOptions options) + { + FormatterOptions = options; + } + + public void Dispose() + { + _optionsReloadToken?.Dispose(); + } + + public string Name => ConsoleLogFormatterNames.Systemd; + + internal SystemdConsoleLogFormatterOptions FormatterOptions { get; set; } + + public LogMessageEntry Format(LogLevel logLevel, string logName, EventId eventId, TState state, Exception exception, Func formatter, IExternalScopeProvider scopeProvider) + { + var message = formatter(state, exception); + if (!string.IsNullOrEmpty(message) || exception != null) + { + return Format(logLevel, logName, eventId.Id, message, exception, scopeProvider); + } + return default; + } + + private LogMessageEntry Format(LogLevel logLevel, string logName, int eventId, string message, Exception exception, IExternalScopeProvider scopeProvider) + { + var logBuilder = _logBuilder; + _logBuilder = null; + + if (logBuilder == null) + { + logBuilder = new StringBuilder(); + } + + // systemd reads messages from standard out line-by-line in a 'message' format. + // newline characters are treated as message delimiters, so we must replace them. + // Messages longer than the journal LineMax setting (default: 48KB) are cropped. + // Example: + // <6>ConsoleApp.Program[10] Request received + + // loglevel + var logLevelString = GetSyslogSeverityString(logLevel); + logBuilder.Append(logLevelString); + + // timestamp + var timestampFormat = FormatterOptions.TimestampFormat; + if (timestampFormat != null) + { + var dateTime = GetCurrentDateTime(); + logBuilder.Append(dateTime.ToString(timestampFormat)); + } + + // category and event id + logBuilder.Append(logName); + logBuilder.Append("["); + logBuilder.Append(eventId); + logBuilder.Append("]"); + + // scope information + GetScopeInformation(logBuilder, scopeProvider); + + // message + if (!string.IsNullOrEmpty(message)) + { + logBuilder.Append(' '); + // message + AppendAndReplaceNewLine(logBuilder, message); + } + + // exception + // System.InvalidOperationException at Namespace.Class.Function() in File:line X + if (exception != null) + { + logBuilder.Append(' '); + AppendAndReplaceNewLine(logBuilder, exception.ToString()); + } + + // newline delimiter + logBuilder.Append(Environment.NewLine); + + + var formattedMessage = logBuilder.ToString(); + logBuilder.Clear(); + if (logBuilder.Capacity > 1024) + { + logBuilder.Capacity = 1024; + } + _logBuilder = logBuilder; + + var messages = new ConsoleMessage[1] { new ConsoleMessage(formattedMessage, null, null) }; + + return new LogMessageEntry( + messages: messages, + logAsError: logLevel >= FormatterOptions.LogToStandardErrorThreshold + ); + + static void AppendAndReplaceNewLine(StringBuilder sb, string message) + { + var len = sb.Length; + sb.Append(message); + sb.Replace(Environment.NewLine, " ", len, message.Length); + } + } + + private DateTime GetCurrentDateTime() + { + return FormatterOptions.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now; + } + + private static string GetSyslogSeverityString(LogLevel logLevel) + { + // 'Syslog Message Severities' from https://tools.ietf.org/html/rfc5424. + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + return "<7>"; // debug-level messages + case LogLevel.Information: + return "<6>"; // informational messages + case LogLevel.Warning: + return "<4>"; // warning conditions + case LogLevel.Error: + return "<3>"; // error conditions + case LogLevel.Critical: + return "<2>"; // critical conditions + default: + throw new ArgumentOutOfRangeException(nameof(logLevel)); + } + } + + private void GetScopeInformation(StringBuilder stringBuilder, IExternalScopeProvider scopeProvider) + { + if (FormatterOptions.IncludeScopes && scopeProvider != null) + { + var initialLength = stringBuilder.Length; + + scopeProvider.ForEachScope((scope, state) => + { + var (builder, paddAt) = state; + var padd = paddAt == builder.Length; + if (padd) + { + builder.Append(_messagePadding); + builder.Append("=> "); + } + else + { + builder.Append(" => "); + } + builder.Append(scope); + }, (stringBuilder, -1)); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/SystemdConsoleLogFormatterOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/SystemdConsoleLogFormatterOptions.cs new file mode 100644 index 00000000000000..adab8abcd08f6a --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Formatter/SystemdConsoleLogFormatterOptions.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; + +namespace Microsoft.Extensions.Logging.Console +{ + /// + /// Options for the built-in systemd console log formatter. + /// + public class SystemdConsoleLogFormatterOptions + { + public SystemdConsoleLogFormatterOptions() { } + + /// + /// Includes scopes when . + /// + public bool IncludeScopes { get; set; } + + /// + /// Gets or sets value indicating the minimum level of messages that would get written to Console.Error. + /// + public Microsoft.Extensions.Logging.LogLevel LogToStandardErrorThreshold { get; set; } + + /// + /// Gets or sets format string used to format timestamp in logging messages. Defaults to null. + /// + public string TimestampFormat { get; set; } + + /// + /// Gets or sets indication whether or not UTC timezone should be used to for timestamps in logging messages. Defaults to false. + /// + public bool UseUtcTimestamp { get; set; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/LogMessageEntry.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/LogMessageEntry.cs index d1beda4e7e27e9..b5dc688016f9b5 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/LogMessageEntry.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/LogMessageEntry.cs @@ -6,25 +6,28 @@ namespace Microsoft.Extensions.Logging.Console { - internal readonly struct LogMessageEntry + public readonly struct LogMessageEntry { - public LogMessageEntry(string message, string timeStamp = null, string levelString = null, ConsoleColor? levelBackground = null, ConsoleColor? levelForeground = null, ConsoleColor? messageColor = null, bool logAsError = false) + public LogMessageEntry(ConsoleMessage[] messages, bool logAsError = false) { - TimeStamp = timeStamp; - LevelString = levelString; - LevelBackground = levelBackground; - LevelForeground = levelForeground; - MessageColor = messageColor; - Message = message; + Messages = messages; LogAsError = logAsError; } - public readonly string TimeStamp; - public readonly string LevelString; - public readonly ConsoleColor? LevelBackground; - public readonly ConsoleColor? LevelForeground; - public readonly ConsoleColor? MessageColor; - public readonly string Message; + public readonly ConsoleMessage[] Messages; public readonly bool LogAsError; } + + public readonly struct ConsoleMessage + { + public ConsoleMessage(string message, ConsoleColor? background = null, ConsoleColor? foreground = null) + { + Message = message; + Background = background; + Foreground = foreground; + } + public readonly string Message; + public readonly ConsoleColor? Background; + public readonly ConsoleColor? Foreground; + } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Microsoft.Extensions.Logging.Console.csproj b/src/libraries/Microsoft.Extensions.Logging.Console/src/Microsoft.Extensions.Logging.Console.csproj index 204a13c1432525..bc09be96c23e54 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/Microsoft.Extensions.Logging.Console.csproj +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Microsoft.Extensions.Logging.Console.csproj @@ -22,11 +22,21 @@ + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ConsoleLoggerTest.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ConsoleLoggerTest.cs index 8c8786ca49461a..726934d682230a 100644 --- a/src/libraries/Microsoft.Extensions.Logging/tests/Common/ConsoleLoggerTest.cs +++ b/src/libraries/Microsoft.Extensions.Logging/tests/Common/ConsoleLoggerTest.cs @@ -21,6 +21,7 @@ public class ConsoleLoggerTest private const string _loggerName = "test"; private const string _state = "This is a test, and {curly braces} are just fine!"; private readonly Func _defaultFormatter = (state, exception) => state.ToString(); + private readonly IEnumerable _formatters = new ILogFormatter[] { new DefaultLogFormatter { Options = new ConsoleLoggerOptions() }, new SystemdLogFormatter { Options = new ConsoleLoggerOptions() } }; private static (ConsoleLogger Logger, ConsoleSink Sink, ConsoleSink ErrorSink, Func GetLevelPrefix, int WritesPerMsg) SetUp(ConsoleLoggerOptions options = null) { @@ -33,9 +34,24 @@ private static (ConsoleLogger Logger, ConsoleSink Sink, ConsoleSink ErrorSink, F consoleLoggerProcessor.Console = console; consoleLoggerProcessor.ErrorConsole = errorConsole; - var logger = new ConsoleLogger(_loggerName, consoleLoggerProcessor); - logger.ScopeProvider = new LoggerExternalScopeProvider(); - logger.Options = options ?? new ConsoleLoggerOptions(); + var testOptions = options ?? new ConsoleLoggerOptions(); + var scopeProvider = new LoggerExternalScopeProvider(); + var formatters = new ILogFormatter[] + { + new DefaultLogFormatter + { + Options = testOptions, + ScopeProvider = scopeProvider + }, + new SystemdLogFormatter + { + Options = testOptions, + ScopeProvider = scopeProvider + } + }; + var logger = new ConsoleLogger(_loggerName, consoleLoggerProcessor, formatters); + logger.ScopeProvider = scopeProvider; + logger.Options = testOptions; Func levelAsString; int writesPerMsg; switch (logger.Options.Format) @@ -994,7 +1010,7 @@ public void LogAfterDisposeWritesLog() var processor = new ConsoleLoggerProcessor(); processor.Console = console; - var logger = new ConsoleLogger(_loggerName, loggerProcessor: processor); + var logger = new ConsoleLogger(_loggerName, loggerProcessor: processor, _formatters); logger.Options = new ConsoleLoggerOptions(); // Act processor.Dispose(); @@ -1023,7 +1039,7 @@ public void ConsoleLoggerOptions_DisableColors_IsAppliedToLoggers() { // Arrange var monitor = new TestOptionsMonitor(new ConsoleLoggerOptions() { DisableColors = true }); - var loggerProvider = new ConsoleLoggerProvider(monitor); + var loggerProvider = new ConsoleLoggerProvider(monitor, _formatters); var logger = (ConsoleLogger)loggerProvider.CreateLogger("Name"); // Act & Assert @@ -1054,7 +1070,7 @@ public void ConsoleLoggerOptions_TimeStampFormat_IsReloaded() { // Arrange var monitor = new TestOptionsMonitor(new ConsoleLoggerOptions()); - var loggerProvider = new ConsoleLoggerProvider(monitor); + var loggerProvider = new ConsoleLoggerProvider(monitor, _formatters); var logger = (ConsoleLogger)loggerProvider.CreateLogger("Name"); // Act & Assert @@ -1084,7 +1100,7 @@ public void ConsoleLoggerOptions_TimeStampFormat_IsReadFromLoggingConfiguration( public void ConsoleLoggerOptions_TimeStampFormat_MultipleReloads() { var monitor = new TestOptionsMonitor(new ConsoleLoggerOptions()); - var loggerProvider = new ConsoleLoggerProvider(monitor); + var loggerProvider = new ConsoleLoggerProvider(monitor, _formatters); var logger = (ConsoleLogger)loggerProvider.CreateLogger("Name"); Assert.Null(logger.Options.TimestampFormat); @@ -1099,7 +1115,7 @@ public void ConsoleLoggerOptions_IncludeScopes_IsAppliedToLoggers() { // Arrange var monitor = new TestOptionsMonitor(new ConsoleLoggerOptions() { IncludeScopes = true }); - var loggerProvider = new ConsoleLoggerProvider(monitor); + var loggerProvider = new ConsoleLoggerProvider(monitor, _formatters); var logger = (ConsoleLogger)loggerProvider.CreateLogger("Name"); // Act & Assert @@ -1130,7 +1146,7 @@ public void ConsoleLoggerOptions_LogAsErrorLevel_IsAppliedToLoggers() { // Arrange var monitor = new TestOptionsMonitor(new ConsoleLoggerOptions()); - var loggerProvider = new ConsoleLoggerProvider(monitor); + var loggerProvider = new ConsoleLoggerProvider(monitor, _formatters); var logger = (ConsoleLogger)loggerProvider.CreateLogger("Name"); // Act & Assert @@ -1144,7 +1160,7 @@ public void ConsoleLoggerOptions_UseUtcTimestamp_IsAppliedToLoggers() { // Arrange var monitor = new TestOptionsMonitor(new ConsoleLoggerOptions()); - var loggerProvider = new ConsoleLoggerProvider(monitor); + var loggerProvider = new ConsoleLoggerProvider(monitor, _formatters); var logger = (ConsoleLogger)loggerProvider.CreateLogger("Name"); // Act & Assert @@ -1275,4 +1291,4 @@ public void Set(ConsoleLoggerOptions options) _onChange?.Invoke(options, ""); } } -} +} \ No newline at end of file