From 10e96d1913d1b4fca52579726265f7ddc4ac4e8f Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 6 Apr 2017 19:29:41 +0200 Subject: [PATCH] File Channel --- .dockerignore | 4 +- .gitignore | 4 +- .../Log/ApplicationInfoLogAppender.cs | 16 +++- src/Squidex.Infrastructure/Log/FileChannel.cs | 34 +++++++ .../Log/Internal/FileLogChannel.cs | 95 +++++++++++++++++++ .../Log/TimestampLogAppender.cs | 6 +- .../Config/Domain/InfrastructureModule.cs | 13 ++- src/Squidex/appsettings.json | 2 +- .../Log/SemanticLogTests.cs | 16 +++- 9 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 src/Squidex.Infrastructure/Log/FileChannel.cs create mode 100644 src/Squidex.Infrastructure/Log/Internal/FileLogChannel.cs diff --git a/.dockerignore b/.dockerignore index 19ec84e3b..9b8b49c2a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,4 +19,6 @@ **/node_modules/ # Scripts (should be copied from node_modules on build) -**/wwwroot/scripts/**/*.* \ No newline at end of file +**/wwwroot/scripts/**/*.* + +**/src/Squidex/appsettings.Development.json \ No newline at end of file diff --git a/.gitignore b/.gitignore index 549a8b85d..4c398ec98 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ _test-output/ node_modules/ # Scripts (should be copied from node_modules on build) -**/wwwroot/scripts/**/*.* \ No newline at end of file +**/wwwroot/scripts/**/*.* + +/src/Squidex/appsettings.Development.json diff --git a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs b/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs index 12f73f827..f97943a44 100644 --- a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs +++ b/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs @@ -17,20 +17,26 @@ namespace Squidex.Infrastructure.Log private readonly string applicationVersion; private readonly string applicationSessionId; - public ApplicationInfoLogAppender(Assembly assembly) + public ApplicationInfoLogAppender(Type type, Guid applicationSession) + : this(type?.GetTypeInfo().Assembly, applicationSession) + { + } + + public ApplicationInfoLogAppender(Assembly assembly, Guid applicationSession) { Guard.NotNull(assembly, nameof(assembly)); applicationName = assembly.GetName().Name; applicationVersion = assembly.GetName().Version.ToString(); - applicationSessionId = Guid.NewGuid().ToString(); + applicationSessionId = applicationSession.ToString(); } public void Append(IObjectWriter writer) { - writer.WriteProperty("appName", applicationName); - writer.WriteProperty("appVersion", applicationVersion); - writer.WriteProperty("appSessionId", applicationSessionId); + writer.WriteObject("app", w => w + .WriteProperty("name", applicationName) + .WriteProperty("version", applicationVersion) + .WriteProperty("sessionId", applicationSessionId)); } } } diff --git a/src/Squidex.Infrastructure/Log/FileChannel.cs b/src/Squidex.Infrastructure/Log/FileChannel.cs new file mode 100644 index 000000000..aea8b916e --- /dev/null +++ b/src/Squidex.Infrastructure/Log/FileChannel.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// FileChannel.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.Log.Internal; + +namespace Squidex.Infrastructure.Log +{ + public sealed class FileChannel : ILogChannel, IExternalSystem + { + private readonly FileLogProcessor processor; + + public FileChannel(string path) + { + Guard.NotNullOrEmpty(path, nameof(path)); + + processor = new FileLogProcessor(path); + } + + public void Log(SemanticLogLevel logLevel, string message) + { + processor.EnqueueMessage(new LogMessageEntry { Message = message, Level = logLevel }); + } + + public void Connect() + { + processor.Connect(); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/Internal/FileLogChannel.cs b/src/Squidex.Infrastructure/Log/Internal/FileLogChannel.cs new file mode 100644 index 000000000..f9477dbd1 --- /dev/null +++ b/src/Squidex.Infrastructure/Log/Internal/FileLogChannel.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// FileLogChannel.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Log.Internal +{ + public class FileLogProcessor : IDisposable + { + private const int MaxQueuedMessages = 1024; + private const int Retries = 10; + private readonly BlockingCollection messageQueue = new BlockingCollection(MaxQueuedMessages); + private readonly Task outputTask; + private readonly string path; + + public FileLogProcessor(string path) + { + this.path = path; + + outputTask = Task.Factory.StartNew(ProcessLogQueue, this, TaskCreationOptions.LongRunning); + } + + public void Connect() + { + var fileInfo = new FileInfo(path); + + if (!fileInfo.Directory.Exists) + { + throw new ConfigurationException($"Log directory '{fileInfo.Directory.FullName}' does not exist."); + } + } + + public void EnqueueMessage(LogMessageEntry message) + { + messageQueue.Add(message); + } + + private async Task ProcessLogQueue() + { + foreach (var entry in messageQueue.GetConsumingEnumerable()) + { + for (var i = 1; i <= Retries; i++) + { + try + { + File.AppendAllText(path, entry.Message + Environment.NewLine, Encoding.UTF8); + + break; + } + catch (Exception ex) + { + await Task.Delay(i * 10); + + if (i == Retries) + { + Console.WriteLine("Failed to write to log file '{0}': {1}", path, ex); + } + } + } + } + } + + private static Task ProcessLogQueue(object state) + { + var processor = (FileLogProcessor)state; + + return processor.ProcessLogQueue(); + } + + public void Dispose() + { + messageQueue.CompleteAdding(); + + try + { + outputTask.Wait(1500); + } + catch (TaskCanceledException) + { + } + catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) + { + } + } + } +} diff --git a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs b/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs index 9eab14555..cb399e591 100644 --- a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs +++ b/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs @@ -12,14 +12,14 @@ namespace Squidex.Infrastructure.Log { public sealed class TimestampLogAppender : ILogAppender { - private readonly Func timestamp; + private readonly Func timestamp; public TimestampLogAppender() - : this(() => DateTimeOffset.UtcNow.ToUnixTimeSeconds()) + : this(() => DateTime.UtcNow) { } - public TimestampLogAppender(Func timestamp) + public TimestampLogAppender(Func timestamp) { Guard.NotNull(timestamp, nameof(timestamp)); diff --git a/src/Squidex/Config/Domain/InfrastructureModule.cs b/src/Squidex/Config/Domain/InfrastructureModule.cs index 395402274..b84720bde 100644 --- a/src/Squidex/Config/Domain/InfrastructureModule.cs +++ b/src/Squidex/Config/Domain/InfrastructureModule.cs @@ -23,7 +23,6 @@ using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Log; using Squidex.Pipeline; -using IntrospectionExtensions = System.Reflection.IntrospectionExtensions; // ReSharper disable UnusedAutoPropertyAccessor.Local @@ -53,7 +52,17 @@ namespace Squidex.Config.Domain .SingleInstance(); } - builder.Register(c => new ApplicationInfoLogAppender(IntrospectionExtensions.GetTypeInfo(typeof(InfrastructureModule)).Assembly)) + var logFile = Configuration.GetValue("squidex:logging:file"); + + if (!string.IsNullOrWhiteSpace(logFile)) + { + builder.RegisterInstance(new FileChannel(logFile)) + .As() + .As() + .SingleInstance(); + } + + builder.Register(c => new ApplicationInfoLogAppender(GetType(), Guid.NewGuid())) .As() .SingleInstance(); diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 5671c99fd..b0e5cc2af 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -4,7 +4,7 @@ "baseUrl": "http://localhost:5000" }, "logging": { - "human": true + "human": false }, "clusterer": { "type": "none", diff --git a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs b/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs index 32d03770f..ac82728e1 100644 --- a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs @@ -45,14 +45,16 @@ namespace Squidex.Infrastructure.Log [Fact] public void Should_log_timestamp() { - appenders.Add(new TimestampLogAppender(() => 1500)); + var now = DateTime.UtcNow; + + appenders.Add(new TimestampLogAppender(() => now)); Log.LogFatal(w => {}); var expected = MakeTestCall(w => w .WriteProperty("logLevel", "Fatal") - .WriteProperty("timestamp", 1500)); + .WriteProperty("timestamp", now)); Assert.Equal(expected, output); } @@ -75,15 +77,19 @@ namespace Squidex.Infrastructure.Log [Fact] public void Should_log_application_info() { - appenders.Add(new ApplicationInfoLogAppender(GetType().GetTypeInfo().Assembly)); + var sessionId = Guid.NewGuid(); + + appenders.Add(new ApplicationInfoLogAppender(GetType(), sessionId)); Log.LogFatal(m => { }); var expected = MakeTestCall(w => w .WriteProperty("logLevel", "Fatal") - .WriteProperty("applicationName", "Squidex.Infrastructure.Tests") - .WriteProperty("applicationVersion", "1.0.0.0")); + .WriteObject("app", a => a + .WriteProperty("name", "Squidex.Infrastructure.Tests") + .WriteProperty("version", "1.0.0.0") + .WriteProperty("sessionId", sessionId.ToString()))); Assert.Equal(expected, output); }