File: SystemdConsoleFormatter.cs
Web Access
Project: src\src\libraries\Microsoft.Extensions.Logging.Console\src\Microsoft.Extensions.Logging.Console.csproj (Microsoft.Extensions.Logging.Console)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
 
namespace Microsoft.Extensions.Logging.Console
{
    internal sealed class SystemdConsoleFormatter : ConsoleFormatter, IDisposable
    {
        private readonly IDisposable? _optionsReloadToken;
 
        public SystemdConsoleFormatter(IOptionsMonitor<ConsoleFormatterOptions> options)
            : base(ConsoleFormatterNames.Systemd)
        {
            ReloadLoggerOptions(options.CurrentValue);
            _optionsReloadToken = options.OnChange(ReloadLoggerOptions);
        }
 
        [MemberNotNull(nameof(FormatterOptions))]
        private void ReloadLoggerOptions(ConsoleFormatterOptions options)
        {
            FormatterOptions = options;
        }
 
        public void Dispose()
        {
            _optionsReloadToken?.Dispose();
        }
 
        internal ConsoleFormatterOptions FormatterOptions { get; set; }
 
        public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
        {
            if (logEntry.State is BufferedLogRecord bufferedRecord)
            {
                string message = bufferedRecord.FormattedMessage ?? string.Empty;
                WriteInternal(null, textWriter, message, bufferedRecord.LogLevel, logEntry.Category, bufferedRecord.EventId.Id, bufferedRecord.Exception, bufferedRecord.Timestamp);
            }
            else
            {
                string message = logEntry.Formatter(logEntry.State, logEntry.Exception);
                if (logEntry.Exception == null && message == null)
                {
                    return;
                }
 
                // We extract most of the work into a non-generic method to save code size. If this was left in the generic
                // method, we'd get generic specialization for all TState parameters, but that's unnecessary.
                WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.Category, logEntry.EventId.Id, logEntry.Exception?.ToString(), GetCurrentDateTime());
            }
        }
 
        private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter textWriter, string message, LogLevel logLevel, string category,
            int eventId, string? exception, DateTimeOffset stamp)
        {
            // systemd reads messages from standard out line-by-line in a '<pri>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
            string logLevelString = GetSyslogSeverityString(logLevel);
            textWriter.Write(logLevelString);
 
            // timestamp
            string? timestampFormat = FormatterOptions.TimestampFormat;
            if (timestampFormat != null)
            {
                textWriter.Write(stamp.ToString(timestampFormat));
            }
 
            // category and event id
            textWriter.Write(category);
            textWriter.Write('[');
            textWriter.Write(eventId);
            textWriter.Write(']');
 
            // scope information
            WriteScopeInformation(textWriter, scopeProvider);
 
            // message
            if (!string.IsNullOrEmpty(message))
            {
                textWriter.Write(' ');
                // message
                WriteReplacingNewLine(textWriter, message);
            }
 
            // exception
            // System.InvalidOperationException at Namespace.Class.Function() in File:line X
            if (exception != null)
            {
                textWriter.Write(' ');
                WriteReplacingNewLine(textWriter, exception);
            }
 
            // newline delimiter
            textWriter.Write(Environment.NewLine);
 
            static void WriteReplacingNewLine(TextWriter writer, string message)
            {
                string newMessage = message.Replace(Environment.NewLine, " ");
                writer.Write(newMessage);
            }
        }
 
        private DateTimeOffset GetCurrentDateTime()
        {
            return FormatterOptions.TimestampFormat != null
                ? (FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now)
                : DateTimeOffset.MinValue;
        }
 
        private static string GetSyslogSeverityString(LogLevel logLevel)
        {
            // 'Syslog Message Severities' from https://tools.ietf.org/html/rfc5424.
            return logLevel switch
            {
                LogLevel.Trace => "<7>",
                LogLevel.Debug => "<7>",        // debug-level messages
                LogLevel.Information => "<6>",  // informational messages
                LogLevel.Warning => "<4>",     // warning conditions
                LogLevel.Error => "<3>",       // error conditions
                LogLevel.Critical => "<2>",    // critical conditions
                _ => throw new ArgumentOutOfRangeException(nameof(logLevel))
            };
        }
 
        private void WriteScopeInformation(TextWriter textWriter, IExternalScopeProvider? scopeProvider)
        {
            if (FormatterOptions.IncludeScopes && scopeProvider != null)
            {
                scopeProvider.ForEachScope((scope, state) =>
                {
                    state.Write(" => ");
                    state.Write(scope);
                }, textWriter);
            }
        }
    }
}