diff --git a/EonaCat.Logger.sln b/EonaCat.Logger.sln new file mode 100644 index 0000000..e4aa6d2 --- /dev/null +++ b/EonaCat.Logger.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32811.315 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EonaCat.Logger", "EonaCat.Logger\EonaCat.Logger.csproj", "{DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B01183F3-D85E-45FB-9749-DA281F465A0F} + EndGlobalSection +EndGlobal diff --git a/EonaCat.Logger/Constants.cs b/EonaCat.Logger/Constants.cs new file mode 100644 index 0000000..e6f01b3 --- /dev/null +++ b/EonaCat.Logger/Constants.cs @@ -0,0 +1,13 @@ +namespace EonaCat.Logger +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public static class Constants + { + public static class DateTimeFormats + { + public static string LOGGING { get; } = "yyyy-MM-dd HH:mm:ss"; + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/DllInfo.cs b/EonaCat.Logger/DllInfo.cs new file mode 100644 index 0000000..5c54e2f --- /dev/null +++ b/EonaCat.Logger/DllInfo.cs @@ -0,0 +1,26 @@ +namespace EonaCat.Logger.Helpers +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public static class DllInfo + { + public const string NAME = "EonaCatLogger"; + public const string VERSION = "1.0.0"; + + static DllInfo() + { + bool isDebug = false; +#if DEBUG + isDebug = true; +#endif + VERSION_NAME = isDebug ? "DEBUG" : "RELEASE"; + } + + internal static string VERSION_NAME { get; } + + public static string ApplicationName { get; internal set; } = "EonaCatLogger"; + + public static ELogType LogLevel { get; internal set; } = ELogType.INFO; + } +} \ No newline at end of file diff --git a/EonaCat.Logger/Enums.cs b/EonaCat.Logger/Enums.cs new file mode 100644 index 0000000..b176f3a --- /dev/null +++ b/EonaCat.Logger/Enums.cs @@ -0,0 +1,17 @@ +namespace EonaCat.Logger +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public enum ELogType + { + NONE = 0, + INFO = 1, + WARNING = 2, + ERROR = 3, + TRAFFIC = 4, + DEBUG = 5, + CRITICAL = 6, + TRACE = 7 + } +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj new file mode 100644 index 0000000..7c9e819 --- /dev/null +++ b/EonaCat.Logger/EonaCat.Logger.csproj @@ -0,0 +1,64 @@ + + + + + netstandard2.0; + netstandard2.1; + net5.0; + net6.0; + + icon.ico + 1.0.0 + EonaCat (Jeroen Saey) + true + EonaCat (Jeroen Saey) + icon.png + https://www.nuget.org/packages/EonaCat.Logger/ + EonaCat.Logger is a logging library created for .NET Standard. + Public release version + EonaCat (Jeroen Saey) + EonaCat, Logger, .NET Standard, EonaCatLogger, Log, Writer Jeroen, Saey + + README.md + True + LICENSE + True + + + + + + True + \ + + + True + \ + + + True + + + + + + + + + + + + + True + \ + + + True + \ + + + True + \ + + + diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/FileLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/FileLoggerFactoryExtensions.cs new file mode 100644 index 0000000..b6ca567 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/FileLoggerFactoryExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; + +namespace EonaCat.Logger.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Extensions for adding the to the + /// + public static class FileLoggerFactoryExtensions + { + /// + /// Adds a file logger named 'File' to the factory. + /// + /// The to use. + public static ILoggingBuilder AddFile(this ILoggingBuilder builder) + { + builder.Services.AddSingleton(); + return builder; + } + + /// + /// Adds a file logger named 'File' to the factory. + /// + /// The to use. + /// Sets the filename prefix to use for log files + public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string filenamePrefix) + { + builder.AddFile(options => options.FileNamePrefix = filenamePrefix); + return builder; + } + + /// + /// Adds a file logger named 'File' to the factory. + /// + /// The to use. + /// Configure an instance of the to set logging options + public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + builder.AddFile(); + builder.Services.Configure(configure); + + return builder; + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs new file mode 100644 index 0000000..1321b56 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs @@ -0,0 +1,89 @@ +using EonaCat.Logger.Internal; +using System; +using System.IO; + +namespace EonaCat.Logger +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Options for file logging. + /// + public class FileLoggerOptions : BatchingLoggerOptions + { + private int _fileSizeLimit = 200 * 1024 * 1024; + private int _retainedFileCountLimit = 50; + private int _maxRolloverFiles = 10; + + public static string DefaultPath => AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; + + /// + /// Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit. + /// Once the log is full, no more messages will be appended. + /// Defaults to 200MB. + /// + public int FileSizeLimit + { + get => _fileSizeLimit; + + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileSizeLimit)} must be positive."); + } + _fileSizeLimit = value; + } + } + + /// + /// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit. + /// Defaults to 50. + /// + public int RetainedFileCountLimit + { + get => _retainedFileCountLimit; + + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive."); + } + _retainedFileCountLimit = value; + } + } + + /// + /// Gets or sets a strictly positive value representing the maximum retained file rollovers or null for no limit. + /// Defaults to 10. + /// + public int MaxRolloverFiles + { + get => _maxRolloverFiles; + + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(MaxRolloverFiles)} must be positive."); + } + _maxRolloverFiles = value; + } + } + + /// + /// Gets or sets the filename prefix to use for log files. + /// Defaults to EonaCat_. + /// + public string FileNamePrefix { get; set; } = "EonaCat"; + + /// + /// The directory in which log files will be written, relative to the app process. + /// Default to executablePath\logs + /// + /// + public string LogDirectory { get; set; } = Path.Combine(DefaultPath, "logs"); + } +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs new file mode 100644 index 0000000..43a54b1 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs @@ -0,0 +1,138 @@ +using EonaCat.Logger.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.Logger +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// An that writes logs to a file + /// + [ProviderAlias("File")] + public class FileLoggerProvider : BatchingLoggerProvider + { + private readonly string _path; + private readonly string _fileNamePrefix; + private readonly int _maxFileSize; + private readonly int _maxRetainedFiles; + private readonly int _maxRolloverFiles; + private int _rollOverCount = 0; + + public string LogFile { get; private set; } + + /// + /// Creates an instance of the + /// + /// The options object controlling the logger + public FileLoggerProvider(IOptions options) : base(options) + { + FileLoggerOptions loggerOptions = options.Value; + _path = loggerOptions.LogDirectory; + _fileNamePrefix = loggerOptions.FileNamePrefix; + _maxFileSize = loggerOptions.FileSizeLimit; + _maxRetainedFiles = loggerOptions.RetainedFileCountLimit; + _maxRolloverFiles = loggerOptions.MaxRolloverFiles; + } + + /// + protected override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken) + { + Directory.CreateDirectory(_path); + + foreach (IGrouping<(int Year, int Month, int Day), LogMessage> group in messages.GroupBy(GetGrouping)) + { + LogFile = GetFullName(group.Key); + FileInfo fileInfo = new FileInfo(LogFile); + if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize) + { + if (_maxRolloverFiles > 0 && _rollOverCount >= 0) + { + if (_rollOverCount < _maxRolloverFiles) + { + fileInfo.CopyTo(LogFile.Replace(".log", $"_{++_rollOverCount}.log")); + File.WriteAllText(LogFile, string.Empty); + } + else + { + bool areFilesDeleted = false; + for (int i = 0; i < _rollOverCount; i++) + { + File.Delete(LogFile.Replace(".log", $"_{i}.log")); + areFilesDeleted = true; + } + + if (areFilesDeleted) + { + File.Move(LogFile.Replace(".log", $"_{_rollOverCount}.log"), LogFile.Replace(".log", $"_1.log")); + _rollOverCount = 0; + } + } + } + } + + using (StreamWriter streamWriter = File.AppendText(LogFile)) + { + foreach (LogMessage item in group) + { + await streamWriter.WriteAsync(item.Message); + } + } + } + + DeleteOldLogFiles(); + } + + private string GetFullName((int Year, int Month, int Day) group) + { + bool hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix); + if (hasPrefix) + { + return Path.Combine(_path, $"{_fileNamePrefix}_{group.Year:0000}{group.Month:00}{group.Day:00}.log"); + } + else + { + return Path.Combine(_path, $"{group.Year:0000}{group.Month:00}{group.Day:00}.log"); + } + } + + private (int Year, int Month, int Day) GetGrouping(LogMessage message) + { + return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day); + } + + /// + /// Deletes old log files, keeping a number of files defined by + /// + protected void DeleteOldLogFiles() + { + if (_maxRetainedFiles > 0) + { + bool hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix); + IEnumerable files = null; + + if (hasPrefix) + { + files = new DirectoryInfo(_path).GetFiles(_fileNamePrefix + "*"); + } + else + { + files = new DirectoryInfo(_path).GetFiles("*"); + } + + files = files.OrderByDescending(file => file.Name).Skip(_maxRetainedFiles); + + foreach (FileInfo item in files) + { + item.Delete(); + } + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs new file mode 100644 index 0000000..72f8238 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Text; + +namespace EonaCat.Logger.Internal +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class BatchingLogger : ILogger + { + private readonly BatchingLoggerProvider _provider; + private readonly string _category; + + public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName) + { + _provider = loggerProvider; + _category = categoryName; + } + + public IDisposable BeginScope(TState state) + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + if (logLevel == LogLevel.None) + { + return false; + } + return true; + } + + public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + StringBuilder builder = new StringBuilder(); + builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")); + builder.Append(" ["); + builder.Append(logLevel.ToString()); + builder.Append("] "); + builder.Append(_category); + builder.Append(": "); + builder.AppendLine(formatter(state, exception)); + + if (exception != null) + { + builder.AppendLine(exception.ToString()); + } + + _provider.AddMessage(timestamp, builder.ToString()); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter); + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerOptions.cs new file mode 100644 index 0000000..cb320aa --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerOptions.cs @@ -0,0 +1,64 @@ +using System; + +namespace EonaCat.Logger.Internal +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class BatchingLoggerOptions + { + private int _batchSize = 32; + private int _backgroundQueueSize; + private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the period after which logs will be flushed to the store. + /// + public TimeSpan FlushPeriod + { + get => _flushPeriod; + + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FlushPeriod)} must be positive."); + } + _flushPeriod = value; + } + } + + /// + /// Gets or sets the maximum size of the background log message queue or less than 1 for no limit. + /// After maximum queue size is reached log event sink would start blocking. + /// Defaults to 0. + /// + public int BackgroundQueueSize + { + get => _backgroundQueueSize; + + set + { + _backgroundQueueSize = value; + } + } + + /// + /// Gets or sets a maximum number of events to include in a single batch or less than 1 for no limit. + /// + public int BatchSize + { + get => _batchSize; + + set + { + _batchSize = value; + } + } + + /// + /// Gets or sets value indicating if logger accepts and queues writes. + /// + public bool IsEnabled { get; set; } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs new file mode 100644 index 0000000..6eb1561 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs @@ -0,0 +1,138 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.Logger.Internal +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public abstract class BatchingLoggerProvider : ILoggerProvider + { + private readonly List _currentBatch = new List(); + private readonly TimeSpan _interval; + private readonly int _queueSize; + private readonly int _batchSize; + + private BlockingCollection _messageQueue; + private Task _outputTask; + private CancellationTokenSource _cancellationTokenSource; + + protected BatchingLoggerProvider(IOptions options) + { + BatchingLoggerOptions loggerOptions = options.Value; + + if (loggerOptions.BatchSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(loggerOptions.BatchSize), $"{nameof(loggerOptions.BatchSize)} must be a positive number."); + } + + if (loggerOptions.FlushPeriod <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero."); + } + + _interval = loggerOptions.FlushPeriod; + _batchSize = loggerOptions.BatchSize; + _queueSize = loggerOptions.BackgroundQueueSize; + + Start(); + } + + protected abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token); + + private async Task ProcessLogQueue(object state) + { + while (!_cancellationTokenSource.IsCancellationRequested) + { + int limit = _batchSize <= 0 ? int.MaxValue : _batchSize; + + while (limit > 0 && _messageQueue.TryTake(out LogMessage message)) + { + _currentBatch.Add(message); + limit--; + } + + if (_currentBatch.Count > 0) + { + try + { + await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token); + } + catch + { + // ignored + } + + _currentBatch.Clear(); + } + + await IntervalAsync(_interval, _cancellationTokenSource.Token); + } + } + + protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) + { + return Task.Delay(interval, cancellationToken); + } + + internal void AddMessage(DateTimeOffset timestamp, string message) + { + if (!_messageQueue.IsAddingCompleted) + { + try + { + _messageQueue.Add(new LogMessage { Message = message, Timestamp = timestamp }, _cancellationTokenSource.Token); + } + catch + { + //cancellation token canceled or CompleteAdding called + } + } + } + + private void Start() + { + _messageQueue = _queueSize == 0 ? + new BlockingCollection(new ConcurrentQueue()) : + new BlockingCollection(new ConcurrentQueue(), _queueSize); + + _cancellationTokenSource = new CancellationTokenSource(); + _outputTask = Task.Factory.StartNew( + ProcessLogQueue, + null, + TaskCreationOptions.LongRunning); + } + + private void Stop() + { + _cancellationTokenSource.Cancel(); + _messageQueue.CompleteAdding(); + + try + { + _outputTask.Wait(_interval); + } + catch (TaskCanceledException) + { + } + catch (AggregateException exception) when (exception.InnerExceptions.Count == 1 && exception.InnerExceptions[0] is TaskCanceledException) + { + } + } + + public void Dispose() + { + Stop(); + } + + public ILogger CreateLogger(string categoryName) + { + return new BatchingLogger(this, categoryName); + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/LogMessage.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/LogMessage.cs new file mode 100644 index 0000000..378dd23 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/LogMessage.cs @@ -0,0 +1,13 @@ +using System; + +namespace EonaCat.Logger.Internal +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public struct LogMessage + { + public DateTimeOffset Timestamp { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/Models/EonaCatLogMessage.cs b/EonaCat.Logger/EonaCatCoreLogger/Models/EonaCatLogMessage.cs new file mode 100644 index 0000000..216c21a --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Models/EonaCatLogMessage.cs @@ -0,0 +1,21 @@ +using EonaCat.Logger; +using EonaCat.Logger.Helpers; +using System; + +namespace EonaCatLogger.EonaCatCoreLogger.Models +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class EonaCatLogMessage + { + public DateTime DateTime { get; internal set; } + public string Message { get; internal set; } + public ELogType LogType { get; internal set; } + + public override string ToString() + { + return $"[{EnumHelper.ToString(LogType)}] [{DateTime.ToString(Constants.DateTimeFormats.LOGGING)}] {Message}"; + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/Extensions/OffsetStream.cs b/EonaCat.Logger/Extensions/OffsetStream.cs new file mode 100644 index 0000000..791e285 --- /dev/null +++ b/EonaCat.Logger/Extensions/OffsetStream.cs @@ -0,0 +1,261 @@ +using System; +using System.IO; + +namespace EonaCat.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class OffsetStream : Stream + { + private const int BUFFERSIZE = 4096; + + public OffsetStream(Stream stream, long offset = 0, long length = 0, bool readOnly = false, bool ownStream = false) + { + if (stream.CanSeek) + { + if (offset > stream.Length) + { + throw new EndOfStreamException(); + } + + BaseStreamOffset = offset; + + if (length > stream.Length - offset) + { + throw new EndOfStreamException(); + } + + if (length == 0) + { + Length1 = stream.Length - offset; + } + else + { + Length1 = length; + } + } + else + { + BaseStreamOffset = 0; + Length1 = length; + } + + BaseStream = stream; + ReadOnly = readOnly; + OwnStream = ownStream; + } + + protected override void Dispose(bool disposing) + { + if (Disposed) + { + return; + } + + if (disposing) + { + if (OwnStream & (BaseStream != null)) + { + BaseStream.Dispose(); + } + } + + Disposed = true; + + base.Dispose(disposing); + } + + public override bool CanRead => BaseStream.CanRead; + + public override bool CanSeek => BaseStream.CanSeek; + + public override bool CanWrite => BaseStream.CanWrite && !ReadOnly; + + public override long Length => Length1; + + public override long Position + { + get => Position1; + + set + { + if (value > Length1) + { + throw new EndOfStreamException(); + } + + if (!BaseStream.CanSeek) + { + throw new NotSupportedException("Cannot seek stream."); + } + + Position1 = value; + } + } + + public override void Flush() + { + if (ReadOnly) + { + throw new IOException("OffsetStream is read only."); + } + + BaseStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count < 1) + { + throw new ArgumentOutOfRangeException("Count cannot be less than 1."); + } + + if (Position1 >= Length1) + { + return 0; + } + + if (count > Length1 - Position1) + { + count = Convert.ToInt32(Length1 - Position1); + } + + if (BaseStream.CanSeek) + { + BaseStream.Position = BaseStreamOffset + Position1; + } + + int bytesRead = BaseStream.Read(buffer, offset, count); + Position1 += bytesRead; + + return bytesRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (!BaseStream.CanSeek) + { + throw new IOException("Stream is not seekable."); + } + + long pos; + + switch (origin) + { + case SeekOrigin.Begin: + pos = offset; + break; + + case SeekOrigin.Current: + pos = Position1 + offset; + break; + + case SeekOrigin.End: + pos = Length1 + offset; + break; + + default: + pos = 0; + break; + } + + if (pos < 0 || pos >= Length1) + { + throw new EndOfStreamException("OffsetStream reached begining/end of stream."); + } + + Position1 = pos; + + return pos; + } + + public override void SetLength(long value) + { + if (ReadOnly) + { + throw new IOException("OffsetStream is read only."); + } + + BaseStream.SetLength(BaseStreamOffset + value); + Length1 = value; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (ReadOnly) + { + throw new IOException("OffsetStream is read only."); + } + + if (count < 1) + { + return; + } + + long pos = Position1 + count; + + if (pos > Length1) + { + throw new EndOfStreamException("OffsetStream reached end of stream."); + } + + if (BaseStream.CanSeek) + { + BaseStream.Position = BaseStreamOffset + Position1; + } + + BaseStream.Write(buffer, offset, count); + Position1 = pos; + } + + public long BaseStreamOffset { get; private set; } + + public Stream BaseStream { get; } + public long Length1 { get; set; } + public long Position1 { get; set; } + + public bool ReadOnly { get; } + + public bool Disposed { get; set; } + + public bool OwnStream { get; } + + public void Reset(long offset, long length, long position) + { + BaseStreamOffset = offset; + Length1 = length; + Position1 = position; + } + + public void WriteTo(Stream stream) + { + WriteTo(stream, BUFFERSIZE); + } + + public void WriteTo(Stream stream, int bufferSize) + { + if (!BaseStream.CanSeek) + { + throw new IOException("Stream is not seekable."); + } + + if (Length1 < bufferSize) + { + bufferSize = Convert.ToInt32(Length1); + } + + long previousPosition = Position1; + Position1 = 0; + + try + { + CopyTo(stream, bufferSize); + } + finally + { + Position1 = previousPosition; + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/Helpers/EnumHelper.cs b/EonaCat.Logger/Helpers/EnumHelper.cs new file mode 100644 index 0000000..38ad567 --- /dev/null +++ b/EonaCat.Logger/Helpers/EnumHelper.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.Logger.Helpers +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal static class EnumHelper + where T : struct + { + static EnumHelper() + { + string[] names = Enum.GetNames(typeof(T)); + T[] values = (T[])Enum.GetValues(typeof(T)); + + Names = new Dictionary(names.Length); + Values = new Dictionary(names.Length * 2); + + for (int i = 0; i < names.Length; i++) + { + Names[values[i]] = names[i]; + Values[names[i]] = values[i]; + Values[names[i].ToLower()] = values[i]; + } + } + + public static Dictionary Names { get; } + + public static Dictionary Values { get; private set; } + + public static string ToString(T value) + { + return Names.TryGetValue(value, out string result) ? result : Convert.ToInt64(value).ToString(); + } + + public static bool TryParse(string input, bool ignoreCase, out T value) + { + if (string.IsNullOrEmpty(input)) + { + value = default; + return false; + } + + return Values.TryGetValue(ignoreCase ? input.ToLower() : input, out value); + } + + internal static T Parse(string input, bool ignoreCase, T defaultValue) + { + return TryParse(input, ignoreCase, out T result) ? result : defaultValue; + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/Managers/LogManager.cs b/EonaCat.Logger/Managers/LogManager.cs new file mode 100644 index 0000000..caf95af --- /dev/null +++ b/EonaCat.Logger/Managers/LogManager.cs @@ -0,0 +1,276 @@ +using EonaCat.Extensions; +using EonaCat.Logger.Extensions; +using EonaCat.Logger.Helpers; +using EonaCatLogger.EonaCatCoreLogger.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.Logger.Managers +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class LogManager : IDisposable + { + private readonly object _batton = new object(); + private DateTime _logDate; + + private ILoggerProvider LoggerProvider { get; set; } + private ILoggerFactory LoggerFactory { get; set; } + private ILogger Logger { get; set; } + public string CurrentLogFile => LoggerProvider != null ? ((FileLoggerProvider)LoggerProvider).LogFile : string.Empty; + + public bool IsRunning { get; private set; } + + public StreamWriter Output { get; private set; } + public string LogFolderPath { get; set; } = "logs"; + public FileLoggerOptions FileLoggerOptions { get; private set; } + public int FileSizeLimit { get; set; } = 200 * 1024 * 1024; + public string CategoryName { get; set; } + private LogManager Instance { get; set; } + + private bool _disposed; + + protected virtual void Dispose(bool disposing) + { + lock (this) + { + if (_disposed) + { + return; + } + + if (disposing) + { + StopLogging(); + } + + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } + + public static async Task DownloadLogAsync(HttpResponse response, string logFile, long limit) + { + using (FileStream fileStream = new FileStream(logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + response.ContentType = "text/plain"; + response.Headers.Add("Content-Disposition", "attachment;filename=" + Path.GetFileName(logFile)); + + if (limit > fileStream.Length) + { + limit = fileStream.Length; + } + + using (OffsetStream offsetStream = new OffsetStream(fileStream, 0, limit)) + { + using (Stream stream = response.Body) + { + offsetStream.CopyTo(stream); + + if (fileStream.Length > limit) + { + byte[] buffer = Encoding.UTF8.GetBytes("####___EonaCatLogger_TRUNCATED___####"); + await stream.WriteAsync(buffer, 0, buffer.Length); + } + } + } + } + } + + private void StartNewLog() + { + DateTime now = DateTime.Now; + + if (IsRunning && now.Date > _logDate.Date) + { + StopLogging(); + } + IsRunning = true; + + IServiceCollection serviceCollection = new ServiceCollection(); + CreateDefaultFileLoggerOptions(); + + serviceCollection.AddLogging(builder => builder.AddFile(configuration => + { + configuration.BackgroundQueueSize = FileLoggerOptions.BackgroundQueueSize; + configuration.BatchSize = FileLoggerOptions.BatchSize; + configuration.FileNamePrefix = FileLoggerOptions.FileNamePrefix; + configuration.FileSizeLimit = FileLoggerOptions.FileSizeLimit; + configuration.FlushPeriod = FileLoggerOptions.FlushPeriod; + configuration.IsEnabled = FileLoggerOptions.IsEnabled; + configuration.LogDirectory = FileLoggerOptions.LogDirectory; + configuration.RetainedFileCountLimit = FileLoggerOptions.RetainedFileCountLimit; + })); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + LoggerProvider = serviceProvider.GetService(); + LoggerFactory = serviceProvider.GetService(); + CategoryName = CategoryName ?? string.Empty; + Logger = LoggerFactory.CreateLogger(CategoryName); + + if (!Directory.Exists(FileLoggerOptions.LogDirectory)) + { + Directory.CreateDirectory(FileLoggerOptions.LogDirectory); + } + + _logDate = now; + + Write(now, "EonaCatLogger started."); + } + + private void CreateDefaultFileLoggerOptions() + { + if (FileLoggerOptions == null) + { + FileLoggerOptions = new FileLoggerOptions + { + LogDirectory = LogFolderPath, + FileSizeLimit = FileSizeLimit + }; + } + } + + private void WriteToFile(EonaCatLogMessage EonaCatLogMessage) + { + if (EonaCatLogMessage.LogType == ELogType.CRITICAL) + { + Instance.Logger.LogCritical(EonaCatLogMessage.Message); + } + else if (EonaCatLogMessage.LogType == ELogType.DEBUG) + { + Instance.Logger.LogDebug(EonaCatLogMessage.Message); + } + else if (EonaCatLogMessage.LogType == ELogType.ERROR) + { + Instance.Logger.LogError(EonaCatLogMessage.Message); + } + else if (EonaCatLogMessage.LogType == ELogType.INFO) + { + Instance.Logger.LogInformation(EonaCatLogMessage.Message); + } + else if (EonaCatLogMessage.LogType == ELogType.TRACE) + { + Instance.Logger.LogTrace(EonaCatLogMessage.Message); + } + else if (EonaCatLogMessage.LogType == ELogType.TRAFFIC) + { + Instance.Logger.LogTrace($"[TRAFFIC] {EonaCatLogMessage.Message}"); + } + else if (EonaCatLogMessage.LogType == ELogType.WARNING) + { + Instance.Logger.LogWarning(EonaCatLogMessage.Message); + } + } + + private void Write(DateTime dateTime, string message, ELogType logType = ELogType.INFO) + { + var EonaCatMessage = new EonaCatLogMessage { DateTime = dateTime, Message = message, LogType = logType }; + WriteToFile(EonaCatMessage); + } + + private LogManager() + { + // Do nothing + } + + public LogManager(FileLoggerOptions fileLoggerOptions) + { + FileLoggerOptions = fileLoggerOptions; + SetupLogManager(); + } + + private void SetupLogManager() + { + AppDomain.CurrentDomain.ProcessExit += ProcessExit; + + Instance = this; + + _logDate = DateTime.Now; + + StartNewLog(); + } + + private void ProcessExit(object sender, EventArgs e) + { + Instance?.StopLogging(); + Thread.Sleep(1000); + } + + private void StopLogging() + { + Write(DateTime.Now, "EonaCatLogger stopped."); + IsRunning = false; + } + + public LogManager(string logFolder = null, bool defaultPrefix = true) + { + CreateDefaultFileLoggerOptions(); + if (logFolder != null) + { + FileLoggerOptions.LogDirectory = logFolder; + } + + if (defaultPrefix) + { + FileLoggerOptions.FileNamePrefix = "EonaCat"; + } + else + { + FileLoggerOptions.FileNamePrefix = string.Empty; + } + + SetupLogManager(); + } + + public void Write(Exception exception) + { + Write(exception.ToString()); + } + + public void Write(string message, ELogType logType = ELogType.INFO, ELogType? logLevel = null) + { + lock (_batton) + { + if (logLevel == null) + { + logLevel = DllInfo.LogLevel; + } + + if (logType == ELogType.CRITICAL || logLevel.GetValueOrDefault() <= logType) + { + DateTime now = DateTime.Now; + + if (!IsRunning) + { + StartNewLog(); + } + + Write(now, $"{DllInfo.ApplicationName}: {message}", logType); + } + } + } + + public void DeleteCurrentLogFile() + { + lock (this) + { + if (!string.IsNullOrWhiteSpace(CurrentLogFile)) + { + File.Delete(CurrentLogFile); + } + StartNewLog(); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/icon.ico b/EonaCat.Logger/icon.ico new file mode 100644 index 0000000..406f265 Binary files /dev/null and b/EonaCat.Logger/icon.ico differ diff --git a/EonaCat.Logger/icon.png b/EonaCat.Logger/icon.png new file mode 100644 index 0000000..0595b89 Binary files /dev/null and b/EonaCat.Logger/icon.png differ diff --git a/README.md b/README.md index abce9e7..4422e84 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # EonaCat.Logger -EonaCat Logger \ No newline at end of file +EonaCat Logger + +Log application information to log files \ No newline at end of file