Files
EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/ConsoleFlow.cs
2026-02-28 07:19:29 +01:00

286 lines
9.1 KiB
C#

using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace EonaCat.LogStack.Flows
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
/// <summary>
/// console flow with color support and minimal allocations
/// Uses a ColorSchema for configurable colors
/// </summary>
public sealed class ConsoleFlow : FlowBase
{
private readonly bool _useColors;
private readonly TimestampMode _timestampMode;
private readonly StringBuilder _buffer = new(1024);
private readonly object _consoleLock = new();
private readonly ColorSchema _colors;
private readonly string _template;
private List<Action<LogEvent, StringBuilder>> _compiledTemplate;
public ConsoleFlow(
LogLevel minimumLevel = LogLevel.Trace,
bool useColors = true,
TimestampMode timestampMode = TimestampMode.Local,
ColorSchema? colorSchema = null,
string template = "[{ts}] [{tz}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}")
: base("Console", minimumLevel)
{
_useColors = useColors;
_timestampMode = timestampMode;
_colors = colorSchema ?? new ColorSchema();
_template = template ?? throw new ArgumentNullException(nameof(template));
CompileTemplate(_template);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return Task.FromResult(WriteResult.LevelFiltered);
}
WriteToConsole(logEvent);
Interlocked.Increment(ref BlastedCount);
return Task.FromResult(WriteResult.Success);
}
public override Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
{
if (!IsEnabled)
{
return Task.FromResult(WriteResult.FlowDisabled);
}
foreach (var logEvent in logEvents.Span)
{
if (logEvent.Level >= MinimumLevel)
{
WriteToConsole(logEvent);
Interlocked.Increment(ref BlastedCount);
}
}
return Task.FromResult(WriteResult.Success);
}
private void WriteToConsole(LogEvent logEvent)
{
lock (_consoleLock)
{
_buffer.Clear();
foreach (var action in _compiledTemplate)
{
action(logEvent, _buffer);
}
if (_useColors && TryGetColor(logEvent.Level, out var color))
{
Console.ForegroundColor = color.Foreground;
}
Console.WriteLine(_buffer.ToString());
if (logEvent.Exception != null)
{
if (_useColors)
{
Console.ForegroundColor = ConsoleColor.DarkRed;
}
Console.WriteLine(logEvent.Exception.ToString());
if (_useColors)
{
Console.ResetColor();
}
}
if (_useColors)
{
Console.ResetColor();
}
}
}
private void CompileTemplate(string template)
{
_compiledTemplate = new List<Action<LogEvent, StringBuilder>>();
int pos = 0;
while (pos < template.Length)
{
int open = template.IndexOf('{', pos);
if (open < 0)
{
string lit = template.Substring(pos);
_compiledTemplate.Add((_, sb) => sb.Append(lit));
break;
}
if (open > pos)
{
string lit = template.Substring(pos, open - pos);
_compiledTemplate.Add((_, sb) => sb.Append(lit));
}
int close = template.IndexOf('}', open);
if (close < 0)
{
string lit = template.Substring(open);
_compiledTemplate.Add((_, sb) => sb.Append(lit));
break;
}
string token = template.Substring(open + 1, close - open - 1);
_compiledTemplate.Add(ResolveToken(token));
pos = close + 1;
}
}
private Action<LogEvent, StringBuilder> ResolveToken(string token)
{
switch (token.ToLowerInvariant())
{
case "ts":
return (log, sb) =>
sb.Append(LogEvent.GetDateTime(log.Timestamp)
.ToString("yyyy-MM-dd HH:mm:ss.fff"));
case "tz":
return (_, sb) =>
sb.Append(_timestampMode == TimestampMode.Local
? TimeZoneInfo.Local.StandardName
: "UTC");
case "host":
return (_, sb) => sb.Append(Environment.MachineName);
case "category":
return (log, sb) =>
{
if (!string.IsNullOrEmpty(log.Category))
{
sb.Append(log.Category);
}
};
case "thread":
return (_, sb) => sb.Append(Thread.CurrentThread.ManagedThreadId);
case "pid":
return (_, sb) => sb.Append(Process.GetCurrentProcess().Id);
case "message":
return (log, sb) => sb.Append(log.Message);
case "props":
return AppendProperties;
case "newline":
return (_, sb) => sb.AppendLine();
case "logtype":
return (log, sb) =>
{
var levelText = GetLevelText(log.Level);
if (_useColors && TryGetColor(log.Level, out var color))
{
Console.ForegroundColor = color.Foreground;
Console.BackgroundColor = color.Background;
Console.Write(sb.ToString());
Console.Write(levelText);
Console.ResetColor();
sb.Clear();
}
else
{
sb.Append(levelText);
}
};
default:
return (_, sb) => sb.Append('{').Append(token).Append('}');
}
}
private void AppendProperties(LogEvent log, StringBuilder sb)
{
if (log.Properties.Count == 0)
{
return;
}
sb.Append(" {");
bool first = true;
foreach (var prop in log.Properties)
{
if (!first)
{
sb.Append(", ");
}
sb.Append(prop.Key);
sb.Append('=');
sb.Append(prop.Value?.ToString() ?? "null");
first = false;
}
sb.Append('}');
}
private bool TryGetColor(LogLevel level, out ColorScheme color)
{
color = level switch
{
LogLevel.Trace => _colors.Trace,
LogLevel.Debug => _colors.Debug,
LogLevel.Information => _colors.Info,
LogLevel.Warning => _colors.Warning,
LogLevel.Error => _colors.Error,
LogLevel.Critical => _colors.Critical,
_ => _colors.Info
};
return color != null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string GetLevelText(LogLevel level)
{
return level switch
{
LogLevel.Trace => "TRACE",
LogLevel.Debug => "DEBUG",
LogLevel.Information => "INFO",
LogLevel.Warning => "WARN",
LogLevel.Error => "ERROR",
LogLevel.Critical => "CRITICAL",
_ => "???"
};
}
public override Task FlushAsync(CancellationToken cancellationToken = default)
{
// Console auto-flushes
return Task.CompletedTask;
}
}
}