This commit is contained in:
Jeroen Saey
2026-07-21 16:32:32 +02:00
parent 266af8abdb
commit f2fa009200
5 changed files with 370 additions and 97 deletions
+31 -16
View File
@@ -7,28 +7,43 @@ using EonaCat.LogStack.Core;
var logger = new LogBuilder("MyApp")
.WithMinimumLevel(LogLevel.Information)
.WriteToConsole()
.WriteToFile("C:\\tesss", maxFileSize: 50 * 1024 * 1024)
//.WriteToJsonFile("./logs", maxFileSize: 50 * 1024 * 1024)
//.WriteToHttp("https://127.0.0.1")
//.WriteToUdp("127.0.0.1", 514)
//.WriteToTcp("127.0.0.1", 514)
//.WriteToDatabase(null)
//.WriteToDiscord("https://discord.com/api/webhooks/...")
//.WriteToMicrosoftTeams("https://outlook.office.com/webhook/...")
//.WriteToElasticSearch("http://localhost:9200/logs")
//.WriteToGraylogFlow(null)
//.WriteToZabbixFlow(null)
.WriteToFile("./testlogs", maxFileSize: 50 * 1024 * 1024)
.BoostWithCorrelationId()
.BoostWithProcessId()
.Build();
Console.WriteLine("Logger created successfully");
Console.WriteLine("Writing log entries...");
while (true)
// Test: Write a few log entries and then dispose
for (int i = 0; i < 10; i++)
{
logger.Information("Application started");
logger.Error(new Exception("Nerd!"), "Something went wrong");
await Task.Delay(1);
logger.Information("Application started - iteration " + i);
logger.Error(new Exception("Nerd!"), "Something went wrong - iteration " + i);
}
Console.WriteLine("Wrote 10 log entries");
// Wait a bit for the flush to happen
await Task.Delay(3000);
Console.WriteLine("Disposing logger...");
await logger.DisposeAsync(); // Flushes all logs
Console.WriteLine("Logging complete. Check ./testlogs for log files.");
// List files that were created
var logdir = new System.IO.DirectoryInfo("./testlogs");
if (logdir.Exists)
{
var files = logdir.GetFiles();
Console.WriteLine($"Found {files.Length} log files:");
foreach (var file in files)
{
Console.WriteLine($" - {file.Name} ({file.Length} bytes)");
}
}
else
{
Console.WriteLine("./testlogs directory does not exist!");
}
+3 -3
View File
@@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
<Copyright>EonaCat (Jeroen Saey)</Copyright>
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
<PackageIconUrl />
<FileVersion>0.1.4</FileVersion>
<FileVersion>0.1.6</FileVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
</PropertyGroup>
<PropertyGroup>
<EVRevisionFormat>0.1.4+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVRevisionFormat>0.1.6+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVDefault>true</EVDefault>
<EVInfo>true</EVInfo>
<EVTagMatch>v[0-9]*</EVTagMatch>
@@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
</PropertyGroup>
<PropertyGroup>
<Version>0.1.4</Version>
<Version>0.1.6</Version>
<PackageId>EonaCat.LogStack</PackageId>
<Product>EonaCat.LogStack</Product>
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl>
@@ -9,6 +9,7 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -200,6 +201,18 @@ namespace EonaCat.LogStack.Flows
_outputFormat = outputFormat;
_compressionFormat = compression;
// Use the cascading fallback strategy to resolve the final logging directory
try
{
_directory = ResolveLoggingDirectory(_directory);
}
catch (Exception ex)
{
WriteToConsoleError($"[FileFlow] Critical error in directory resolution: {ex.Message}");
_directory = Path.GetTempPath();
OnDirectoryException?.Invoke(this, $"FileFlow: Critical failure in directory resolution: {ex.Message}. Falling back to temp: '{_directory}'");
}
_logLevelsForSeparateFiles = logLevelsForSeparateFiles != null
? new HashSet<LogLevel>(logLevelsForSeparateFiles)
: new HashSet<LogLevel>();
@@ -207,58 +220,13 @@ namespace EonaCat.LogStack.Flows
SetFileExtension(outputFormat);
CompileTemplate(template);
// Resolve relative path
if (_directory.StartsWith("./", StringComparison.Ordinal))
{
_directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _directory.Substring(2));
}
// Never allow logging initialization to crash the host because of a bad
// directory (ACLs, antivirus locks, read-only locations, etc.).
// Fall back to a per-user temp directory.
try
{
// Use DirectoryPermissionHelper to ensure directory with proper permissions
if (!Helpers.DirectoryPermissionHelper.EnsureDirectoryHierarchy(_directory))
{
throw new UnauthorizedAccessException($"Cannot write to directory: {Helpers.DirectoryPermissionHelper.GetAccessIssueDiagnosis(_directory)}");
}
}
catch (Exception ex)
{
try
{
var processId = Process.GetCurrentProcess().Id;
var newDirectory = Path.Combine(Path.GetTempPath(), "EonaCat.LogStack", processId.ToString());
if (Helpers.DirectoryPermissionHelper.EnsureDirectoryHierarchy(newDirectory))
{
OnDirectoryException?.Invoke(this, $"FileFlow: Could not create directory: '{_directory}' ({ex.Message}), using directory '{newDirectory}' instead");
_directory = newDirectory;
}
else
{
throw new InvalidOperationException($"Failed to create fallback directory: {newDirectory}");
}
}
catch
{
var newDirectory = Path.GetTempPath();
OnDirectoryException?.Invoke(this, $"FileFlow: Could not create any writable directory, falling back to temp: '{newDirectory}'. Original error: {ex.Message}");
_directory = newDirectory;
// Last resort: disable file output by pointing to temp path.
// The writer thread still runs and swallows failures.
}
}
// BlockingCollection with bounded capacity
_queue = new BlockingCollection<LogEvent>(new ConcurrentQueue<LogEvent>(), QueueCapacity);
// Dedicated writer thread
_writerThread = new Thread(WriterThreadBody)
{
IsBackground = true,
IsBackground = false,
Name = "FileFlow.Writer[" + filePrefix + "]",
Priority = ThreadPriority.AboveNormal,
};
@@ -830,6 +798,14 @@ namespace EonaCat.LogStack.Flows
_isDisposing = true; // Signal all threads that disposal is in progress
_queue.CompleteAdding();
// Give the writer thread time to drain the queue before canceling
Stopwatch sw = Stopwatch.StartNew();
while (_queue.Count > 0 && sw.Elapsed < TimeSpan.FromSeconds(5))
{
Thread.Sleep(10);
}
_cts.Cancel();
// Give threads more time to gracefully exit (increased from 2000ms to 5000ms)
@@ -873,6 +849,7 @@ namespace EonaCat.LogStack.Flows
}
private void WriterThreadBody()
{
WriteToConsoleError("[FileFlow] Writer thread started");
try
{
while (!_queue.IsCompleted)
@@ -940,6 +917,8 @@ namespace EonaCat.LogStack.Flows
try
{
// Write to both stdout and stderr to ensure visibility
Console.WriteLine(text);
Console.Error.WriteLine(text);
}
catch
@@ -950,7 +929,7 @@ namespace EonaCat.LogStack.Flows
private void WriteLogEvent(LogEvent log)
{
long size = 0;
long size =0;
try
{
size = EstimateSize(log);
@@ -1015,28 +994,52 @@ namespace EonaCat.LogStack.Flows
{
try
{
EnsureFileOpen(path, log);
if (ShouldRotate(path, line.Length))
if (!EnsureFileOpen(path, log))
{
string archived = RotateFile(path);
EnsureFileOpen(path, log);
if (archived != null)
// File open failed; skip write operation but continue with other processing
}
else
{
if (ShouldRotate(path, line.Length))
{
Action<string> onRotate = _onRotate;
if (onRotate != null)
string archived = RotateFile(path);
if (!EnsureFileOpen(path, log))
{
try { onRotate(archived); }
catch { /* Do nothing */ }
// File reopen after rotation failed; skip this write
}
else
{
OpenFile of;
if (_openFiles.TryGetValue(path, out of))
{
of.Writer.WriteLine(line);
of.Size += line.Length + Environment.NewLine.Length;
}
}
if (archived != null)
{
Action<string> onRotate = _onRotate;
if (onRotate != null)
{
try { onRotate(archived); }
catch { /* Do nothing */ }
}
}
}
else
{
OpenFile of;
if (_openFiles.TryGetValue(path, out of))
{
of.Writer.WriteLine(line);
of.Size += line.Length + Environment.NewLine.Length;
}
else
{
// File handle disappeared between EnsureFileOpen and write attempt
WriteToConsoleError("[FileFlow] File handle for '" + path + "' missing after EnsureFileOpen");
}
}
}
OpenFile of;
if (_openFiles.TryGetValue(path, out of))
{
of.Writer.WriteLine(line);
of.Size += line.Length + Environment.NewLine.Length;
}
}
catch (Exception ex)
@@ -1522,7 +1525,7 @@ namespace EonaCat.LogStack.Flows
}
}
private void EnsureFileOpen(string path, LogEvent logEvent)
private bool EnsureFileOpen(string path, LogEvent logEvent)
{
try
{
@@ -1533,7 +1536,7 @@ namespace EonaCat.LogStack.Flows
{
if (File.Exists(path))
{
return;
return true;
}
}
catch
@@ -1584,6 +1587,7 @@ namespace EonaCat.LogStack.Flows
try
{
of.Writer.Write(CsvHeader);
of.Writer.Flush();
of.HasCsvHeader = true;
}
catch (Exception ex)
@@ -1600,6 +1604,7 @@ namespace EonaCat.LogStack.Flows
{
of.Writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
of.Writer.WriteLine("<logs>");
of.Writer.Flush();
of.HasXmlHeader = true;
}
else
@@ -1614,6 +1619,8 @@ namespace EonaCat.LogStack.Flows
WriteToConsoleError("[FileFlow] XML header write error for '" + path + "': " + ex.Message);
}
}
return true;
}
catch (Exception ex)
{
@@ -1640,6 +1647,7 @@ namespace EonaCat.LogStack.Flows
}
}
WriteToConsoleError("[FileFlow] Failed to open '" + path + "': " + ex.Message + diagnosis);
return false;
}
}
catch (Exception ex)
@@ -1650,6 +1658,7 @@ namespace EonaCat.LogStack.Flows
WriteToConsoleError("[FileFlow] EnsureFileOpen unhandled error: " + ex.Message);
}
catch { /* ignore */ }
return false;
}
}
@@ -1870,6 +1879,7 @@ namespace EonaCat.LogStack.Flows
private void PeriodicFlushLoop()
{
WriteToConsoleError("[FileFlow] Periodic flush loop started");
try
{
while (!_isDisposing && !_cts.Token.IsCancellationRequested)
@@ -2032,6 +2042,141 @@ namespace EonaCat.LogStack.Flows
string.Concat(prefix, "_", CachedMachineName, "_", date.ToString(_dateFormat), _fileExtension));
}
/// <summary>
/// Resolves the logging directory with a cascading fallback strategy:
/// 1. Try the requested directory
/// 2. Try to fix permissions on the requested directory
/// 3. Fall back to %appdata%/EonaCat.LogStack/
/// 4. Fall back to TEMP directory
/// </summary>
private string ResolveLoggingDirectory(string requestedDirectory)
{
// Resolve relative paths first
string workingDirectory = requestedDirectory;
if (workingDirectory.StartsWith("./", StringComparison.Ordinal))
{
workingDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, workingDirectory.Substring(2));
}
// Attempt 1: Use the requested directory
WriteToConsoleError($"[FileFlow] Attempting to use log directory: {workingDirectory}");
if (Helpers.DirectoryPermissionHelper.EnsureDirectoryHierarchy(workingDirectory))
{
WriteToConsoleError($"[FileFlow] Successfully using log directory: {workingDirectory}");
return workingDirectory;
}
WriteToConsoleError($"[FileFlow] Failed to use log directory: {workingDirectory}. Diagnosis: {Helpers.DirectoryPermissionHelper.GetAccessIssueDiagnosis(workingDirectory)}");
// Attempt 2: Try to fix permissions on the requested directory
WriteToConsoleError($"[FileFlow] Attempting to fix permissions on: {workingDirectory}");
if (TryFixDirectoryPermissions(workingDirectory) && Helpers.DirectoryPermissionHelper.EnsureDirectoryHierarchy(workingDirectory))
{
WriteToConsoleError($"[FileFlow] Successfully fixed permissions and using log directory: {workingDirectory}");
return workingDirectory;
}
WriteToConsoleError($"[FileFlow] Failed to fix permissions on: {workingDirectory}");
// Attempt 3: Fall back to AppData directory
string appDataDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"EonaCat.LogStack");
WriteToConsoleError($"[FileFlow] Attempting to use AppData log directory: {appDataDirectory}");
if (Helpers.DirectoryPermissionHelper.EnsureDirectoryHierarchy(appDataDirectory))
{
WriteToConsoleError($"[FileFlow] Successfully using AppData log directory: {appDataDirectory}");
OnDirectoryException?.Invoke(this, $"FileFlow: Could not use requested directory '{requestedDirectory}', fell back to '{appDataDirectory}'");
return appDataDirectory;
}
WriteToConsoleError($"[FileFlow] Failed to use AppData log directory: {appDataDirectory}. Diagnosis: {Helpers.DirectoryPermissionHelper.GetAccessIssueDiagnosis(appDataDirectory)}");
// Attempt 4: Fall back to TEMP directory
string tempDirectory = Path.GetTempPath();
WriteToConsoleError($"[FileFlow] Attempting to use TEMP log directory: {tempDirectory}");
try
{
if (Helpers.DirectoryPermissionHelper.CanWrite(tempDirectory))
{
WriteToConsoleError($"[FileFlow] Successfully using TEMP log directory: {tempDirectory}");
OnDirectoryException?.Invoke(this, $"FileFlow: Could not use requested directory '{requestedDirectory}' or AppData, fell back to TEMP directory '{tempDirectory}'");
return tempDirectory;
}
}
catch
{
// Temp directory check failed
}
WriteToConsoleError($"[FileFlow] Failed to use TEMP log directory: {tempDirectory}. This is a critical error - logging may not work.");
// Final fallback: return the temp path anyway, even if it might not work
OnDirectoryException?.Invoke(this, $"FileFlow: Critical - could not find any writable directory. Attempting to use TEMP: '{tempDirectory}'");
return tempDirectory;
}
/// <summary>
/// Attempts to fix directory permissions across all platforms (Windows, Linux, Mac).
/// On Windows: Clears read-only attributes.
/// On Linux/Mac: Uses chmod to set full permissions.
/// Returns true if fix was successful or attempted, false if it failed or is not applicable.
/// </summary>
private bool TryFixDirectoryPermissions(string dirPath)
{
try
{
if (!Directory.Exists(dirPath))
{
// Try to create it first
try
{
Directory.CreateDirectory(dirPath);
}
catch
{
return false;
}
}
// Attempt to set everyone permissions (handles all platforms)
if (Helpers.DirectoryPermissionHelper.TrySetEveryonePermissions(dirPath))
{
WriteToConsoleError($"[FileFlow] Set permissions on: {dirPath}");
return true;
}
// Fallback: On Windows, try clearing the read-only attribute directly
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
try
{
var dirInfo = new DirectoryInfo(dirPath);
if ((dirInfo.Attributes & FileAttributes.ReadOnly) != 0)
{
dirInfo.Attributes &= ~FileAttributes.ReadOnly;
WriteToConsoleError($"[FileFlow] Cleared read-only attribute on: {dirPath}");
}
return true;
}
catch (Exception ex)
{
WriteToConsoleError($"[FileFlow] Failed to fix permissions: {ex.Message}");
return false;
}
}
// On Unix-like systems, we've already attempted chmod via TrySetEveryonePermissions
return false;
}
catch
{
return false;
}
}
private void SetFileExtension(FileOutputFormat fmt)
{
switch (fmt)
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
@@ -35,7 +36,7 @@ namespace EonaCat.LogStack.Helpers
}
// Try to create a temporary file to verify write access
var testFile = Path.Combine(dirPath, ".permission_test_" + Guid.NewGuid().ToString("."));
var testFile = Path.Combine(dirPath, ".permission_test_" + Guid.NewGuid().ToString("N"));
try
{
using (var fs = File.Create(testFile, 1, FileOptions.DeleteOnClose))
@@ -150,6 +151,115 @@ namespace EonaCat.LogStack.Helpers
}
}
/// <summary>
/// Attempts to set full permissions for everyone on the directory.
/// Falls back to clearing the read-only attribute if needed on Windows.
/// Uses chmod on Linux/Mac and Windows permission APIs on Windows.
/// This is a best-effort operation for all platforms.
/// </summary>
public static bool TrySetEveryonePermissions(string dirPath)
{
if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath))
{
return false;
}
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return TrySetWindowsPermissions(dirPath);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return TrySetUnixPermissions(dirPath);
}
// Unknown platform - try whatever we can
return CanWrite(dirPath);
}
catch
{
return false;
}
}
/// <summary>
/// Attempts to set Windows directory permissions by clearing the read-only attribute.
/// </summary>
private static bool TrySetWindowsPermissions(string dirPath)
{
try
{
var dirInfo = new DirectoryInfo(dirPath);
// Clear read-only attribute if set
if ((dirInfo.Attributes & FileAttributes.ReadOnly) != 0)
{
try
{
dirInfo.Attributes &= ~FileAttributes.ReadOnly;
}
catch
{
// Continue even if attribute clearing fails
}
}
// Verify the directory is now writable
return CanWrite(dirPath);
}
catch
{
return false;
}
}
/// <summary>
/// Attempts to set Unix (Linux/Mac) directory permissions using chmod.
/// Sets full permissions (777) for the directory.
/// </summary>
private static bool TrySetUnixPermissions(string dirPath)
{
if (string.IsNullOrEmpty(dirPath))
{
return false;
}
try
{
// Use chmod to set full permissions (777)
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "chmod",
Arguments = $"777 \"{dirPath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit(5000); // Wait up to 5 seconds
if (process.ExitCode == 0)
{
// Verify the directory is now writable
return CanWrite(dirPath);
}
return false;
}
catch
{
// chmod not available or failed
return false;
}
}
/// <summary>
/// Attempts to grant write permissions on Windows by clearing the read-only attribute.
/// This is a best-effort operation and may fail on restricted systems.
+11 -8
View File
@@ -51,21 +51,24 @@ namespace EonaCat.LogStack.Test.Web
var logBuilder = new LogBuilder();
logBuilder.WithTimestampMode(TimestampMode.Local);
logBuilder.WriteToConsole();
logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Csv);
logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.StructuredJson);
logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Text);
logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Json);
logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Xml);
// Use absolute path to ensure directory creation works
var logDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EonaCat.LogStack", "Logs");
Directory.CreateDirectory(logDirectory);
logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Csv);
logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.StructuredJson);
logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Text);
logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Json);
logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Xml);
logBuilder.WriteToTcp("127.0.0.1", 514);
//logBuilder.WriteToEncryptedFile("./logs");
//logBuilder.WriteDiagnostics();
logBuilder.WriteToAudit(
directory: "./audit",
directory: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EonaCat.LogStack", "Audit"),
auditLevel: AuditLevel.WarningAndAbove, // only Warn+ go to audit trail
includeProperties: true);
//logBuilder.WriteToRollingBuffer();
var consoleFlow = logBuilder.GetFlow(typeof(ConsoleFlow));
logBuilder.WriteToThrottled(consoleFlow);
logBuilder.WriteToStatusFlow(new List<ServiceMonitoring.ServiceStatus> { new ServiceMonitoring.ServiceStatus { Host = "google.com", Port = 443, ServiceType = ServiceMonitoring.ServiceType.HTTPS } }, checkInterval: TimeSpan.FromSeconds(5));
//logBuilder.WriteToSignalR();
//logBuilder.WriteToEmailFlow();