diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs index a2dba27..d6bc593 100644 --- a/ConsoleApp1/Program.cs +++ b/ConsoleApp1/Program.cs @@ -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); +} -await logger.DisposeAsync(); // Flushes all logs \ No newline at end of file +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!"); +} diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj index 97c76d9..d57307e 100644 --- a/EonaCat.LogStack/EonaCat.LogStack.csproj +++ b/EonaCat.LogStack/EonaCat.LogStack.csproj @@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f EonaCat (Jeroen Saey) EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey - 0.1.4 + 0.1.6 README.md True LICENSE @@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f - 0.1.4+{chash:10}.{c:ymd} + 0.1.6+{chash:10}.{c:ymd} true true v[0-9]* @@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f - 0.1.4 + 0.1.6 EonaCat.LogStack EonaCat.LogStack https://git.saey.me/EonaCat/EonaCat.LogStack diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs index fe0e59e..078826e 100644 --- a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs @@ -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; @@ -198,7 +199,19 @@ namespace EonaCat.LogStack.Flows _maxMemoryBytes = maxMemoryBytes; _flushInterval = TimeSpan.FromMilliseconds(flushIntervalMs); _outputFormat = outputFormat; - _compressionFormat = compression; + _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(logLevelsForSeparateFiles) @@ -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(new ConcurrentQueue(), 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 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 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(""); of.Writer.WriteLine(""); + 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)); } + /// + /// 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 + /// + 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; + } + + /// + /// 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. + /// + 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) diff --git a/EonaCat.LogStack/Helpers/DirectoryPermissionHelper.cs b/EonaCat.LogStack/Helpers/DirectoryPermissionHelper.cs index 0bccec8..1674e59 100644 --- a/EonaCat.LogStack/Helpers/DirectoryPermissionHelper.cs +++ b/EonaCat.LogStack/Helpers/DirectoryPermissionHelper.cs @@ -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 } } + /// + /// 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. + /// + 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; + } + } + + /// + /// Attempts to set Windows directory permissions by clearing the read-only attribute. + /// + 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; + } + } + + /// + /// Attempts to set Unix (Linux/Mac) directory permissions using chmod. + /// Sets full permissions (777) for the directory. + /// + 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; + } + } + /// /// 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. diff --git a/Testers/EonaCat.LogStack.Test.Web/Program.cs b/Testers/EonaCat.LogStack.Test.Web/Program.cs index 9945fac..6138a59 100644 --- a/Testers/EonaCat.LogStack.Test.Web/Program.cs +++ b/Testers/EonaCat.LogStack.Test.Web/Program.cs @@ -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 { new ServiceMonitoring.ServiceStatus { Host = "google.com", Port = 443, ServiceType = ServiceMonitoring.ServiceType.HTTPS } }, checkInterval: TimeSpan.FromSeconds(5)); //logBuilder.WriteToSignalR(); //logBuilder.WriteToEmailFlow();