Added extensions
This commit is contained in:
@@ -60,6 +60,7 @@ public static class FileLoggerFactoryExtensions
|
||||
options.Mask = fileLoggerOptions.Mask;
|
||||
options.UseDefaultMasking = fileLoggerOptions.UseDefaultMasking;
|
||||
options.MaskedKeywords = fileLoggerOptions.MaskedKeywords;
|
||||
options.IncludeCorrelationId = fileLoggerOptions.IncludeCorrelationId;
|
||||
}
|
||||
);
|
||||
return builder;
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
||||
|
||||
private string _logFile;
|
||||
private long _currentFileSize;
|
||||
|
||||
private Timer _flushTimer;
|
||||
private readonly ConcurrentDictionary<string, StringBuilder> _buffers = new();
|
||||
private readonly ConcurrentDictionary<string, long> _fileSizes = new();
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
@@ -35,6 +35,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
||||
public string LogFile => _logFile ?? string.Empty;
|
||||
|
||||
public event EventHandler<ErrorMessage> OnError;
|
||||
public event EventHandler<string> OnRollOver;
|
||||
|
||||
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
||||
{
|
||||
@@ -48,6 +49,15 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
||||
|
||||
Directory.CreateDirectory(_path);
|
||||
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; }
|
||||
@@ -179,17 +189,17 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
||||
|
||||
for (int i = _maxRolloverFiles - 1; i >= 1; i--)
|
||||
{
|
||||
var src = Path.Combine(dir, $"{name}.{i}{ext}");
|
||||
var dst = Path.Combine(dir, $"{name}.{i + 1}{ext}");
|
||||
var source = Path.Combine(dir, $"{name}.{i}{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
|
||||
{
|
||||
_rolloverLock.Release();
|
||||
OnRollOver?.Invoke(this, file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,13 +224,10 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
||||
_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) =>
|
||||
string.IsNullOrWhiteSpace(_fileNamePrefix)
|
||||
? Path.Combine(_path, $"{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, $"{Environment.MachineName}_{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()
|
||||
{
|
||||
|
||||
@@ -19,10 +19,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
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 event EventHandler<Exception> OnException;
|
||||
public event EventHandler<string> OnLogDropped;
|
||||
|
||||
public TcpLogger(string categoryName, TcpLoggerOptions options)
|
||||
{
|
||||
@@ -37,6 +39,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
SingleWriter = false
|
||||
});
|
||||
|
||||
// Start background processing task
|
||||
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token), _cts.Token);
|
||||
}
|
||||
|
||||
@@ -51,9 +54,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel) || formatter == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -65,7 +66,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
|
||||
string message = formatter(state, exception);
|
||||
|
||||
// Build message
|
||||
var sb = new StringBuilder(256);
|
||||
sb.Append('[').Append(DateTime.UtcNow.ToString("u")).Append("] | ");
|
||||
sb.Append('[').Append(logLevel).Append("] | ");
|
||||
@@ -77,17 +77,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
{
|
||||
sb.Append(" | Context: ");
|
||||
foreach (var kvp in contextData)
|
||||
{
|
||||
sb.Append(kvp.Key).Append("=").Append(kvp.Value).Append("; ");
|
||||
}
|
||||
}
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -100,51 +97,146 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
TcpClient client = 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
|
||||
// Attempt to connect with exponential backoff
|
||||
int attempt = 0;
|
||||
while (client == null || !client.Connected)
|
||||
{
|
||||
await writer.WriteLineAsync(log).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
client?.Dispose();
|
||||
client = new TcpClient();
|
||||
await ConnectWithRetryAsync(client, _options.Host, _options.Port, token);
|
||||
writer = new StreamWriter(client.GetStream(), Encoding.UTF8) { AutoFlush = true };
|
||||
break; // connected
|
||||
}
|
||||
catch (Exception 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);
|
||||
}
|
||||
}
|
||||
catch (IOException ioEx)
|
||||
{
|
||||
OnException?.Invoke(this, ioEx);
|
||||
|
||||
// Attempt to reconnect
|
||||
writer.Dispose();
|
||||
client.Dispose();
|
||||
client = new TcpClient();
|
||||
await client.ConnectAsync(_options.Host, _options.Port).ConfigureAwait(false);
|
||||
writer = new StreamWriter(client.GetStream(), Encoding.UTF8) { AutoFlush = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
// Process logs in batches
|
||||
var batch = new List<string>(MaxBatchSize);
|
||||
await foreach (var log in _logChannel.Reader.ReadAllAsync(token))
|
||||
{
|
||||
OnException?.Invoke(this, ex);
|
||||
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)
|
||||
{
|
||||
break; // normal shutdown
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnException?.Invoke(this, ex);
|
||||
await Task.Delay(1000, token); // prevent tight loop
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
||||
// Cleanup
|
||||
writer?.Dispose();
|
||||
client?.Dispose();
|
||||
}
|
||||
|
||||
private async Task ConnectWithRetryAsync(TcpClient client, string host, int port, CancellationToken token)
|
||||
{
|
||||
int attempt = 0;
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
// normal shutdown
|
||||
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);
|
||||
}
|
||||
finally
|
||||
{
|
||||
writer?.Dispose();
|
||||
client?.Dispose();
|
||||
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()
|
||||
{
|
||||
_cts.Cancel();
|
||||
@@ -152,7 +244,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
|
||||
try
|
||||
{
|
||||
_processingTask.Wait();
|
||||
_processingTask.Wait(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -13,5 +13,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
public int Port { get; set; }
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
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;
|
||||
|
||||
public bool IncludeCorrelationId { get; set; }
|
||||
|
||||
public event EventHandler<Exception> OnException;
|
||||
public event EventHandler<string> OnLogDropped;
|
||||
|
||||
public UdpLogger(string categoryName, UdpLoggerOptions options)
|
||||
{
|
||||
@@ -93,7 +95,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
string fullMessage = string.Join(" | ", logParts);
|
||||
|
||||
// Drop oldest if full
|
||||
_logChannel.Writer.TryWrite(fullMessage);
|
||||
if (!_logChannel.Writer.TryWrite(fullMessage))
|
||||
{
|
||||
OnLogDropped?.Invoke(this, fullMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -105,16 +110,42 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
|
||||
if (_options.EnableBatching && _options.BatchSize > 1)
|
||||
{
|
||||
try
|
||||
// batching mode
|
||||
var batch = new List<string>(_options.BatchSize);
|
||||
|
||||
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(message);
|
||||
await _udpClient.SendAsync(bytes, bytes.Length, _options.Host, _options.Port);
|
||||
batch.Add(message);
|
||||
|
||||
if (batch.Count >= _options.BatchSize)
|
||||
{
|
||||
await SendBatchAsync(batch, token);
|
||||
batch.Clear();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
// send remaining messages
|
||||
if (batch.Count > 0)
|
||||
{
|
||||
OnException?.Invoke(this, ex);
|
||||
await SendBatchAsync(batch, token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// non-batching mode: send messages one by one
|
||||
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(message);
|
||||
await _udpClient.SendAsync(bytes, bytes.Length, _options.Host, _options.Port);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnException?.Invoke(this, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +153,30 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
OnException?.Invoke(this, ex);
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
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
|
||||
{
|
||||
/// <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>
|
||||
/// Dumps any object to a string in JSON, XML, or detailed tree format.
|
||||
/// </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)
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
@@ -421,5 +646,218 @@ namespace EonaCat.Logger.Extensions
|
||||
}
|
||||
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