# GitConverter.Lib.Logging Small, testable logging abstraction and adapters providing structured diagnostic output for the library and CLI application. --- ## Table of Contents - [Purpose](#purpose) - [Architecture](#architecture) - [Key Types](#key-types) - [Usage Patterns](#usage-patterns) - [Logging Helpers](#logging-helpers) - [Configuration](#configuration) - [Testing](#testing) - [Troubleshooting](#troubleshooting) - [Best Practices](#best-practices) - [API Reference](#api-reference) --- ## Purpose Provide a minimal, testable logging abstraction (`IAppLogger`) used by library components (converters, factory, utils) while allowing multiple underlying implementations: - **No-op logging** (default) — zero overhead when logging is disabled - **File logging** via log4net — structured, rolling logs for production - **Test logging** — capture diagnostics for test assertions **Design principle**: Logging is **best-effort** and must never change program control flow. Initialization failures are reported but do not crash the application. --- ## Architecture ``` ┌─────────────────────────────────────────┐ │ Library Components │ │ (Converters, Factory, Utils) │ │ │ │ Uses: Log.Debug/Info/Warn/Error │ └──────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Log (Static Facade) │ │ - Current: IAppLogger │ │ - SetFile(path): Configure log4net │ │ - Enable()/Disable(): Activate/deactivate│ └──────────────┬──────────────────────────┘ │ ┌───────┴───────┐ │ │ ▼ ▼ ┌─────────┐ ┌──────────────┐ │NullLogger│ │Log4NetAdapter│ │(No-op) │ │(File logging)│ └─────────┘ └──────────────┘ ``` --- ## Key Types ### `IAppLogger` Core abstraction defining the logging contract. **Methods:** - `void Debug(string message)` — Detailed diagnostic info (verbose) - `void Info(string message)` — High-level milestones and lifecycle events - `void Warn(string message)` — Recoverable issues, unexpected conditions - `void Error(string message, Exception ex = null)` — Failures affecting operation **Contract:** - Implementations must be **non-throwing** - Methods should be **lightweight** (async not required) - Thread-safe for concurrent callers (best-effort) --- ### `Log` (Static Facade) Convenience entry point for library code. Wraps an `IAppLogger` instance. #### Properties ``` // Active logger (defaults to NullLogger) public static IAppLogger Current { get; set; } ``` #### Methods ``` // Configure file logging with log4net public static void SetFile(string logFilePath) // Activate logging (use existing log4net config) public static void Enable() // Disable logging (revert to NullLogger) public static void Disable() // Convenience forwarding methods public static void Debug(string message) public static void Info(string message) public static void Warn(string message) public static void Error(string message, Exception ex = null) ``` **Example:** ``` // Configure file logging (call once at startup) Log.SetFile("logs/conversion.log"); Log.Enable(); // Use throughout library code Log.Info("Starting conversion process"); Log.Debug($"Processing file: {fileName}"); Log.Error("Conversion failed", exception); ``` --- ### `NullLogger` No-op implementation used when logging is disabled. - **Zero overhead** — all methods are empty - **Default logger** — `Log.Current` defaults to `NullLogger` - **Thread-safe** — stateless singleton pattern **When used:** - Application startup before logging is configured - Tests that don't require diagnostics - Production scenarios where logging is explicitly disabled --- ### `Log4NetAdapter` Adapter forwarding calls to log4net's `ILog` interface. **Configuration:** - Programmatic setup via `Log.SetFile(path)` (recommended) - External XML configuration (app.config/log4net.config) **Features:** - **Rolling file appender** — 10MB per file, 5 backups - **Pattern layout**: `%date %-5level %logger - %message%newline%exception` - **Minimal locking** — allows concurrent file readers - **All levels enabled** — DEBUG through FATAL **Example output:** ``` 2024-01-15 10:23:45 INFO GitConverter.Lib.Factories.ConverterFactory - ConverterFactory initialized 2024-01-15 10:23:46 DEBUG GitConverter.Lib.Converters.GeoJsonConverter - Reading input: input.shp 2024-01-15 10:23:47 ERROR GitConverter.Lib.Converters.GeoJsonConverter - Conversion failed System.IO.FileNotFoundException: File not found: input.shp at ... ``` --- ### `LogHelper` (Formatting Utilities) Static helper class providing structured log formatting. #### Methods ``` // Underline matching message length LogHelper.LogWithUnderline(IAppLogger logger, string message, char underlineChar = '=') // Fixed-length separator LogHelper.LogWithSeparator(IAppLogger logger, string message, char separatorChar = '-', int separatorLength = 80) // Boxed message LogHelper.LogWithBox(IAppLogger logger, string message, char borderChar = '=') // Blank line LogHelper.LogBlankLine(IAppLogger logger) ``` **Example:** ``` // Section header with underline LogHelper.LogWithUnderline(Log.Current, "Processing Phase 1", '='); // Output: // Processing Phase 1 // ================== // Fixed-length separator LogHelper.LogWithSeparator(Log.Current, "Initialization complete", '-', 80); // Output: // Initialization complete // -------------------------------------------------------------------------------- // Boxed message for emphasis LogHelper.LogWithBox(Log.Current, "CRITICAL: Validation Required", '*'); // Output: // ************************** // | CRITICAL: Validation Required | // ************************** // Blank line for spacing LogHelper.LogBlankLine(Log.Current); ``` --- ## Usage Patterns ### Library Components ``` using GitConverter.Lib.Logging; public class GeoJsonConverter : IConverter { public ConversionResult Convert(...) { Log.Info($"Starting GeoJSON conversion: {gisInputFilePath}"); try { // ... conversion logic ... Log.Debug($"Processed {featureCount} features"); return ConversionResult.Success("Conversion completed"); } catch (Exception ex) { Log.Error("Conversion failed", ex); return ConversionResult.Failure($"Error: {ex.Message}"); } } } ``` --- ### CLI Application (Program.cs) ``` private static void Main(string[] args) { // Enable UTF-8 for emoji and international characters Console.OutputEncoding = System.Text.Encoding.UTF8; // Parse optional log path from args var logPath = GetOptionalLogFolderPath(args, 5); // Configure logging (best-effort) SetLogger(logPath); // ... run conversion ... } private static void SetLogger(string logFilePath) { if (string.IsNullOrWhiteSpace(logFilePath)) { Log.Disable(); return; } try { Log.SetFile(logFilePath.Trim().Trim('"')); Log.Enable(); } catch (Exception ex) { Console.Error.WriteLine($"⚠️ Warning: logging disabled — {ex.Message}"); Log.Disable(); } } ``` --- ## Logging Helpers ### Structured Output Examples ``` // Simple separator Log.Info("Starting batch conversion"); LogHelper.LogWithSeparator(Log.Current, "Processing 50 files", '-', 80); // Section with underline LogHelper.LogWithUnderline(Log.Current, "Validation Results", '='); Log.Info("All files validated successfully"); // Critical notice LogHelper.LogWithBox(Log.Current, "BACKUP RECOMMENDED", '*'); // Spacing LogHelper.LogBlankLine(Log.Current); Log.Info("Next phase starting..."); ``` **Output:** ``` 2024-01-15 10:23:45 INFO App - Starting batch conversion 2024-01-15 10:23:45 INFO App - Processing 50 files 2024-01-15 10:23:45 INFO App - -------------------------------------------------------------------------------- 2024-01-15 10:23:45 INFO App - Validation Results 2024-01-15 10:23:45 INFO App - =================== 2024-01-15 10:23:45 INFO App - All files validated successfully 2024-01-15 10:23:45 INFO App - ************************ 2024-01-15 10:23:45 INFO App - | BACKUP RECOMMENDED | 2024-01-15 10:23:45 INFO App - ************************ 2024-01-15 10:23:45 INFO App - 2024-01-15 10:23:45 INFO App - Next phase starting... ``` --- ## Configuration ### File Logging Setup ``` // Programmatic configuration (recommended) Log.SetFile("logs/conversion.log"); Log.Enable(); ``` **What happens:** 1. Creates `logs/` directory if missing 2. Tests write permissions (fails fast) 3. Configures `RollingFileAppender`: - **Pattern**: `%date %-5level %logger - %message%newline%exception` - **Rolling**: Size-based (10MB max, 5 backups) - **Locking**: Minimal (concurrent reads allowed) - **Levels**: ALL (DEBUG, INFO, WARN, ERROR, FATAL) - **Encoding**: UTF-8 4. **Repository setup**: Configures log4net repository for entry/executing assembly 5. **Adapter creation**: Replaces `Log.Current` with `Log4NetAdapter` **Error handling:** - Throws `ArgumentException` when `logFilePath` is null/empty - Throws `IOException` when path is invalid or permissions denied --- ### Custom Logger Injection ``` // Replace global logger public class CustomLogger : IAppLogger { public void Debug(string message) => /* custom logic */; public void Info(string message) => /* custom logic */; public void Warn(string message) => /* custom logic */; public void Error(string message, Exception ex = null) => /* custom logic */; } // Option 1: Replace global logger Log.Current = new CustomLogger(); // Option 2: Inject into factory var factory = new ConverterFactory(null, new CustomLogger()); ``` --- ## Testing ### Test Logger Pattern ``` public class TestLogger : IAppLogger { public List DebugMessages { get; } = new List(); public List InfoMessages { get; } = new List(); public List WarnMessages { get; } = new List(); public List<(string Message, Exception Exception)> ErrorMessages { get; } = new List<(string, Exception)>(); public void Debug(string message) => DebugMessages.Add(message); public void Info(string message) => InfoMessages.Add(message); public void Warn(string message) => WarnMessages.Add(message); public void Error(string message, Exception ex = null) => ErrorMessages.Add((message, ex)); } ``` **Unit Test Example** ``` [Fact] public void ConverterFactory_LogsRegistrations() { // Arrange var testLogger = new TestLogger(); var factory = new ConverterFactory(registrations: null, logger: testLogger); // Act var converter = factory.Create("GeoJson"); // Assert Assert.Contains("ConverterFactory initialized", testLogger.InfoMessages[0]); Assert.Contains("GeoJson", testLogger.DebugMessages.First(m => m.Contains("registered"))); } ``` --- ## Troubleshooting ### No log file created **Symptoms:** - `Log.SetFile(path)` called but no file appears **Solutions:** 1. **Check permissions**: Ensure process can write to log directory ``` // SetFile will throw IOException if write fails try { Log.SetFile("logs/app.log"); } catch (IOException ex) { Console.Error.WriteLine(ex.Message); } ``` 2. **Verify path**: Absolute paths recommended ``` var fullPath = Path.GetFullPath("logs/app.log"); Log.SetFile(fullPath); ``` 3. **Check `Log.Enable()` called**: SetFile configures but doesn't activate ``` Log.SetFile("logs/app.log"); Log.Enable(); // Required to activate ``` --- ### Empty or missing log entries **Symptoms:** - Log file created but no content or missing expected messages **Solutions:** 1. **Verify log level**: Ensure messages use appropriate level ``` // DEBUG messages may not appear if level filtered Log.Debug("This may be filtered"); Log.Info("This should appear"); ``` 2. **Check logger is enabled**: ``` if (Log.Current is NullLogger) { Console.WriteLine("Logging is disabled!"); } ``` 3. **Force flush** (for debugging): ``` // log4net auto-flushes but you can force: log4net.LogManager.Shutdown(); ``` --- ### Unicode characters show as `?` **Symptoms:** - Emoji or special characters render as `?` in log files **Solution:** ``` // Ensure UTF-8 encoding (already configured in Log.SetFile) // For custom layouts, explicitly set: var layout = new PatternLayout { ConversionPattern = "..." }; layout.ActivateOptions(); var appender = new RollingFileAppender { Encoding = System.Text.Encoding.UTF8, // Explicit UTF-8 Layout = layout, // ... }; ``` --- ### Performance concerns **Symptoms:** - Excessive logging slows application **Solutions:** 1. **Use appropriate levels**: Avoid DEBUG in hot paths ``` // ❌ Bad: logs every iteration foreach (var feature in features) Log.Debug($"Processing {feature.Id}"); // ✅ Good: summary after batch Log.Info($"Processed {features.Count} features"); ``` 2. **Disable logging**: Use `NullLogger` in production if not needed ``` Log.Disable(); // Zero overhead ``` 3. **Async appenders**: For high-throughput scenarios ``` // Wrap appender in AsyncAppender (log4net) var asyncAppender = new AsyncAppender(); asyncAppender.AddAppender(rollingFileAppender); ``` --- ### Test logger not capturing messages **Symptoms:** - `TestLogger` lists remain empty **Solutions:** 1. **Check logger assignment timing**: ``` // ❌ Bad: set after factory creation var factory = new ConverterFactory(); Log.Current = new TestLogger(); // Too late! // ✅ Good: set before or inject var testLogger = new TestLogger(); var factory = new ConverterFactory(null, testLogger); ``` 2. **Verify logger is used**: ``` var testLogger = new TestLogger(); Log.Current = testLogger; Log.Info("Test"); Assert.Single(testLogger.InfoMessages); // Should pass ``` --- ## Best Practices 1. **Configure once**: Call `Log.SetFile()` at application startup 2. **Best-effort**: Handle logging failures gracefully 3. **Appropriate levels**: - `DEBUG`: Detailed flow, variable values - `INFO`: Milestones, start/complete - `WARN`: Recoverable issues - `ERROR`: Failures requiring attention 4. **Structured helpers**: Use `LogHelper` for section headers 5. **Testing**: Inject `TestLogger` for assertions 6. **No control flow**: Never use logging for application logic --- ## API Reference ### `IAppLogger` | Method | Description | |--------|-------------| | `Debug(string)` | Verbose diagnostics | | `Info(string)` | Lifecycle events | | `Warn(string)` | Non-fatal issues | | `Error(string, Exception?)` | Failures | --- ### `Log` (Static) | Member | Description | |--------|-------------| | `Current` | Active logger (get/set) | | `SetFile` | Configure file logging | | `Enable()` | Activate logging | | `Disable()` | Deactivate (use NullLogger) | | `Debug/Info/Warn/Error(...)` | Forwarding methods | --- ### `LogHelper` (Static) | Method | Parameters | Description | |--------|------------|-------------| | `LogWithUnderline` | `logger, message, char='='` | Underline matching text length | | `LogWithSeparator` | `logger, message, char='-', length=80` | Fixed separator | | `LogWithBox` | `logger, message, char='='` | Bordered box | | `LogBlankLine` | `logger` | Empty line | --- ## Dependencies - **log4net** 2.0.17 (optional, required for file logging) - **.NET Standard 2.0** (library) --- ## See Also - [Main README](../../README.md) — Project overview - [ConverterFactory](../Factories/README.md) — Converter registry - [IConverter](../Converters/README.md) — Converter interface # verify changes git status git diff -- GitConverter.Lib/Logging/README.md # commit git add GitConverter.Lib/Logging/README.md git commit -m "docs(logging): tidy and expand GitConverter.Lib.Logging README" git push