├── .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 |
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 |
--------------------------------------------------------------------------------