Added extensions
This commit is contained in:
@@ -60,6 +60,7 @@ public static class FileLoggerFactoryExtensions
|
|||||||
options.Mask = fileLoggerOptions.Mask;
|
options.Mask = fileLoggerOptions.Mask;
|
||||||
options.UseDefaultMasking = fileLoggerOptions.UseDefaultMasking;
|
options.UseDefaultMasking = fileLoggerOptions.UseDefaultMasking;
|
||||||
options.MaskedKeywords = fileLoggerOptions.MaskedKeywords;
|
options.MaskedKeywords = fileLoggerOptions.MaskedKeywords;
|
||||||
|
options.IncludeCorrelationId = fileLoggerOptions.IncludeCorrelationId;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return builder;
|
return builder;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|||||||
|
|
||||||
private string _logFile;
|
private string _logFile;
|
||||||
private long _currentFileSize;
|
private long _currentFileSize;
|
||||||
|
private Timer _flushTimer;
|
||||||
private readonly ConcurrentDictionary<string, StringBuilder> _buffers = new();
|
private readonly ConcurrentDictionary<string, StringBuilder> _buffers = new();
|
||||||
private readonly ConcurrentDictionary<string, long> _fileSizes = new();
|
private readonly ConcurrentDictionary<string, long> _fileSizes = new();
|
||||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||||
@@ -35,6 +35,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|||||||
public string LogFile => _logFile ?? string.Empty;
|
public string LogFile => _logFile ?? string.Empty;
|
||||||
|
|
||||||
public event EventHandler<ErrorMessage> OnError;
|
public event EventHandler<ErrorMessage> OnError;
|
||||||
|
public event EventHandler<string> OnRollOver;
|
||||||
|
|
||||||
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
||||||
{
|
{
|
||||||
@@ -48,6 +49,15 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|||||||
|
|
||||||
Directory.CreateDirectory(_path);
|
Directory.CreateDirectory(_path);
|
||||||
InitializeCurrentFile();
|
InitializeCurrentFile();
|
||||||
|
_flushTimer = new Timer(_ => FlushAllAsync().GetAwaiter().GetResult(), null, 5000, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FlushAllAsync()
|
||||||
|
{
|
||||||
|
foreach (var kv in _buffers)
|
||||||
|
{
|
||||||
|
await FlushAsync(kv.Key, kv.Value, CancellationToken.None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; }
|
public bool IncludeCorrelationId { get; }
|
||||||
@@ -179,17 +189,17 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|||||||
|
|
||||||
for (int i = _maxRolloverFiles - 1; i >= 1; i--)
|
for (int i = _maxRolloverFiles - 1; i >= 1; i--)
|
||||||
{
|
{
|
||||||
var src = Path.Combine(dir, $"{name}.{i}{ext}");
|
var source = Path.Combine(dir, $"{name}.{i}{ext}");
|
||||||
var dst = Path.Combine(dir, $"{name}.{i + 1}{ext}");
|
var destination = Path.Combine(dir, $"{name}.{i + 1}{ext}");
|
||||||
|
|
||||||
if (File.Exists(dst))
|
if (File.Exists(destination))
|
||||||
{
|
{
|
||||||
File.Delete(dst);
|
File.Delete(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File.Exists(src))
|
if (File.Exists(source))
|
||||||
{
|
{
|
||||||
File.Move(src, dst);
|
File.Move(source, destination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +214,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_rolloverLock.Release();
|
_rolloverLock.Release();
|
||||||
|
OnRollOver?.Invoke(this, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,13 +224,10 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|||||||
_buffers.TryAdd(file, new StringBuilder(4096));
|
_buffers.TryAdd(file, new StringBuilder(4096));
|
||||||
}
|
}
|
||||||
|
|
||||||
private (int Year, int Month, int Day) GetGrouping(LogMessage m) =>
|
|
||||||
(m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day);
|
|
||||||
|
|
||||||
private string GetFullName((int Year, int Month, int Day) g) =>
|
private string GetFullName((int Year, int Month, int Day) g) =>
|
||||||
string.IsNullOrWhiteSpace(_fileNamePrefix)
|
string.IsNullOrWhiteSpace(_fileNamePrefix)
|
||||||
? Path.Combine(_path, $"{g.Year:0000}{g.Month:00}{g.Day:00}.log")
|
? Path.Combine(_path, $"{Environment.MachineName}_{g.Year:0000}{g.Month:00}{g.Day:00}.log")
|
||||||
: Path.Combine(_path, $"{_fileNamePrefix}_{g.Year:0000}{g.Month:00}{g.Day:00}.log");
|
: Path.Combine(_path, $"{_fileNamePrefix}_{Environment.MachineName}_{g.Year:0000}{g.Month:00}{g.Day:00}.log");
|
||||||
|
|
||||||
private void DeleteOldLogFiles()
|
private void DeleteOldLogFiles()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
|
|
||||||
private const int MaxQueueSize = 1000;
|
private const int MaxQueueSize = 5000;
|
||||||
|
private const int MaxBatchSize = 20; // send logs in batches to reduce TCP writes
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; set; }
|
public bool IncludeCorrelationId { get; set; }
|
||||||
public event EventHandler<Exception> OnException;
|
public event EventHandler<Exception> OnException;
|
||||||
|
public event EventHandler<string> OnLogDropped;
|
||||||
|
|
||||||
public TcpLogger(string categoryName, TcpLoggerOptions options)
|
public TcpLogger(string categoryName, TcpLoggerOptions options)
|
||||||
{
|
{
|
||||||
@@ -37,6 +39,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
SingleWriter = false
|
SingleWriter = false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start background processing task
|
||||||
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token), _cts.Token);
|
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token), _cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,9 +54,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
Exception exception, Func<TState, Exception, string> formatter)
|
Exception exception, Func<TState, Exception, string> formatter)
|
||||||
{
|
{
|
||||||
if (!IsEnabled(logLevel) || formatter == null)
|
if (!IsEnabled(logLevel) || formatter == null)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -65,7 +66,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
string message = formatter(state, exception);
|
string message = formatter(state, exception);
|
||||||
|
|
||||||
// Build message
|
|
||||||
var sb = new StringBuilder(256);
|
var sb = new StringBuilder(256);
|
||||||
sb.Append('[').Append(DateTime.UtcNow.ToString("u")).Append("] | ");
|
sb.Append('[').Append(DateTime.UtcNow.ToString("u")).Append("] | ");
|
||||||
sb.Append('[').Append(logLevel).Append("] | ");
|
sb.Append('[').Append(logLevel).Append("] | ");
|
||||||
@@ -77,17 +77,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
sb.Append(" | Context: ");
|
sb.Append(" | Context: ");
|
||||||
foreach (var kvp in contextData)
|
foreach (var kvp in contextData)
|
||||||
{
|
|
||||||
sb.Append(kvp.Key).Append("=").Append(kvp.Value).Append("; ");
|
sb.Append(kvp.Key).Append("=").Append(kvp.Value).Append("; ");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (exception != null)
|
if (exception != null)
|
||||||
{
|
|
||||||
sb.Append(" | Exception: ").Append(exception);
|
sb.Append(" | Exception: ").Append(exception);
|
||||||
}
|
|
||||||
|
|
||||||
_logChannel.Writer.TryWrite(sb.ToString());
|
if (!_logChannel.Writer.TryWrite(sb.ToString()))
|
||||||
|
OnLogDropped?.Invoke(this, sb.ToString()); // notify if log is dropped
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -100,50 +97,145 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
TcpClient client = null;
|
TcpClient client = null;
|
||||||
StreamWriter writer = null;
|
StreamWriter writer = null;
|
||||||
|
|
||||||
try
|
while (!token.IsCancellationRequested)
|
||||||
{
|
|
||||||
client = new TcpClient();
|
|
||||||
await client.ConnectAsync(_options.Host, _options.Port).ConfigureAwait(false);
|
|
||||||
|
|
||||||
writer = new StreamWriter(client.GetStream(), Encoding.UTF8) { AutoFlush = true };
|
|
||||||
|
|
||||||
await foreach (var log in _logChannel.Reader.ReadAllAsync(token))
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await writer.WriteLineAsync(log).ConfigureAwait(false);
|
// Attempt to connect with exponential backoff
|
||||||
}
|
int attempt = 0;
|
||||||
catch (IOException ioEx)
|
while (client == null || !client.Connected)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, ioEx);
|
try
|
||||||
|
{
|
||||||
// Attempt to reconnect
|
client?.Dispose();
|
||||||
writer.Dispose();
|
|
||||||
client.Dispose();
|
|
||||||
client = new TcpClient();
|
client = new TcpClient();
|
||||||
await client.ConnectAsync(_options.Host, _options.Port).ConfigureAwait(false);
|
await ConnectWithRetryAsync(client, _options.Host, _options.Port, token);
|
||||||
writer = new StreamWriter(client.GetStream(), Encoding.UTF8) { AutoFlush = true };
|
writer = new StreamWriter(client.GetStream(), Encoding.UTF8) { AutoFlush = true };
|
||||||
|
break; // connected
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, ex);
|
OnException?.Invoke(this, ex);
|
||||||
|
attempt++;
|
||||||
|
int delay = Math.Min(1000 * (int)Math.Pow(2, attempt), 30000); // max 30s backoff
|
||||||
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process logs in batches
|
||||||
|
var batch = new List<string>(MaxBatchSize);
|
||||||
|
await foreach (var log in _logChannel.Reader.ReadAllAsync(token))
|
||||||
|
{
|
||||||
|
batch.Add(log);
|
||||||
|
if (batch.Count >= MaxBatchSize)
|
||||||
|
{
|
||||||
|
await SafeWriteBatch(writer, batch, client, token);
|
||||||
|
batch.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any remaining logs
|
||||||
|
if (batch.Count > 0)
|
||||||
|
await SafeWriteBatch(writer, batch, client, token);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// normal shutdown
|
break; // normal shutdown
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, ex);
|
OnException?.Invoke(this, ex);
|
||||||
|
await Task.Delay(1000, token); // prevent tight loop
|
||||||
}
|
}
|
||||||
finally
|
}
|
||||||
{
|
|
||||||
|
// Cleanup
|
||||||
writer?.Dispose();
|
writer?.Dispose();
|
||||||
client?.Dispose();
|
client?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ConnectWithRetryAsync(TcpClient client, string host, int port, CancellationToken token)
|
||||||
|
{
|
||||||
|
int attempt = 0;
|
||||||
|
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var connectTask = client.ConnectAsync(host, port);
|
||||||
|
var delayTask = Task.Delay(Timeout.Infinite, token);
|
||||||
|
|
||||||
|
var completed = await Task.WhenAny(connectTask, delayTask);
|
||||||
|
if (completed == connectTask)
|
||||||
|
{
|
||||||
|
await connectTask;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
attempt++;
|
||||||
|
OnException?.Invoke(this, ex);
|
||||||
|
int delayMs = Math.Min(1000 * (int)Math.Pow(2, attempt), 30000);
|
||||||
|
await Task.Delay(delayMs, token);
|
||||||
|
client.Dispose();
|
||||||
|
client = new TcpClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SafeWriteBatch(StreamWriter writer, List<string> batch, TcpClient client, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (writer != null && client?.Connected == true)
|
||||||
|
{
|
||||||
|
await writer.WriteLineAsync(string.Join(Environment.NewLine, batch)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Optionally write to local file if TCP server is down
|
||||||
|
await FallbackWriteAsync(batch).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnException?.Invoke(this, ex);
|
||||||
|
await FallbackWriteAsync(batch).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FallbackWriteAsync(List<string> batch)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_options.EnableFallbackLogging)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fallbackFile = _options.FallbackLogFilePath ?? Path.Combine(AppContext.BaseDirectory, "tcp_logger_fallback.log");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(fallbackFile)!);
|
||||||
|
|
||||||
|
using (var writer = new StreamWriter(fallbackFile, append: true, encoding: Encoding.UTF8))
|
||||||
|
{
|
||||||
|
foreach (var line in batch)
|
||||||
|
{
|
||||||
|
await writer.WriteLineAsync(line).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -152,7 +244,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_processingTask.Wait();
|
_processingTask.Wait(TimeSpan.FromSeconds(5));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; set; } = true;
|
||||||
public bool IncludeCorrelationId { get; set; }
|
public bool IncludeCorrelationId { get; set; }
|
||||||
|
public string FallbackLogFilePath { get; set; }
|
||||||
|
public bool EnableFallbackLogging { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; set; }
|
public bool IncludeCorrelationId { get; set; }
|
||||||
|
|
||||||
public event EventHandler<Exception> OnException;
|
public event EventHandler<Exception> OnException;
|
||||||
|
public event EventHandler<string> OnLogDropped;
|
||||||
|
|
||||||
public UdpLogger(string categoryName, UdpLoggerOptions options)
|
public UdpLogger(string categoryName, UdpLoggerOptions options)
|
||||||
{
|
{
|
||||||
@@ -93,7 +95,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
string fullMessage = string.Join(" | ", logParts);
|
string fullMessage = string.Join(" | ", logParts);
|
||||||
|
|
||||||
// Drop oldest if full
|
// Drop oldest if full
|
||||||
_logChannel.Writer.TryWrite(fullMessage);
|
if (!_logChannel.Writer.TryWrite(fullMessage))
|
||||||
|
{
|
||||||
|
OnLogDropped?.Invoke(this, fullMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -105,6 +110,31 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (_options.EnableBatching && _options.BatchSize > 1)
|
||||||
|
{
|
||||||
|
// batching mode
|
||||||
|
var batch = new List<string>(_options.BatchSize);
|
||||||
|
|
||||||
|
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
|
||||||
|
{
|
||||||
|
batch.Add(message);
|
||||||
|
|
||||||
|
if (batch.Count >= _options.BatchSize)
|
||||||
|
{
|
||||||
|
await SendBatchAsync(batch, token);
|
||||||
|
batch.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send remaining messages
|
||||||
|
if (batch.Count > 0)
|
||||||
|
{
|
||||||
|
await SendBatchAsync(batch, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// non-batching mode: send messages one by one
|
||||||
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
|
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -118,10 +148,35 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// normal shutdown
|
// normal shutdown
|
||||||
}
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
OnException?.Invoke(this, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a batch of log messages as a single UDP packet.
|
||||||
|
/// </summary>
|
||||||
|
private async Task SendBatchAsync(List<string> batch, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string combinedMessage = string.Join(Environment.NewLine, batch);
|
||||||
|
|
||||||
|
const int maxUdpSize = 60_000;
|
||||||
|
if (combinedMessage.Length > maxUdpSize)
|
||||||
|
{
|
||||||
|
combinedMessage = combinedMessage.Substring(0, maxUdpSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(combinedMessage);
|
||||||
|
await _udpClient.SendAsync(bytes, bytes.Length, _options.Host, _options.Port);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, ex);
|
OnException?.Invoke(this, ex);
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; set; } = true;
|
||||||
public bool IncludeCorrelationId { get; set; }
|
public bool IncludeCorrelationId { get; set; }
|
||||||
|
public bool EnableBatching { get; set; }
|
||||||
|
public int BatchSize { get; set; } = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,59 @@ namespace EonaCat.Logger.Extensions
|
|||||||
|
|
||||||
public static class ObjectExtensions
|
public static class ObjectExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes an action on the object if it satisfies a predicate.
|
||||||
|
/// </summary>
|
||||||
|
public static T If<T>(this T obj, Func<T, bool> predicate, Action<T> action)
|
||||||
|
{
|
||||||
|
if (obj != null && predicate(obj))
|
||||||
|
{
|
||||||
|
action(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes an action on the object if it does NOT satisfy a predicate.
|
||||||
|
/// </summary>
|
||||||
|
public static T IfNot<T>(this T obj, Func<T, bool> predicate, Action<T> action)
|
||||||
|
{
|
||||||
|
if (obj != null && !predicate(obj))
|
||||||
|
{
|
||||||
|
action(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a function on an object if not null, returns object itself.
|
||||||
|
/// Useful for chaining.
|
||||||
|
/// </summary>
|
||||||
|
public static T Tap<T>(this T obj, Action<T> action)
|
||||||
|
{
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
action(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if object implements a given interface.
|
||||||
|
/// </summary>
|
||||||
|
public static bool Implements<TInterface>(this object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof(TInterface).IsAssignableFrom(obj.GetType());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dumps any object to a string in JSON, XML, or detailed tree format.
|
/// Dumps any object to a string in JSON, XML, or detailed tree format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -61,6 +114,178 @@ namespace EonaCat.Logger.Extensions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a default value if the object is null.
|
||||||
|
/// </summary>
|
||||||
|
public static T OrDefault<T>(this T obj, T defaultValue = default) =>
|
||||||
|
obj == null ? defaultValue : obj;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the object if not null; otherwise executes a function to get a fallback.
|
||||||
|
/// </summary>
|
||||||
|
public static T OrElse<T>(this T obj, Func<T> fallback) =>
|
||||||
|
obj != null ? obj : fallback();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an object to JSON string with optional formatting.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToJson(this object obj, bool indented = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return obj == null
|
||||||
|
? string.Empty
|
||||||
|
: !indented ? Json.JsonHelper.ToJson(obj, Formatting.None) : Json.JsonHelper.ToJson(obj, Formatting.Indented);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an object to string safely, returns empty string if null.
|
||||||
|
/// </summary>
|
||||||
|
public static string SafeToString(this object obj) =>
|
||||||
|
obj?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if an object is null or default.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsNullOrDefault<T>(this T obj) =>
|
||||||
|
EqualityComparer<T>.Default.Equals(obj, default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Safely casts an object to a specific type, returns default if cast fails.
|
||||||
|
/// </summary>
|
||||||
|
public static T SafeCast<T>(this object obj)
|
||||||
|
{
|
||||||
|
if (obj is T variable)
|
||||||
|
{
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Safely tries to convert object to integer.
|
||||||
|
/// </summary>
|
||||||
|
public static int ToInt(this object obj, int defaultValue = 0)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int.TryParse(obj.ToString(), out var val) ? val : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Safely tries to convert object to long.
|
||||||
|
/// </summary>
|
||||||
|
public static long ToLong(this object obj, long defaultValue = 0)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return long.TryParse(obj.ToString(), out var val) ? val : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Safely tries to convert object to double.
|
||||||
|
/// </summary>
|
||||||
|
public static double ToDouble(this object obj, double defaultValue = 0)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return double.TryParse(obj.ToString(), out var val) ? val : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Safely tries to convert object to bool.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ToBool(this object obj, bool defaultValue = false)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bool.TryParse(obj.ToString(), out var val) ? val : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if an object is of a specific type.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsType<T>(this object obj) => obj is T;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes an action if the object is not null.
|
||||||
|
/// </summary>
|
||||||
|
public static void IfNotNull<T>(this T obj, Action<T> action)
|
||||||
|
{
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
action(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes an action if the object is null.
|
||||||
|
/// </summary>
|
||||||
|
public static void IfNull<T>(this T obj, Action action)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the object into a single-item enumerable.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<T> AsEnumerable<T>(this T obj)
|
||||||
|
{
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
yield return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Safely returns a string representation with max length truncation.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSafeString(this object obj, int maxLength)
|
||||||
|
{
|
||||||
|
string str = obj.SafeToString();
|
||||||
|
return str.Length <= maxLength ? str : str.Substring(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns object hash code safely (0 if null).
|
||||||
|
/// </summary>
|
||||||
|
public static int SafeHashCode(this object obj) =>
|
||||||
|
obj?.GetHashCode() ?? 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the object or throws a custom exception if null.
|
||||||
|
/// </summary>
|
||||||
|
public static T OrThrow<T>(this T obj, Func<Exception> exceptionFactory)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
throw exceptionFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
private static string DumpJson(object currentObject, bool isDetailed)
|
private static string DumpJson(object currentObject, bool isDetailed)
|
||||||
{
|
{
|
||||||
var settings = new JsonSerializerSettings
|
var settings = new JsonSerializerSettings
|
||||||
@@ -421,5 +646,218 @@ namespace EonaCat.Logger.Extensions
|
|||||||
}
|
}
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts any object to a human-readable log string, including collections and nested objects.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToLogString(this object obj, int maxDepth = 3, int currentDepth = 0)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDepth >= maxDepth)
|
||||||
|
{
|
||||||
|
return "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle strings separately
|
||||||
|
if (obj is string str)
|
||||||
|
{
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IEnumerable
|
||||||
|
if (obj is IEnumerable enumerable)
|
||||||
|
{
|
||||||
|
var items = new List<string>();
|
||||||
|
foreach (var item in enumerable)
|
||||||
|
{
|
||||||
|
items.Add(item.ToLogString(maxDepth, currentDepth + 1));
|
||||||
|
}
|
||||||
|
return "[" + string.Join(", ", items) + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle primitive types
|
||||||
|
var type = obj.GetType();
|
||||||
|
if (type.IsPrimitive || obj is decimal || obj is DateTime || obj is Guid)
|
||||||
|
{
|
||||||
|
return obj.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle objects with properties
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var props = type.GetProperties();
|
||||||
|
var sb = new StringBuilder("{");
|
||||||
|
bool first = true;
|
||||||
|
foreach (var p in props)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
sb.Append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
var val = p.GetValue(obj);
|
||||||
|
sb.Append($"{p.Name}={val.ToLogString(maxDepth, currentDepth + 1)}");
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
sb.Append("}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return obj.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if an object is considered "empty": null, empty string, empty collection.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsEmpty(this object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj is string str)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj is ICollection col)
|
||||||
|
{
|
||||||
|
return col.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj is IEnumerable enumerable)
|
||||||
|
{
|
||||||
|
return !enumerable.Cast<object>().Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes an action if the object is not null and not empty.
|
||||||
|
/// </summary>
|
||||||
|
public static void IfNotEmpty<T>(this T obj, Action<T> action)
|
||||||
|
{
|
||||||
|
if (!obj.IsEmpty())
|
||||||
|
{
|
||||||
|
action(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a default value if the object is null or empty.
|
||||||
|
/// </summary>
|
||||||
|
public static T OrDefaultIfEmpty<T>(this T obj, T defaultValue)
|
||||||
|
{
|
||||||
|
return obj.IsEmpty() ? defaultValue : obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the object is numeric (int, float, double, decimal, long, etc.).
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsNumeric(this object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return double.TryParse(obj.ToString(), out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an object to a numeric double, returns default if conversion fails.
|
||||||
|
/// </summary>
|
||||||
|
public static double ToDoubleSafe(this object obj, double defaultValue = 0)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return double.TryParse(obj.ToString(), out var d) ? d : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an object to a numeric int, returns default if conversion fails.
|
||||||
|
/// </summary>
|
||||||
|
public static int ToIntSafe(this object obj, int defaultValue = 0)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int.TryParse(obj.ToString(), out var i) ? i : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the type name of an object safely.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetTypeName(this object obj) =>
|
||||||
|
obj?.GetType().Name ?? "null";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a function if object is not null and returns a fallback value otherwise.
|
||||||
|
/// </summary>
|
||||||
|
public static TResult Map<T, TResult>(this T obj, Func<T, TResult> mapper, TResult fallback = default)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapper(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Masks sensitive strings (like passwords, tokens). Keeps first and last 2 characters visible.
|
||||||
|
/// </summary>
|
||||||
|
public static string MaskSensitive(this string str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str) || str.Length <= 4)
|
||||||
|
{
|
||||||
|
return "****";
|
||||||
|
}
|
||||||
|
|
||||||
|
int len = str.Length - 4;
|
||||||
|
return str.Substring(0, 2) + new string('*', len) + str.Substring(str.Length - 2, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Masks sensitive data in any object property that matches a keyword.
|
||||||
|
/// </summary>
|
||||||
|
public static void MaskProperties(this object obj, params string[] keywords)
|
||||||
|
{
|
||||||
|
if (obj == null || keywords == null || keywords.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var props = obj.GetType().GetProperties();
|
||||||
|
foreach (var p in props)
|
||||||
|
{
|
||||||
|
if (!p.CanRead || !p.CanWrite)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keywords.Any(k => p.Name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0))
|
||||||
|
{
|
||||||
|
var val = p.GetValue(obj) as string;
|
||||||
|
if (!string.IsNullOrEmpty(val))
|
||||||
|
{
|
||||||
|
p.SetValue(obj, val.MaskSensitive());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user