├── .gitignore ├── Logo ├── logo-200x200.png └── logo-original.png ├── L.Tests ├── L.Tests.csproj └── Tests.cs ├── L ├── L.csproj ├── FolderCleaner.cs ├── OpenStreams.cs └── L.cs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | bin/ 3 | obj/ -------------------------------------------------------------------------------- /Logo/logo-200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wulf/net-L/master/Logo/logo-200x200.png -------------------------------------------------------------------------------- /Logo/logo-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wulf/net-L/master/Logo/logo-original.png -------------------------------------------------------------------------------- /L.Tests/L.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /L/L.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | L 5 | 5.0.0.0 6 | Talles L <talleslasmar@gmail.com> 7 | Logging as simple as it can be. 8 | 5.0.0.0 9 | true 10 | log;logging;zero;configuration 11 | 5.0.0 12 | https://raw.githubusercontent.com/tallesl/net-L/master/Logo/logo-200x200.png 13 | https://github.com/tallesl/net-L 14 | netstandard2.0 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /L/FolderCleaner.cs: -------------------------------------------------------------------------------- 1 | namespace LLibrary 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading; 7 | 8 | internal sealed class FolderCleaner : IDisposable 9 | { 10 | private readonly string _directory; 11 | 12 | private readonly OpenStreams _openStreams; 13 | 14 | private readonly TimeSpan _threshold; 15 | 16 | private readonly object _cleanLock; 17 | 18 | private readonly Timer _timer; 19 | 20 | internal FolderCleaner(string path, OpenStreams streams, TimeSpan threshold, TimeSpan interval) 21 | { 22 | _directory = path; 23 | _openStreams = streams; 24 | _threshold = threshold; 25 | _cleanLock = new object(); 26 | _timer = new Timer(Clean, null, TimeSpan.Zero, interval); 27 | } 28 | 29 | public void Dispose() 30 | { 31 | lock (_cleanLock) 32 | { 33 | _timer.Dispose(); 34 | } 35 | } 36 | 37 | private void Clean(object ignored) 38 | { 39 | lock (_cleanLock) 40 | { 41 | if (!Directory.Exists(_directory)) 42 | return; 43 | 44 | var now = DateTime.Now; 45 | var openFiles = _openStreams.Filepaths(); 46 | var files = Directory.GetFiles(_directory).Except(openFiles); 47 | 48 | foreach (var filepath in files) 49 | { 50 | var file = new FileInfo(filepath); 51 | var lifetime = now - file.CreationTime; 52 | 53 | if (lifetime >= _threshold) 54 | file.Delete(); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | logo 4 | 5 |

6 | 7 | # L 8 | 9 | [![][build-img]][build] 10 | [![][nuget-img]][nuget] 11 | 12 | Logging as simple as it can be. 13 | 14 | Most of the time I don't need a sophisticated logger and I got tired of [configuring the same thing for log4net] over 15 | and over again. 16 | 17 | [build]: https://ci.appveyor.com/project/TallesL/net-L 18 | [build-img]: https://ci.appveyor.com/api/projects/status/github/tallesl/net-L?svg=true 19 | [nuget]: https://www.nuget.org/packages/L 20 | [nuget-img]: https://badge.fury.io/nu/L.svg 21 | [configuring the same thing for log4net]: https://logging.apache.org/log4net/release/manual/configuration.html 22 | 23 | ## Usage 24 | 25 | ```cs 26 | using LLibrary; 27 | 28 | var myLogger = new L(); 29 | 30 | myLogger.Log("INFO", "Some information"); 31 | myLogger.Log("ERROR", new Exception("BOOM!")); 32 | ``` 33 | 34 | You can use built-in methods for the classical DEBUG, INFO, WARN, ERROR, FATAL labels (but there's no logging level here, they're just labels): 35 | 36 | ```cs 37 | myLogger.Info("Some information"); 38 | myLogger.Error(new Exception("BOOM!")); 39 | ``` 40 | 41 | A file named `yyyy-MM-dddd.log` will be created in a `logs` folder (located where the application is running), like `2014-12-16.log` containing: 42 | 43 | ``` 44 | 2014-12-16 19:21:45 INFO Some information. 45 | 2014-12-16 19:21:52 ERROR A System.Exception happened: BOOM! 46 | ``` 47 | 48 | ## Configuration 49 | 50 | The library works out-of-the-box without configuration, but you can configure a thing or two if you want: 51 | 52 | ```cs 53 | var myLogger = new L( 54 | // True to use UTC time rather than local time. 55 | // Defaults to false. 56 | useUtcTime: true, 57 | 58 | // If other than null it sets to delete any file in the log folder that is older than the time set. 59 | // Defaults to null. 60 | deleteOldFiles: TimeSpan.FromDays(10), 61 | 62 | // Format string to use when calling DateTime.Format. 63 | // Defaults to "yyyy-MM-dd HH:mm:ss". 64 | dateTimeFormat: "dd MMM HH:mm:ss", 65 | 66 | // Directory where to create the log files. 67 | // Defaults to null, which creates a local "logs" directory. 68 | directory: @"C:\custom-directory\my-logs\", 69 | 70 | // Labels enabled to be logged by the library, an attempt to log with a label that is not enabled is ignored (no error is raised), null or empty enables all labels. 71 | // Defaults to null. 72 | enabledLabels: "INFO", "ERROR" 73 | ); 74 | ``` 75 | 76 | ## But I want... 77 | 78 | To restrict the file size? 79 | Log to a database? 80 | What about a fancy web interface? 81 | 82 | If that is the case, this library is not for you. 83 | Consider using other library such as [log4net], [NLog], [ELMAH] or [Logary]. 84 | 85 | [log4net]: http://logging.apache.org/log4net 86 | [NLog]: http://nlog-project.org 87 | [ELMAH]: https://code.google.com/p/elmah 88 | [Logary]: http://logary.github.io -------------------------------------------------------------------------------- /L/OpenStreams.cs: -------------------------------------------------------------------------------- 1 | namespace LLibrary 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Globalization; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Threading; 10 | 11 | internal sealed class OpenStreams : IDisposable 12 | { 13 | private readonly string _directory; 14 | 15 | private readonly Dictionary _streams; 16 | 17 | private readonly Timer _timer; 18 | 19 | private readonly object _lock; 20 | 21 | internal OpenStreams(string directory) 22 | { 23 | _directory = directory; 24 | _streams = new Dictionary(); 25 | _lock = new object(); 26 | _timer = new Timer(ClosePastStreams, null, 0, (int)TimeSpan.FromHours(2).TotalMilliseconds); 27 | } 28 | 29 | public void Dispose() 30 | { 31 | _timer.Dispose(); 32 | CloseAllStreams(); 33 | } 34 | 35 | internal void Append(DateTime date, string content) 36 | { 37 | lock (_lock) 38 | { 39 | GetStream(date.Date).WriteLine(content); 40 | } 41 | } 42 | 43 | internal string[] Filepaths() => 44 | _streams.Values.Select(s => s.BaseStream).Cast().Select(s => s.Name).ToArray(); 45 | 46 | private void ClosePastStreams(object ignored) 47 | { 48 | lock (_lock) 49 | { 50 | var today = DateTime.Today; 51 | var past = _streams.Where(kvp => kvp.Key < today).ToList(); 52 | 53 | foreach (var kvp in past) 54 | { 55 | kvp.Value.Dispose(); 56 | _streams.Remove(kvp.Key); 57 | } 58 | } 59 | } 60 | 61 | private void CloseAllStreams() 62 | { 63 | lock (_lock) 64 | { 65 | foreach (var stream in _streams.Values) 66 | stream.Dispose(); 67 | 68 | _streams.Clear(); 69 | } 70 | } 71 | 72 | [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", 73 | Justification = "It's disposed on this class Dispose.")] 74 | private StreamWriter GetStream(DateTime date) 75 | { 76 | // Opening the stream if needed 77 | if (!_streams.ContainsKey(date)) 78 | { 79 | // Building stream's filepath 80 | var filename = $"{date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}.log"; 81 | var filepath = Path.Combine(_directory, filename); 82 | 83 | // Making sure the directory exists 84 | Directory.CreateDirectory(_directory); 85 | 86 | // Opening the stream 87 | var stream = new StreamWriter( 88 | File.Open(filepath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite) 89 | ); 90 | stream.AutoFlush = true; 91 | 92 | // Storing the created stream 93 | _streams[date] = stream; 94 | } 95 | 96 | return _streams[date]; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /L.Tests/Tests.cs: -------------------------------------------------------------------------------- 1 | namespace LLibrary.Tests 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | using System.Globalization; 6 | using System.IO; 7 | 8 | [TestClass] 9 | public class Tests 10 | { 11 | private string FilePath 12 | { 13 | get 14 | { 15 | return $@"logs\{DateTime.Today.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}.log"; 16 | } 17 | } 18 | 19 | private string FileContent 20 | { 21 | get 22 | { 23 | using (var file = File.Open(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) 24 | using (var reader = new StreamReader(file)) 25 | { 26 | return reader.ReadToEnd().TrimEnd(); 27 | } 28 | } 29 | } 30 | 31 | private enum Enum 32 | { 33 | Foo, 34 | Bar, 35 | } 36 | 37 | [TestCleanup] 38 | public void Cleanup() => File.Delete(FilePath); 39 | 40 | [TestMethod] 41 | public void String() 42 | { 43 | using (var logger = new L()) 44 | { 45 | logger.Info("Some information."); 46 | Assert.IsTrue(FileContent.EndsWith("INFO Some information.")); 47 | } 48 | } 49 | 50 | [TestMethod] 51 | public void NotString() 52 | { 53 | using (var logger = new L()) 54 | { 55 | logger.Error(new Exception("BOOM!")); 56 | Assert.IsTrue(FileContent.EndsWith("ERROR System.Exception: BOOM!")); 57 | } 58 | } 59 | 60 | [TestMethod] 61 | public void NoWriteLock() 62 | { 63 | using (var logger = new L()) 64 | { 65 | logger.Info("Some information."); 66 | 67 | using (var file = File.Open(FilePath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite)) 68 | using (var writer = new StreamWriter(file)) 69 | { 70 | writer.WriteLine("Do a barrel roll!"); 71 | } 72 | } 73 | } 74 | 75 | [TestMethod] 76 | public void EnumAsLabel() 77 | { 78 | using (var logger = new L()) 79 | { 80 | logger.Log(Enum.Foo, "Here's foo."); 81 | Assert.IsTrue(FileContent.EndsWith("FOO Here's foo.")); 82 | 83 | logger.Log(Enum.Bar, "And here's bar."); 84 | Assert.IsTrue(FileContent.EndsWith("BAR And here's bar.")); 85 | } 86 | } 87 | 88 | [TestMethod, ExpectedException(typeof(ObjectDisposedException))] 89 | public void DisposedException() 90 | { 91 | using (var logger = new L()) 92 | { 93 | logger.Dispose(); 94 | logger.Info("Some information."); 95 | } 96 | } 97 | 98 | [TestMethod] 99 | public void EnabledLabels() 100 | { 101 | using (var logger = new L(enabledLabels: "FOO")) 102 | { 103 | logger.Log(Enum.Foo, "Here's foo."); 104 | Assert.IsTrue(FileContent.EndsWith("FOO Here's foo.")); 105 | 106 | logger.Log(Enum.Bar, "And here's bar."); 107 | Assert.IsTrue(FileContent.EndsWith("FOO Here's foo.")); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /L/L.cs: -------------------------------------------------------------------------------- 1 | namespace LLibrary 2 | { 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Globalization; 6 | using System.IO; 7 | using System.Linq; 8 | 9 | /// 10 | /// Logs in a log file the given information. 11 | /// 12 | public sealed class L : IDisposable 13 | { 14 | private readonly bool _printToStandardOut; 15 | 16 | private readonly bool _useUtcTime; 17 | 18 | private readonly TimeSpan? _deleteOldFiles; 19 | 20 | private readonly string _dateTimeFormat; 21 | 22 | private readonly string _directory; 23 | 24 | private readonly string[] _enabledLabels; 25 | 26 | private readonly object _lock; 27 | 28 | private readonly OpenStreams _openStreams; 29 | 30 | private readonly FolderCleaner _cleaner; 31 | 32 | private int _longestLabel; 33 | 34 | private bool _disposed; 35 | 36 | /// 37 | /// Constructs the logger using the given configuration. 38 | /// 39 | /// True to use UTC time rather than local time 40 | /// 41 | /// If other than null it sets to delete any file in the log folder that is older than the specified time 42 | /// 43 | /// Format string to use when calling DateTime.Format 44 | /// 45 | /// Directory where to create the log files, null to use a local "logs" directory 46 | /// 47 | /// 48 | /// Labels enabled to be logged by the library, an attempt to log with a label that is not enabled is ignored 49 | /// (no error is raised), null or empty enables all labels 50 | /// 51 | public L( 52 | bool useUtcTime = false, TimeSpan? deleteOldFiles = null, string dateTimeFormat = "yyyy-MM-dd HH:mm:ss", 53 | string directory = null, bool printToStandardOut = false, params string[] enabledLabels) 54 | { 55 | _printToStandardOut = printToStandardOut; 56 | _useUtcTime = useUtcTime; 57 | _deleteOldFiles = deleteOldFiles; 58 | _dateTimeFormat = dateTimeFormat; 59 | _directory = directory ?? Path.Combine(AppContext.BaseDirectory, "logs"); 60 | _enabledLabels = (enabledLabels ?? new string[0]).Select(Normalize).ToArray(); 61 | _lock = new object(); 62 | _openStreams = new OpenStreams(_directory); 63 | 64 | if (_deleteOldFiles.HasValue) 65 | { 66 | var min = TimeSpan.FromSeconds(5); 67 | var max = TimeSpan.FromHours(8); 68 | 69 | var cleanUpTime = new TimeSpan(_deleteOldFiles.Value.Ticks / 5); 70 | 71 | if (cleanUpTime < min) 72 | cleanUpTime = min; 73 | 74 | if (cleanUpTime > max) 75 | cleanUpTime = max; 76 | 77 | _cleaner = new FolderCleaner(_directory, _openStreams, _deleteOldFiles.Value, cleanUpTime); 78 | } 79 | 80 | _longestLabel = _enabledLabels.Any() ? _enabledLabels.Select(l => l.Length).Max() : 5; 81 | _disposed = false; 82 | } 83 | 84 | private DateTime Now => _useUtcTime ? DateTime.UtcNow : DateTime.Now; 85 | 86 | /// 87 | /// Logs the given information. 88 | /// 89 | /// Label to use when logging 90 | /// A string with a message or an object to call ToString() on it 91 | [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", 92 | Justification = "The called function validates it.")] 93 | public void Log(Enum label, string content) => Log(label.ToString(), content); 94 | 95 | /// 96 | /// Formats the given information and logs it. 97 | /// 98 | /// Label to use when logging 99 | /// A string with a message or an object to call ToString() on it 100 | public void Log(string label, object content) 101 | { 102 | if (label == null) 103 | throw new ArgumentNullException("label"); 104 | 105 | if (content == null) 106 | throw new ArgumentNullException("content"); 107 | 108 | label = Normalize(label); 109 | 110 | if (_enabledLabels.Any() && !_enabledLabels.Contains(label)) 111 | return; 112 | 113 | _longestLabel = Math.Max(_longestLabel, label.Length); 114 | 115 | var date = Now; 116 | var formattedDate = date.ToString(_dateTimeFormat, CultureInfo.InvariantCulture); 117 | var padding = new string(' ', _longestLabel - label.Length); 118 | 119 | var line = $"{formattedDate} {label} {padding}{content}"; 120 | 121 | if (_printToStandardOut) { 122 | Console.WriteLine(line); 123 | } 124 | 125 | lock (_lock) 126 | { 127 | if (_disposed) 128 | throw new ObjectDisposedException("Cannot access a disposed object."); 129 | 130 | _openStreams.Append(date, line); 131 | } 132 | } 133 | 134 | /// 135 | /// Logs the given information with DEBUG label. 136 | /// 137 | /// A string with a message or an object to call ToString() on it 138 | public void Debug(object content) => Log("DEBUG", content); 139 | 140 | /// 141 | /// Logs the given information with INFO label. 142 | /// 143 | /// A string with a message or an object to call ToString() on it 144 | public void Info(object content) => Log("INFO", content); 145 | 146 | /// 147 | /// Logs the given information with WARN label. 148 | /// 149 | /// A string with a message or an object to call ToString() on it 150 | public void Warn(object content) => Log("WARN", content); 151 | 152 | /// 153 | /// Logs the given information with ERROR label. 154 | /// 155 | /// A string with a message or an object to call ToString() on it 156 | public void Error(object content) => Log("ERROR", content); 157 | 158 | /// 159 | /// Logs the given information with FATAL label. 160 | /// 161 | /// A string with a message or an object to call ToString() on it 162 | public void Fatal(object content) => Log("FATAL", content); 163 | 164 | /// 165 | /// Disposes the file writer and the directory cleaner used by this instance. 166 | /// 167 | public void Dispose() 168 | { 169 | lock (_lock) 170 | { 171 | if (_disposed) 172 | return; 173 | 174 | if (_openStreams != null) 175 | _openStreams.Dispose(); 176 | 177 | if (_cleaner != null) 178 | _cleaner.Dispose(); 179 | 180 | _disposed = true; 181 | } 182 | } 183 | 184 | private string Normalize(string label) => label.Trim().ToUpperInvariant(); 185 | } 186 | } 187 | --------------------------------------------------------------------------------