From a3ac1f3d8af0a6b380165c72f5d1dd1bcabaf8cb Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Fri, 12 Aug 2016 12:26:10 -0700 Subject: [PATCH 1/3] Add Blob AzureWebApp provider --- ...AzureWebAppDiagnosticsFactoryExtensions.cs | 15 +- .../AzureWebAppDiagnosticsLoggerProvider.cs | 34 ++-- .../AzureWebAppDiagnosticsSettings.cs | 43 +++++ .../Internal/AzureBlobLoggerProvider.cs | 60 +++++++ .../Internal/AzureBlobSink.cs | 106 ++++++++++++ .../Internal/FileLoggerProvider.cs | 74 ++++----- .../Internal/IWebAppContext.cs | 5 + .../Internal/SerilogLoggerProvider.cs | 113 +------------ .../Internal/WebAppContext.cs | 14 +- .../Internal/WebAppLogConfiguration.cs | 8 +- .../Internal/WebAppLogConfigurationReader.cs | 5 +- .../WebConfigurationReaderLevelSwitch.cs | 68 ++++++++ .../project.json | 7 +- .../SerilogLoggerProviderTests.cs | 155 +++++++++--------- .../project.json | 3 +- 15 files changed, 446 insertions(+), 264 deletions(-) create mode 100644 src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsSettings.cs create mode 100644 src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs create mode 100644 src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs create mode 100644 src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebConfigurationReaderLevelSwitch.cs diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsFactoryExtensions.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsFactoryExtensions.cs index d1a09a85..2315728c 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsFactoryExtensions.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsFactoryExtensions.cs @@ -15,13 +15,22 @@ public static class AzureWebAppDiagnosticsFactoryExtensions /// Adds an Azure Web Apps diagnostics logger. /// /// The extension method argument - /// A strictly positive value representing the maximum log size in megabytes. Once the log is full, no more message will be appended - public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory, int fileSizeLimitMb = FileLoggerProvider.DefaultFileSizeLimitMb) + public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory) + { + return AddAzureWebAppDiagnostics(factory, null); + } + + /// + /// Adds an Azure Web Apps diagnostics logger. + /// + /// The extension method argument + /// The setting object to configure loggers. + public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory, AzureWebAppDiagnosticsSettings settings) { if (WebAppContext.Default.IsRunningInAzureWebApp) { // Only add the provider if we're in Azure WebApp. That cannot change once the apps started - factory.AddProvider(new AzureWebAppDiagnosticsLoggerProvider(WebAppContext.Default, fileSizeLimitMb)); + factory.AddProvider(new AzureWebAppDiagnosticsLoggerProvider(WebAppContext.Default, settings)); } return factory; } diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs index 8029af32..61c3628f 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal; +using Serilog; namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics { @@ -13,31 +14,36 @@ public class AzureWebAppDiagnosticsLoggerProvider : ILoggerProvider { private readonly IWebAppLogConfigurationReader _configurationReader; - private readonly ILoggerProvider _innerLoggerProvider; - private readonly bool _runningInWebApp; + private readonly LoggerFactory _loggerFactory; /// /// Creates a new instance of the class. /// - public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, int fileSizeLimitMb) + public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, AzureWebAppDiagnosticsSettings settings) { _configurationReader = new WebAppLogConfigurationReader(context); var config = _configurationReader.Current; - _runningInWebApp = config.IsRunningInWebApp; + var runningInWebApp = config.IsRunningInWebApp; - if (!_runningInWebApp) + if (runningInWebApp) { - _innerLoggerProvider = NullLoggerProvider.Instance; - } - else - { - _innerLoggerProvider = new FileLoggerProvider(_configurationReader, fileSizeLimitMb); + _loggerFactory = new LoggerFactory(); + var fileLoggerProvider = new FileLoggerProvider( + settings.FileSizeLimit, + settings.RetainedFileCountLimit, + settings.OutputTemplate); + _loggerFactory.AddSerilog(fileLoggerProvider.ConfigureLogger(_configurationReader)); if (!string.IsNullOrEmpty(config.BlobContainerUrl)) { - // TODO: Add the blob logger by creating a composite inner logger which calls - // both loggers + var blobLoggerProvider = new AzureBlobLoggerProvider( + settings.OutputTemplate, + context.SiteName, + settings.BlobName, + settings.BlobBatchSize, + settings.BlobCommitPeriod); + _loggerFactory.AddSerilog(blobLoggerProvider.ConfigureLogger(_configurationReader)); } } } @@ -45,13 +51,13 @@ public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, int fileSizeL /// public ILogger CreateLogger(string categoryName) { - return _innerLoggerProvider.CreateLogger(categoryName); + return _loggerFactory?.CreateLogger(categoryName) ?? NullLogger.Instance; } /// public void Dispose() { - _innerLoggerProvider.Dispose(); + _loggerFactory.Dispose(); _configurationReader.Dispose(); } } diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsSettings.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsSettings.cs new file mode 100644 index 00000000..9fa8b80f --- /dev/null +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsSettings.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics +{ + /// + /// Settings for . + /// + public class AzureWebAppDiagnosticsSettings + { + /// + /// A strictly positive value representing the maximum log size in bytes. Once the log is full, no more message will be appended + /// + public int FileSizeLimit { get; set; } = 10 * 1024 * 1024; + + /// + /// A strictly positive value representing the maximum retained file count + /// + public int RetainedFileCountLimit { get; set; } = 2; + + /// + /// A message template describing the output messages + /// + public string OutputTemplate { get; set; } = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"; + + /// + /// The maximum number of events to include in a single blob append batch. + /// + public int BlobBatchSize { get; set; } = 32; + + /// + /// The time to wait between checking for blob log batches + /// + public TimeSpan BlobCommitPeriod { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// The last section of log blob name. + /// + public string BlobName { get; set; } = "applicationLog.txt"; + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs new file mode 100644 index 00000000..a0c3623d --- /dev/null +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Serilog; +using Serilog.Core; +using Serilog.Formatting.Display; +using Microsoft.WindowsAzure.Storage.Blob; + +namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal +{ + /// + /// The implemenation that creates instances of connected to . + /// + public class AzureBlobLoggerProvider : SerilogLoggerProvider + { + private readonly string _outputTemplate; + private readonly string _appName; + private readonly string _fileName; + private readonly int _batchSize; + private readonly TimeSpan _period; + + /// + /// Creates a new instance of the class. + /// + /// + /// + /// + /// + /// + public AzureBlobLoggerProvider(string outputTemplate, string appName, string fileName, int batchSize, TimeSpan period) + { + _outputTemplate = outputTemplate; + _appName = appName; + _fileName = fileName; + _batchSize = batchSize; + _period = period; + } + + /// + public override Logger ConfigureLogger(IWebAppLogConfigurationReader reader) + { + var messageFormatter = new MessageTemplateTextFormatter(_outputTemplate, null); + var container = new CloudBlobContainer(new Uri(reader.Current.BlobContainerUrl)); + var azureBlobSink = new AzureBlobSink(container, _appName, _fileName, messageFormatter, _batchSize, _period); + var backgroundSink = new BackgroundSink(azureBlobSink, BackgroundSink.DefaultLogMessagesQueueSize); + LoggerConfiguration loggerConfiguration = new LoggerConfiguration(); + + loggerConfiguration.WriteTo.Sink(backgroundSink); + loggerConfiguration.MinimumLevel.ControlledBy(new WebConfigurationReaderLevelSwitch(reader, + configuration => + { + return configuration.BlobLoggingEnabled ? configuration.BlobLoggingLevel: LogLevel.None; + })); + + return loggerConfiguration.CreateLogger(); + } + + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs new file mode 100644 index 00000000..4ae617c1 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Blob; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Sinks.PeriodicBatching; + +namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal +{ + /// + /// The implemenation that stores messages by appending them to Azure Blob in batches. + /// + public class AzureBlobSink : PeriodicBatchingSink + { + private const string BlobPathSeparator = "/"; + + private readonly string _appName; + private readonly string _fileName; + private readonly ITextFormatter _formatter; + private readonly CloudBlobContainer _container; + + /// + /// Creates a new instance of + /// + /// The container to store logs to. + /// The application name to use in blob path generation. + /// The last segment of blob name. + /// The for log messages. + /// The maximum number of events to include in a single batch. + /// The time to wait between checking for event batches. + public AzureBlobSink(CloudBlobContainer container, + string appName, + string fileName, + ITextFormatter formatter, + int batchSizeLimit, + TimeSpan period) : base(batchSizeLimit, period) + { + _appName = appName; + _fileName = fileName; + _formatter = formatter; + if (batchSizeLimit < 1) + { + throw new ArgumentException(nameof(batchSizeLimit)); + } + _container = container; + } + + /// + protected override async Task EmitBatchAsync(IEnumerable events) + { + var eventGroups = events.GroupBy(GetBlobKey); + foreach (var eventGroup in eventGroups) + { + var blobName = string.Concat( + _appName, BlobPathSeparator, + eventGroup.Key.Item1, BlobPathSeparator, + eventGroup.Key.Item2, BlobPathSeparator, + eventGroup.Key.Item3, BlobPathSeparator, + eventGroup.Key.Item4, BlobPathSeparator, + _fileName + ); + + var blob = _container.GetAppendBlobReference(blobName); + + CloudBlobStream stream; + try + { + stream = await blob.OpenWriteAsync(createNew: false); + } + // Blob does not exist + catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) + { + await blob.CreateOrReplaceAsync(AccessCondition.GenerateIfNotExistsCondition(), null, null); + stream = await blob.OpenWriteAsync(createNew: false); + } + + using (stream) + { + using (var writer = new StreamWriter(stream)) + { + foreach (var logEvent in eventGroup) + { + _formatter.Format(logEvent, writer); + } + } + } + } + } + + private Tuple GetBlobKey(LogEvent e) + { + return Tuple.Create(e.Timestamp.Year, + e.Timestamp.Month, + e.Timestamp.Day, + e.Timestamp.Hour); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/FileLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/FileLoggerProvider.cs index f1a12ff6..e2c96059 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/FileLoggerProvider.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/FileLoggerProvider.cs @@ -4,69 +4,65 @@ using System; using System.IO; using Serilog; +using Serilog.Core; using Serilog.Formatting.Display; using Serilog.Sinks.RollingFile; namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal { /// - /// A file logger for Azure WebApp. + /// The implemenation that creates instances of connected to . /// - public class FileLoggerProvider : SerilogLoggerProvider + public class FileLoggerProvider: SerilogLoggerProvider { - /// - /// The default file size limit in megabytes - /// - public const int DefaultFileSizeLimitMb = 10; + private readonly int _fileSizeLimit; + private readonly int _retainedFileCountLimit; + private readonly string _outputTemplate; - // Two days retention limit is okay because the file logger turns itself off after 12 hours (portal feature) - private const int RetainedFileCountLimit = 2; // Days (also number of files because we have 1 file/day) - - private const string OutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"; private const string FileNamePattern = "diagnostics-{Date}.txt"; /// /// Creates a new instance of the class. /// - /// A configuration reader /// A strictly positive value representing the maximum log size in megabytes. Once the log is full, no more message will be appended - public FileLoggerProvider(IWebAppLogConfigurationReader configReader, int fileSizeLimit) - : base(configReader, (loggerConfiguration, webAppConfiguration) => - { - if (string.IsNullOrEmpty(webAppConfiguration.FileLoggingFolder)) - { - throw new ArgumentNullException(nameof(webAppConfiguration.FileLoggingFolder), "The file logger path cannot be null or empty."); - } - - var logsFolder = webAppConfiguration.FileLoggingFolder; - if (!Directory.Exists(logsFolder)) - { - Directory.CreateDirectory(logsFolder); - } - var logsFilePattern = Path.Combine(logsFolder, FileNamePattern); - - var fileSizeLimitBytes = fileSizeLimit * 1024 * 1024; - - var messageFormatter = new MessageTemplateTextFormatter(OutputTemplate, null); - var rollingFileSink = new RollingFileSink(logsFilePattern, messageFormatter, fileSizeLimitBytes, RetainedFileCountLimit); - var backgroundSink = new BackgroundSink(rollingFileSink, BackgroundSink.DefaultLogMessagesQueueSize); - - loggerConfiguration.WriteTo.Sink(backgroundSink); - }) + /// + /// + public FileLoggerProvider(int fileSizeLimit, int retainedFileCountLimit, string outputTemplate) { + _fileSizeLimit = fileSizeLimit; + _retainedFileCountLimit = retainedFileCountLimit; + _outputTemplate = outputTemplate; } /// - protected override void OnConfigurationChanged(WebAppLogConfiguration newConfiguration) + public override Logger ConfigureLogger(IWebAppLogConfigurationReader reader) { - if (!newConfiguration.FileLoggingEnabled) + var webAppConfiguration = reader.Current; + if (string.IsNullOrEmpty(webAppConfiguration.FileLoggingFolder)) { - LevelSwitcher.MinimumLevel = LogLevelDisabled; + throw new ArgumentNullException(nameof(webAppConfiguration.FileLoggingFolder), + "The file logger path cannot be null or empty."); } - else + + var logsFolder = webAppConfiguration.FileLoggingFolder; + if (!Directory.Exists(logsFolder)) { - LevelSwitcher.MinimumLevel = LogLevelToLogEventLevel(newConfiguration.FileLoggingLevel); + Directory.CreateDirectory(logsFolder); } + var logsFilePattern = Path.Combine(logsFolder, FileNamePattern); + + var messageFormatter = new MessageTemplateTextFormatter(_outputTemplate, null); + var rollingFileSink = new RollingFileSink(logsFilePattern, messageFormatter, _fileSizeLimit, _retainedFileCountLimit); + var backgroundSink = new BackgroundSink(rollingFileSink, BackgroundSink.DefaultLogMessagesQueueSize); + + LoggerConfiguration loggerConfiguration = new LoggerConfiguration(); + loggerConfiguration.WriteTo.Sink(backgroundSink); + loggerConfiguration.MinimumLevel.ControlledBy(new WebConfigurationReaderLevelSwitch(reader, + configuration => + { + return configuration.FileLoggingEnabled ? configuration.FileLoggingLevel : LogLevel.None; + })); + return loggerConfiguration.CreateLogger(); } } } diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/IWebAppContext.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/IWebAppContext.cs index 7af58cf3..7a29da51 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/IWebAppContext.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/IWebAppContext.cs @@ -13,6 +13,11 @@ public interface IWebAppContext /// string HomeFolder { get; } + /// + /// Gets the name of site if running in Azure WebApp + /// + string SiteName { get; } + /// /// Gets a value indicating whether or new we're in an Azure WebApp /// diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/SerilogLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/SerilogLoggerProvider.cs index 6316742b..adbdfc64 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/SerilogLoggerProvider.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/SerilogLoggerProvider.cs @@ -4,122 +4,19 @@ using System; using Serilog; using Serilog.Core; -using Serilog.Events; namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal { /// /// Represents a Serilog logger provider use for Azure WebApp. /// - public abstract class SerilogLoggerProvider : ILoggerProvider + public abstract class SerilogLoggerProvider { - // Solution suggested by the Serilog creator http://stackoverflow.com/questions/30849166/how-to-turn-off-serilog /// - /// The log level at which the logger is disabled. + /// /// - protected static LogEventLevel LogLevelDisabled = ((LogEventLevel)1 + (int)LogEventLevel.Fatal); - - private readonly LoggingLevelSwitch _levelSwitch = new LoggingLevelSwitch(); - - private readonly IWebAppLogConfigurationReader _configReader; - private readonly ILoggerFactory _loggerFactory; - - /// - /// Creates a new instance of the class. - /// - /// The configuration reader - /// The actions required to configure the logger - public SerilogLoggerProvider(IWebAppLogConfigurationReader configReader, Action configureLogger) - { - if (configReader == null) - { - throw new ArgumentNullException(nameof(configReader)); - } - if (configureLogger == null) - { - throw new ArgumentNullException(nameof(configureLogger)); - } - - _configReader = configReader; - var webAppsConfiguration = configReader.Current; - - configReader.OnConfigurationChanged += OnConfigurationChanged; - - var loggerConfiguration = new LoggerConfiguration() - .MinimumLevel.ControlledBy(_levelSwitch); - configureLogger(loggerConfiguration, webAppsConfiguration); - var serilogLogger = loggerConfiguration.CreateLogger(); - - OnConfigurationChanged(webAppsConfiguration); - - _loggerFactory = new LoggerFactory(); - _loggerFactory.AddSerilog(serilogLogger); - } - - /// - /// The switch used the modify the logging level. - /// - protected LoggingLevelSwitch LevelSwitcher => _levelSwitch; - - /// - /// Called when the configuration changes - /// - /// The new configuration values - protected abstract void OnConfigurationChanged(WebAppLogConfiguration newConfiguration); - - /// - public ILogger CreateLogger(string categoryName) - { - return _loggerFactory.CreateLogger(categoryName); - } - - /// - /// Disposes this object. - /// - public void Dispose() - { - _configReader.OnConfigurationChanged -= OnConfigurationChanged; - _loggerFactory.Dispose(); - } - - private void OnConfigurationChanged(object sender, WebAppLogConfiguration newConfiguration) - { - OnConfigurationChanged(newConfiguration); - } - - /// - /// Converts a object to . - /// - /// The log level to convert - /// A instance - protected static LogEventLevel LogLevelToLogEventLevel(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Trace: - return LogEventLevel.Verbose; - - case LogLevel.Debug: - return LogEventLevel.Debug; - - case LogLevel.Information: - return LogEventLevel.Information; - - case LogLevel.Warning: - return LogEventLevel.Warning; - - case LogLevel.Error: - return LogEventLevel.Error; - - case LogLevel.Critical: - return LogEventLevel.Fatal; - - case LogLevel.None: - return LogLevelDisabled; - - default: - throw new ArgumentOutOfRangeException($"Unknown log level: {logLevel}"); - } - } + /// + /// + public abstract Logger ConfigureLogger(IWebAppLogConfigurationReader reader); } } diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppContext.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppContext.cs index 4d473dbf..4a35c024 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppContext.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppContext.cs @@ -21,14 +21,10 @@ private WebAppContext() { } public string HomeFolder { get; } = Environment.GetEnvironmentVariable("HOME"); /// - public bool IsRunningInAzureWebApp - { - get - { - return - !string.IsNullOrEmpty(HomeFolder) && - !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME")); - } - } + public string SiteName { get; } = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME"); + + /// + public bool IsRunningInAzureWebApp => !string.IsNullOrEmpty(HomeFolder) && + !string.IsNullOrEmpty(SiteName); } } diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppLogConfiguration.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppLogConfiguration.cs index 12b7545b..48cf489f 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppLogConfiguration.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppLogConfiguration.cs @@ -14,12 +14,12 @@ public class WebAppLogConfiguration public static WebAppLogConfiguration Disabled { get; } = new WebAppLogConfigurationBuilder().Build(); internal WebAppLogConfiguration( - bool isRunningInWebApp, + bool isRunningInWebApp, bool fileLoggingEnabled, - LogLevel fileLoggingLevel, + LogLevel fileLoggingLevel, string fileLoggingFolder, - bool blobLoggingEnabled, - LogLevel blobLoggingLevel, + bool blobLoggingEnabled, + LogLevel blobLoggingLevel, string blobContainerUrl) { IsRunningInWebApp = isRunningInWebApp; diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppLogConfigurationReader.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppLogConfigurationReader.cs index db519f9e..95a31205 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppLogConfigurationReader.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppLogConfigurationReader.cs @@ -79,10 +79,7 @@ private void OnConfigurationTokenChange(object state) ReloadConfiguration(); SubscribeToConfigurationChangeEvent(); - if (OnConfigurationChanged != null) - { - OnConfigurationChanged(this, _latestConfiguration); - } + OnConfigurationChanged?.Invoke(this, _latestConfiguration); } private void SubscribeToConfigurationChangeEvent() diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebConfigurationReaderLevelSwitch.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebConfigurationReaderLevelSwitch.cs new file mode 100644 index 00000000..48642346 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebConfigurationReaderLevelSwitch.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using Serilog.Core; +using Serilog.Events; + +namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal +{ + /// + /// The implementation that runs callback + /// when event fires. + /// + public class WebConfigurationReaderLevelSwitch : LoggingLevelSwitch + { + /// + /// The log level at which the logger is disabled. + /// + private static readonly LogEventLevel LogLevelDisabled = LogEventLevel.Fatal + 1; + + /// + /// Creates a new instance of the class. + /// + /// + /// + public WebConfigurationReaderLevelSwitch(IWebAppLogConfigurationReader reader, Func convert ) + { + reader.OnConfigurationChanged += (sender, configuration) => + { + MinimumLevel = LogLevelToLogEventLevel(convert(configuration)); + }; + } + + /// + /// Converts a object to . + /// + /// The log level to convert + /// A instance + private static LogEventLevel LogLevelToLogEventLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return LogEventLevel.Verbose; + + case LogLevel.Debug: + return LogEventLevel.Debug; + + case LogLevel.Information: + return LogEventLevel.Information; + + case LogLevel.Warning: + return LogEventLevel.Warning; + + case LogLevel.Error: + return LogEventLevel.Error; + + case LogLevel.Critical: + return LogEventLevel.Fatal; + + case LogLevel.None: + return LogLevelDisabled; + + default: + throw new ArgumentOutOfRangeException($"Unknown log level: {logLevel}"); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/project.json b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/project.json index af5a6079..3e9b7ce8 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/project.json +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/project.json @@ -17,7 +17,9 @@ "Microsoft.Extensions.Logging": "1.1.0-*", "Microsoft.Extensions.Logging.Abstractions": "1.1.0-*", "Serilog.Extensions.Logging": "1.0.0", - "Serilog.Sinks.RollingFile": "2.1.0" + "Serilog.Sinks.RollingFile": "2.1.0", + "Serilog.Sinks.PeriodicBatching": "2.0.0", + "WindowsAzure.Storage": "7.2.0" }, "frameworks": { "net451": { @@ -31,7 +33,8 @@ "dependencies": { "System.Console": "4.0.0-*", "System.Threading.Thread": "4.0.0-*" - } + }, + "imports": [ "portable-net40+sl5+win8+wp8+wpa81", "portable-net45+win8+wp8+wpa81" ] } } } \ No newline at end of file diff --git a/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/SerilogLoggerProviderTests.cs b/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/SerilogLoggerProviderTests.cs index e7b2bf10..985e3fd3 100644 --- a/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/SerilogLoggerProviderTests.cs +++ b/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/SerilogLoggerProviderTests.cs @@ -22,11 +22,9 @@ public void OnStartDisable() // Nothing should be called on this object var testSink = new Mock(MockBehavior.Strict); - using (var provider = new TestWebAppSerilogLoggerProvider(testSink.Object, configReader.Object)) - { - var logger = provider.CreateLogger("TestLogger"); - logger.LogInformation("Test"); - } + var provider = new TestWebAppSerilogLoggerProvider(testSink.Object); + var logger = provider.ConfigureLogger(configReader.Object); + logger.Information("Test"); } [Fact] @@ -40,15 +38,13 @@ public void OnStartLoggingLevel() var configReader = new Mock(); configReader.SetupGet(m => m.Current).Returns(config); - + var testSink = new Mock(MockBehavior.Strict); testSink.Setup(m => m.Emit(It.IsAny())); - using (var provider = new TestWebAppSerilogLoggerProvider(testSink.Object, configReader.Object)) - { - var logger = provider.CreateLogger("TestLogger"); - logger.LogInformation("Test"); - } + var provider = new TestWebAppSerilogLoggerProvider(testSink.Object); + var logger = provider.ConfigureLogger(configReader.Object); + logger.Information("Test"); testSink.Verify(m => m.Emit(It.IsAny()), Times.Once); } @@ -70,11 +66,11 @@ public void DynamicDisable() var testSink = new Mock(MockBehavior.Strict); testSink.Setup(m => m.Emit(It.IsAny())); - using (var provider = new TestWebAppSerilogLoggerProvider(testSink.Object, configReader.Object)) + var provider = new TestWebAppSerilogLoggerProvider(testSink.Object); { - var logger = provider.CreateLogger("TestLogger"); + var logger = provider.ConfigureLogger(configReader.Object); - logger.LogInformation("Test1"); + logger.Information("Test1"); testSink.Verify(m => m.Emit(It.IsAny()), Times.Once); configBuilder.SetFileLoggingEnabled(false); @@ -83,7 +79,7 @@ public void DynamicDisable() configReader.Raise(m => m.OnConfigurationChanged += (sender, e) => { }, null, currentConfig); // Logging should be disabled now - logger.LogInformation("Test1"); + logger.Information("Test1"); testSink.Verify(m => m.Emit(It.IsAny()), Times.Once); } } @@ -92,11 +88,11 @@ public void DynamicDisable() public void DynamicLoggingLevel() { var configBuilder = new WebAppLogConfigurationBuilder() - .SetIsRunningInAzureWebApps(true) - .SetFileLoggingEnabled(true) - .SetFileLoggingLevel(LogLevel.Critical); + .SetIsRunningInAzureWebApps(true) + .SetFileLoggingEnabled(true) + .SetFileLoggingLevel(LogLevel.Critical); - var currentConfig = configBuilder.Build(); + var currentConfig = configBuilder.Build(); var configReader = new Mock(); configReader.SetupGet(m => m.Current) @@ -105,22 +101,20 @@ public void DynamicLoggingLevel() var testSink = new Mock(MockBehavior.Strict); testSink.Setup(m => m.Emit(It.IsAny())); - using (var provider = new TestWebAppSerilogLoggerProvider(testSink.Object, configReader.Object)) - { - var logger = provider.CreateLogger("TestLogger"); + var provider = new TestWebAppSerilogLoggerProvider(testSink.Object); + var logger = provider.ConfigureLogger(configReader.Object); - logger.LogDebug("Test1"); - testSink.Verify(m => m.Emit(It.IsAny()), Times.Never); + logger.Debug("Test1"); + testSink.Verify(m => m.Emit(It.IsAny()), Times.Never); - configBuilder.SetFileLoggingLevel(LogLevel.Debug); - currentConfig = configBuilder.Build(); + configBuilder.SetFileLoggingLevel(LogLevel.Debug); + currentConfig = configBuilder.Build(); - configReader.Raise(m => m.OnConfigurationChanged += (sender, e) => { }, null, currentConfig); + configReader.Raise(m => m.OnConfigurationChanged += (sender, e) => { }, null, currentConfig); - // Logging for this level should be enabled now - logger.LogDebug("Test1"); - testSink.Verify(m => m.Emit(It.IsAny()), Times.Once); - } + // Logging for this level should be enabled now + logger.Debug("Test1"); + testSink.Verify(m => m.Emit(It.IsAny()), Times.Once); } // Checks that the .net log level to serilog level mappings are doing what we expect @@ -128,8 +122,8 @@ public void DynamicLoggingLevel() public void LevelMapping() { var configBuilder = new WebAppLogConfigurationBuilder() - .SetIsRunningInAzureWebApps(true) - .SetFileLoggingEnabled(true); + .SetIsRunningInAzureWebApps(true) + .SetFileLoggingEnabled(true); var currentConfig = configBuilder.Build(); @@ -140,68 +134,69 @@ public void LevelMapping() var testSink = new Mock(MockBehavior.Strict); testSink.Setup(m => m.Emit(It.IsAny())); - using (var provider = new TestWebAppSerilogLoggerProvider(testSink.Object, configReader.Object)) + var provider = new TestWebAppSerilogLoggerProvider(testSink.Object); + var levelsToCheck = new LogLevel[] { - var levelsToCheck = new LogLevel[] { - LogLevel.None, - LogLevel.Critical, - LogLevel.Error, - LogLevel.Warning, - LogLevel.Information, - LogLevel.Debug, - LogLevel.Trace - }; - - var logger = provider.CreateLogger("TestLogger"); - - for (int i = 0; i < levelsToCheck.Length; i++) - { - var enabledLevel = levelsToCheck[i]; + LogLevel.None, + LogLevel.Critical, + LogLevel.Error, + LogLevel.Warning, + LogLevel.Information, + LogLevel.Debug, + LogLevel.Trace + }; + + var seriloglogger = provider.ConfigureLogger(configReader.Object); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddSerilog(seriloglogger); + var logger = loggerFactory.CreateLogger("TestLogger"); + + for (int i = 0; i < levelsToCheck.Length; i++) + { + var enabledLevel = levelsToCheck[i]; - // Change the logging level - configBuilder.SetFileLoggingLevel(enabledLevel); - currentConfig = configBuilder.Build(); - configReader.Raise(m => m.OnConfigurationChanged += (sender, e) => { }, null, currentConfig); + // Change the logging level + configBuilder.SetFileLoggingLevel(enabledLevel); + currentConfig = configBuilder.Build(); + configReader.Raise(m => m.OnConfigurationChanged += (sender, e) => { }, null, currentConfig); - // Don't try to log "None" (start at 1) - for (int j = 1; j < levelsToCheck.Length; j++) - { - logger.Log(levelsToCheck[j], 1, new object(), null, (state, ex) => string.Empty); - } + // Don't try to log "None" (start at 1) + for (int j = 1; j < levelsToCheck.Length; j++) + { + logger.Log(levelsToCheck[j], 1, new object(), null, (state, ex) => string.Empty); + } - // On each level we expect an extra message from the previous - testSink.Verify( - m => m.Emit(It.IsAny()), - Times.Exactly(i), - $"Enabled level: {enabledLevel}"); + // On each level we expect an extra message from the previous + testSink.Verify( + m => m.Emit(It.IsAny()), + Times.Exactly(i), + $"Enabled level: {enabledLevel}"); - testSink.ResetCalls(); - } + testSink.ResetCalls(); } } private class TestWebAppSerilogLoggerProvider : SerilogLoggerProvider { - public TestWebAppSerilogLoggerProvider(ILogEventSink sink, IWebAppLogConfigurationReader configReader) : - base(configReader, - (logger, config) => - { - logger.WriteTo.Sink(sink); - }) + private readonly ILogEventSink _sink; + + public TestWebAppSerilogLoggerProvider(ILogEventSink sink) { + _sink = sink; } - protected override void OnConfigurationChanged(WebAppLogConfiguration newConfiguration) + public override Logger ConfigureLogger(IWebAppLogConfigurationReader reader) { - if (!newConfiguration.FileLoggingEnabled) - { - LevelSwitcher.MinimumLevel = LogLevelDisabled; - } - else - { - LevelSwitcher.MinimumLevel = LogLevelToLogEventLevel(newConfiguration.FileLoggingLevel); - } + var loggerConfiguration = new LoggerConfiguration(); + loggerConfiguration.WriteTo.Sink(_sink); + loggerConfiguration.MinimumLevel.ControlledBy(new WebConfigurationReaderLevelSwitch(reader, + configuration => + { + return configuration.FileLoggingEnabled ? configuration.FileLoggingLevel : LogLevel.None; + })); + + return loggerConfiguration.CreateLogger(); } } } diff --git a/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/project.json b/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/project.json index 7e3ab744..ac5eff87 100644 --- a/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/project.json +++ b/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/project.json @@ -17,7 +17,8 @@ "version": "1.0.0-*", "type": "platform" } - } + }, + "imports": [ "portable-net40+sl5+win8+wp8+wpa81", "portable-net45+win8+wp8+wpa81" ] }, "net451": {} } From 248ae2b49ae5b0f4d9fca3b9ac953e2ce65a88e0 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Thu, 18 Aug 2016 16:26:45 -0700 Subject: [PATCH 2/3] Adress PR comments --- ...AzureWebAppDiagnosticsFactoryExtensions.cs | 2 +- .../AzureWebAppDiagnosticsLoggerProvider.cs | 13 ++++- .../AzureWebAppDiagnosticsSettings.cs | 15 +++-- .../Internal/AzureBlobLoggerProvider.cs | 58 ++++++++++++++----- .../Internal/AzureBlobSink.cs | 25 ++++++-- .../Internal/FileLoggerProvider.cs | 32 +++++++--- .../Internal/IWebAppContext.cs | 5 ++ .../Internal/SerilogLoggerProvider.cs | 22 ------- .../Internal/WebAppContext.cs | 3 + .../WebConfigurationReaderLevelSwitch.cs | 1 + .../SerilogLoggerProviderTests.cs | 10 ++-- 11 files changed, 125 insertions(+), 61 deletions(-) delete mode 100644 src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/SerilogLoggerProvider.cs diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsFactoryExtensions.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsFactoryExtensions.cs index 2315728c..0124877d 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsFactoryExtensions.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsFactoryExtensions.cs @@ -17,7 +17,7 @@ public static class AzureWebAppDiagnosticsFactoryExtensions /// The extension method argument public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory) { - return AddAzureWebAppDiagnostics(factory, null); + return AddAzureWebAppDiagnostics(factory, new AzureWebAppDiagnosticsSettings()); } /// diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs index 61c3628f..7102e4d1 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs @@ -1,7 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.Extensions.Logging.Abstractions; +using System; +using Microsoft.Extensions.Logging.Abstractions.Internal; using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal; using Serilog; @@ -21,6 +22,11 @@ public class AzureWebAppDiagnosticsLoggerProvider : ILoggerProvider /// public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, AzureWebAppDiagnosticsSettings settings) { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + _configurationReader = new WebAppLogConfigurationReader(context); var config = _configurationReader.Current; @@ -32,6 +38,7 @@ public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, AzureWebAppDi var fileLoggerProvider = new FileLoggerProvider( settings.FileSizeLimit, settings.RetainedFileCountLimit, + settings.BackgroundQueueSize, settings.OutputTemplate); _loggerFactory.AddSerilog(fileLoggerProvider.ConfigureLogger(_configurationReader)); @@ -40,8 +47,10 @@ public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, AzureWebAppDi var blobLoggerProvider = new AzureBlobLoggerProvider( settings.OutputTemplate, context.SiteName, + context.SiteInstanceId, settings.BlobName, settings.BlobBatchSize, + settings.BackgroundQueueSize, settings.BlobCommitPeriod); _loggerFactory.AddSerilog(blobLoggerProvider.ConfigureLogger(_configurationReader)); } @@ -57,7 +66,7 @@ public ILogger CreateLogger(string categoryName) /// public void Dispose() { - _loggerFactory.Dispose(); + _loggerFactory?.Dispose(); _configurationReader.Dispose(); } } diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsSettings.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsSettings.cs index 9fa8b80f..3ee7e71f 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsSettings.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsSettings.cs @@ -11,33 +11,36 @@ namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics public class AzureWebAppDiagnosticsSettings { /// - /// A strictly positive value representing the maximum log size in bytes. Once the log is full, no more message will be appended + /// Gets or sets a strictly positive value representing the maximum log size in bytes. Once the log is full, no more message will be appended. /// public int FileSizeLimit { get; set; } = 10 * 1024 * 1024; /// - /// A strictly positive value representing the maximum retained file count + /// Gets or sets a strictly positive value representing the maximum retained file count. /// public int RetainedFileCountLimit { get; set; } = 2; /// - /// A message template describing the output messages + /// Gets or sets a message template describing the output messages. /// public string OutputTemplate { get; set; } = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"; /// - /// The maximum number of events to include in a single blob append batch. + /// Gets or sets a maximum number of events to include in a single blob append batch. /// public int BlobBatchSize { get; set; } = 32; /// - /// The time to wait between checking for blob log batches + /// Gets or sets a time to wait between checking for blob log batches. /// public TimeSpan BlobCommitPeriod { get; set; } = TimeSpan.FromSeconds(5); /// - /// The last section of log blob name. + /// Gets or sets the last section of log blob name. /// public string BlobName { get; set; } = "applicationLog.txt"; + + /// Gets of sets the maximum size of the background log message queue. + public int BackgroundQueueSize { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs index a0c3623d..278a89be 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs @@ -10,47 +10,79 @@ namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal { /// - /// The implemenation that creates instances of connected to . + /// The implemenation of logger provider that creates instances of . /// - public class AzureBlobLoggerProvider : SerilogLoggerProvider + public class AzureBlobLoggerProvider { private readonly string _outputTemplate; private readonly string _appName; + private readonly string _instanceId; private readonly string _fileName; private readonly int _batchSize; + private readonly int _backgroundQueueSize; private readonly TimeSpan _period; /// /// Creates a new instance of the class. /// - /// - /// - /// - /// - /// - public AzureBlobLoggerProvider(string outputTemplate, string appName, string fileName, int batchSize, TimeSpan period) + /// A message template describing the output messages + /// The application name to use in blob name + /// The application instance id to use in blob name + /// The last section in log blob name + /// A maximum number of events to include in a single blob append batch + /// The maximum size of the background queue + /// A time to wait between checking for blob log batches + public AzureBlobLoggerProvider(string outputTemplate, string appName, string instanceId, string fileName, int batchSize, int backgroundQueueSize, TimeSpan period) { + if (outputTemplate == null) + { + throw new ArgumentNullException(nameof(outputTemplate)); + } + if (appName == null) + { + throw new ArgumentNullException(nameof(appName)); + } + if (instanceId == null) + { + throw new ArgumentNullException(nameof(instanceId)); + } + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + if (batchSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(batchSize), $"{nameof(batchSize)} should be a positive number."); + } + if (period <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(period), $"{nameof(period)} should be longer than zero."); + } + _outputTemplate = outputTemplate; _appName = appName; + _instanceId = instanceId; _fileName = fileName; _batchSize = batchSize; + _backgroundQueueSize = backgroundQueueSize; _period = period; } /// - public override Logger ConfigureLogger(IWebAppLogConfigurationReader reader) + public Logger ConfigureLogger(IWebAppLogConfigurationReader reader) { var messageFormatter = new MessageTemplateTextFormatter(_outputTemplate, null); var container = new CloudBlobContainer(new Uri(reader.Current.BlobContainerUrl)); - var azureBlobSink = new AzureBlobSink(container, _appName, _fileName, messageFormatter, _batchSize, _period); - var backgroundSink = new BackgroundSink(azureBlobSink, BackgroundSink.DefaultLogMessagesQueueSize); - LoggerConfiguration loggerConfiguration = new LoggerConfiguration(); + var fileName = _instanceId + "-" + _fileName; + var azureBlobSink = new AzureBlobSink(container, _appName, fileName, messageFormatter, _batchSize, _period); + var backgroundSink = new BackgroundSink(azureBlobSink, _backgroundQueueSize); + var loggerConfiguration = new LoggerConfiguration(); loggerConfiguration.WriteTo.Sink(backgroundSink); loggerConfiguration.MinimumLevel.ControlledBy(new WebConfigurationReaderLevelSwitch(reader, configuration => { - return configuration.BlobLoggingEnabled ? configuration.BlobLoggingLevel: LogLevel.None; + return configuration.BlobLoggingEnabled ? configuration.BlobLoggingLevel : LogLevel.None; })); return loggerConfiguration.CreateLogger(); diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs index 4ae617c1..03a5cb15 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs @@ -43,13 +43,30 @@ public AzureBlobSink(CloudBlobContainer container, int batchSizeLimit, TimeSpan period) : base(batchSizeLimit, period) { + if (appName == null) + { + throw new ArgumentNullException(nameof(appName)); + } + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + if (formatter == null) + { + throw new ArgumentNullException(nameof(formatter)); + } + if (batchSizeLimit <= 0) + { + throw new ArgumentOutOfRangeException(nameof(batchSizeLimit), $"{nameof(batchSizeLimit)} should be a positive number."); + } + if (period <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(period), $"{nameof(period)} should be longer than zero."); + } + _appName = appName; _fileName = fileName; _formatter = formatter; - if (batchSizeLimit < 1) - { - throw new ArgumentException(nameof(batchSizeLimit)); - } _container = container; } diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/FileLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/FileLoggerProvider.cs index e2c96059..10c7241e 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/FileLoggerProvider.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/FileLoggerProvider.cs @@ -11,12 +11,13 @@ namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal { /// - /// The implemenation that creates instances of connected to . + /// The logger provider that creates instances of . /// - public class FileLoggerProvider: SerilogLoggerProvider + public class FileLoggerProvider { private readonly int _fileSizeLimit; private readonly int _retainedFileCountLimit; + private readonly int _backgroundQueueSize; private readonly string _outputTemplate; private const string FileNamePattern = "diagnostics-{Date}.txt"; @@ -25,17 +26,32 @@ public class FileLoggerProvider: SerilogLoggerProvider /// Creates a new instance of the class. /// /// A strictly positive value representing the maximum log size in megabytes. Once the log is full, no more message will be appended - /// - /// - public FileLoggerProvider(int fileSizeLimit, int retainedFileCountLimit, string outputTemplate) + /// A strictly positive value representing the maximum retained file count + /// The maximum size of the background queue + /// A message template describing the output messages + public FileLoggerProvider(int fileSizeLimit, int retainedFileCountLimit, int backgroundQueueSize, string outputTemplate) { + if (outputTemplate == null) + { + throw new ArgumentNullException(nameof(outputTemplate)); + } + if (fileSizeLimit <= 0) + { + throw new ArgumentOutOfRangeException(nameof(fileSizeLimit), $"{nameof(fileSizeLimit)} should be positive."); + } + if (retainedFileCountLimit <= 0) + { + throw new ArgumentOutOfRangeException(nameof(retainedFileCountLimit), $"{nameof(retainedFileCountLimit)} should be positive."); + } + _fileSizeLimit = fileSizeLimit; _retainedFileCountLimit = retainedFileCountLimit; + _backgroundQueueSize = backgroundQueueSize; _outputTemplate = outputTemplate; } /// - public override Logger ConfigureLogger(IWebAppLogConfigurationReader reader) + public Logger ConfigureLogger(IWebAppLogConfigurationReader reader) { var webAppConfiguration = reader.Current; if (string.IsNullOrEmpty(webAppConfiguration.FileLoggingFolder)) @@ -53,9 +69,9 @@ public override Logger ConfigureLogger(IWebAppLogConfigurationReader reader) var messageFormatter = new MessageTemplateTextFormatter(_outputTemplate, null); var rollingFileSink = new RollingFileSink(logsFilePattern, messageFormatter, _fileSizeLimit, _retainedFileCountLimit); - var backgroundSink = new BackgroundSink(rollingFileSink, BackgroundSink.DefaultLogMessagesQueueSize); + var backgroundSink = new BackgroundSink(rollingFileSink, _backgroundQueueSize); - LoggerConfiguration loggerConfiguration = new LoggerConfiguration(); + var loggerConfiguration = new LoggerConfiguration(); loggerConfiguration.WriteTo.Sink(backgroundSink); loggerConfiguration.MinimumLevel.ControlledBy(new WebConfigurationReaderLevelSwitch(reader, configuration => diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/IWebAppContext.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/IWebAppContext.cs index 7a29da51..ee64358a 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/IWebAppContext.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/IWebAppContext.cs @@ -18,6 +18,11 @@ public interface IWebAppContext /// string SiteName { get; } + /// + /// Gets the id of site if running in Azure WebApp + /// + string SiteInstanceId { get; } + /// /// Gets a value indicating whether or new we're in an Azure WebApp /// diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/SerilogLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/SerilogLoggerProvider.cs deleted file mode 100644 index adbdfc64..00000000 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/SerilogLoggerProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Serilog; -using Serilog.Core; - -namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal -{ - /// - /// Represents a Serilog logger provider use for Azure WebApp. - /// - public abstract class SerilogLoggerProvider - { - /// - /// - /// - /// - /// - public abstract Logger ConfigureLogger(IWebAppLogConfigurationReader reader); - } -} diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppContext.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppContext.cs index 4a35c024..cd4e58c3 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppContext.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebAppContext.cs @@ -23,6 +23,9 @@ private WebAppContext() { } /// public string SiteName { get; } = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME"); + /// + public string SiteInstanceId { get; } = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID"); + /// public bool IsRunningInAzureWebApp => !string.IsNullOrEmpty(HomeFolder) && !string.IsNullOrEmpty(SiteName); diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebConfigurationReaderLevelSwitch.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebConfigurationReaderLevelSwitch.cs index 48642346..f4710d4d 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebConfigurationReaderLevelSwitch.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/WebConfigurationReaderLevelSwitch.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using Serilog.Core; using Serilog.Events; diff --git a/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/SerilogLoggerProviderTests.cs b/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/SerilogLoggerProviderTests.cs index 985e3fd3..4954f303 100644 --- a/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/SerilogLoggerProviderTests.cs +++ b/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/SerilogLoggerProviderTests.cs @@ -135,7 +135,7 @@ public void LevelMapping() testSink.Setup(m => m.Emit(It.IsAny())); var provider = new TestWebAppSerilogLoggerProvider(testSink.Object); - var levelsToCheck = new LogLevel[] + var levelsToCheck = new [] { LogLevel.None, LogLevel.Critical, @@ -151,7 +151,7 @@ public void LevelMapping() loggerFactory.AddSerilog(seriloglogger); var logger = loggerFactory.CreateLogger("TestLogger"); - for (int i = 0; i < levelsToCheck.Length; i++) + for (var i = 0; i < levelsToCheck.Length; i++) { var enabledLevel = levelsToCheck[i]; @@ -161,7 +161,7 @@ public void LevelMapping() configReader.Raise(m => m.OnConfigurationChanged += (sender, e) => { }, null, currentConfig); // Don't try to log "None" (start at 1) - for (int j = 1; j < levelsToCheck.Length; j++) + for (var j = 1; j < levelsToCheck.Length; j++) { logger.Log(levelsToCheck[j], 1, new object(), null, (state, ex) => string.Empty); } @@ -177,7 +177,7 @@ public void LevelMapping() } - private class TestWebAppSerilogLoggerProvider : SerilogLoggerProvider + private class TestWebAppSerilogLoggerProvider { private readonly ILogEventSink _sink; @@ -186,7 +186,7 @@ public TestWebAppSerilogLoggerProvider(ILogEventSink sink) _sink = sink; } - public override Logger ConfigureLogger(IWebAppLogConfigurationReader reader) + public Logger ConfigureLogger(IWebAppLogConfigurationReader reader) { var loggerConfiguration = new LoggerConfiguration(); loggerConfiguration.WriteTo.Sink(_sink); From 862a2df0fa07701a103b6a84a9592cb25a792da3 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Mon, 29 Aug 2016 10:13:56 -0700 Subject: [PATCH 3/3] Add tests --- .../AzureWebAppDiagnosticsLoggerProvider.cs | 1 + .../Internal/AzureBlobLoggerProvider.cs | 9 +- .../Internal/AzureBlobSink.cs | 40 ++-- .../Internal/BlobAppendReferenceWrapper.cs | 36 ++++ .../Internal/ICloudAppendBlob.cs | 26 +++ .../AzureBlobSinkTests.cs | 181 ++++++++++++++++++ 6 files changed, 266 insertions(+), 27 deletions(-) create mode 100644 src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/BlobAppendReferenceWrapper.cs create mode 100644 src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/ICloudAppendBlob.cs create mode 100644 test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/AzureBlobSinkTests.cs diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs index 7102e4d1..4ee51c27 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions.Internal; using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal; using Serilog; diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs index 278a89be..07ac3eea 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs @@ -74,7 +74,14 @@ public Logger ConfigureLogger(IWebAppLogConfigurationReader reader) var messageFormatter = new MessageTemplateTextFormatter(_outputTemplate, null); var container = new CloudBlobContainer(new Uri(reader.Current.BlobContainerUrl)); var fileName = _instanceId + "-" + _fileName; - var azureBlobSink = new AzureBlobSink(container, _appName, fileName, messageFormatter, _batchSize, _period); + var azureBlobSink = new AzureBlobSink( + name => new BlobAppendReferenceWrapper(container.GetAppendBlobReference(name)), + _appName, + fileName, + messageFormatter, + _batchSize, + _period); + var backgroundSink = new BackgroundSink(azureBlobSink, _backgroundQueueSize); var loggerConfiguration = new LoggerConfiguration(); diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs index 03a5cb15..cb137ba8 100644 --- a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; using Serilog.Core; using Serilog.Events; using Serilog.Formatting; @@ -20,23 +19,21 @@ namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal /// public class AzureBlobSink : PeriodicBatchingSink { - private const string BlobPathSeparator = "/"; - private readonly string _appName; private readonly string _fileName; private readonly ITextFormatter _formatter; - private readonly CloudBlobContainer _container; + private readonly Func _blobReferenceFactory; /// /// Creates a new instance of /// - /// The container to store logs to. + /// The container to store logs to. /// The application name to use in blob path generation. /// The last segment of blob name. /// The for log messages. /// The maximum number of events to include in a single batch. /// The time to wait between checking for event batches. - public AzureBlobSink(CloudBlobContainer container, + public AzureBlobSink(Func blobReferenceFactory, string appName, string fileName, ITextFormatter formatter, @@ -67,7 +64,7 @@ public AzureBlobSink(CloudBlobContainer container, _appName = appName; _fileName = fileName; _formatter = formatter; - _container = container; + _blobReferenceFactory = blobReferenceFactory; } /// @@ -76,37 +73,28 @@ protected override async Task EmitBatchAsync(IEnumerable events) var eventGroups = events.GroupBy(GetBlobKey); foreach (var eventGroup in eventGroups) { - var blobName = string.Concat( - _appName, BlobPathSeparator, - eventGroup.Key.Item1, BlobPathSeparator, - eventGroup.Key.Item2, BlobPathSeparator, - eventGroup.Key.Item3, BlobPathSeparator, - eventGroup.Key.Item4, BlobPathSeparator, - _fileName - ); + var key = eventGroup.Key; + var blobName = $"{_appName}/{key.Item1}/{key.Item2:00}/{key.Item3:00}/{key.Item4:00}/{_fileName}"; - var blob = _container.GetAppendBlobReference(blobName); + var blob = _blobReferenceFactory(blobName); - CloudBlobStream stream; + Stream stream; try { - stream = await blob.OpenWriteAsync(createNew: false); + stream = await blob.OpenWriteAsync(); } // Blob does not exist catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) { - await blob.CreateOrReplaceAsync(AccessCondition.GenerateIfNotExistsCondition(), null, null); - stream = await blob.OpenWriteAsync(createNew: false); + await blob.CreateAsync(); + stream = await blob.OpenWriteAsync(); } - using (stream) + using (var writer = new StreamWriter(stream)) { - using (var writer = new StreamWriter(stream)) + foreach (var logEvent in eventGroup) { - foreach (var logEvent in eventGroup) - { - _formatter.Format(logEvent, writer); - } + _formatter.Format(logEvent, writer); } } } diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/BlobAppendReferenceWrapper.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/BlobAppendReferenceWrapper.cs new file mode 100644 index 00000000..8c3bf1d1 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/BlobAppendReferenceWrapper.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Blob; + +namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal +{ + /// + public class BlobAppendReferenceWrapper : ICloudAppendBlob + { + private readonly CloudAppendBlob _cloudAppendBlob; + + /// + /// Creates new instance of . + /// + /// The instance to wrap. + public BlobAppendReferenceWrapper(CloudAppendBlob cloudAppendBlob) + { + _cloudAppendBlob = cloudAppendBlob; + } + /// + public async Task OpenWriteAsync() + { + return await _cloudAppendBlob.OpenWriteAsync(createNew: false); + } + + /// + public async Task CreateAsync() + { + await _cloudAppendBlob.CreateOrReplaceAsync(AccessCondition.GenerateIfNotExistsCondition(), options: null, operationContext: null); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/ICloudAppendBlob.cs b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/ICloudAppendBlob.cs new file mode 100644 index 00000000..b03f9920 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/ICloudAppendBlob.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal +{ + /// + /// Represents an append blob, a type of blob where blocks of data are always committed to the end of the blob. + /// + public interface ICloudAppendBlob + { + /// + /// Initiates an asynchronous operation to open a stream for writing to the blob. + /// + /// A object of type that represents the asynchronous operation. + Task OpenWriteAsync(); + + /// + /// Initiates an asynchronous operation to create an empty append blob. + /// + /// A object that represents the asynchronous operation. + Task CreateAsync(); + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/AzureBlobSinkTests.cs b/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/AzureBlobSinkTests.cs new file mode 100644 index 00000000..8431c205 --- /dev/null +++ b/test/Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test/AzureBlobSinkTests.cs @@ -0,0 +1,181 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal; +using Microsoft.WindowsAzure.Storage; +using Moq; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting.Display; +using Serilog.Parsing; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureWebApps.Test +{ + public class AzureBlobSinkTests + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); + + [Fact] + public void WritesMessagesInBatches() + { + var blob = new Mock(); + var buffers = new List(); + blob.Setup(b => b.OpenWriteAsync()).Returns(() => Task.FromResult((Stream)new TestMemoryStream(buffers))); + + var sink = new TestAzureBlobSink(name => blob.Object, 5); + var logger = CreateLogger(sink); + + for (int i = 0; i < 5; i++) + { + logger.Information("Text " + i); + } + + Assert.True(sink.CountdownEvent.Wait(DefaultTimeout)); + +#if NET451 + Assert.Equal(1, buffers.Count); + Assert.Equal(Encoding.UTF8.GetString(buffers[0]), @"Information Text 0 +Information Text 1 +Information Text 2 +Information Text 3 +Information Text 4 +"); +#else + // PeriodicBatchingSink always writes first message as seperate batch on coreclr + // https://github.com/serilog/serilog-sinks-periodicbatching/issues/7 + Assert.Equal(2, buffers.Count); + Assert.Equal(Encoding.UTF8.GetString(buffers[0]), @"Information Text 0" + Environment.NewLine); + Assert.Equal(Encoding.UTF8.GetString(buffers[1]), @"Information Text 1 +Information Text 2 +Information Text 3 +Information Text 4 +"); +#endif + } + + [Fact] + public void GroupsByHour() + { + var blob = new Mock(); + var buffers = new List(); + var names = new List(); + + blob.Setup(b => b.OpenWriteAsync()).Returns(() => Task.FromResult((Stream)new TestMemoryStream(buffers))); + + var sink = new TestAzureBlobSink(name => + { + names.Add(name); + return blob.Object; + }, 3); + var logger = CreateLogger(sink); + + var startDate = new DateTime(2016, 8, 29, 22, 0, 0); + for (int i = 0; i < 3; i++) + { + var addHours = startDate.AddHours(i); + logger.Write(new LogEvent( + new DateTimeOffset(addHours), + LogEventLevel.Information, + null, + new MessageTemplate("Text", Enumerable.Empty()), + Enumerable.Empty())); + } + + Assert.True(sink.CountdownEvent.Wait(DefaultTimeout)); + + Assert.Equal(3, buffers.Count); + + Assert.Equal("appname/2016/08/29/22/filename", names[0]); + Assert.Equal("appname/2016/08/29/23/filename", names[1]); + Assert.Equal("appname/2016/08/30/00/filename", names[2]); + } + + [Fact] + public void CreatesBlobIfNotExists() + { + var blob = new Mock(); + var buffers = new List(); + bool created = false; + + blob.Setup(b => b.OpenWriteAsync()).Returns(() => + { + if (!created) + { + throw new StorageException(new RequestResult() { HttpStatusCode = 404 }, string.Empty, null); + } + return Task.FromResult((Stream) new TestMemoryStream(buffers)); + }); + + blob.Setup(b => b.CreateAsync()).Returns(() => + { + created = true; + return Task.FromResult(0); + }); + + var sink = new TestAzureBlobSink((name) => blob.Object, 1); + var logger = CreateLogger(sink); + logger.Information("Text"); + + Assert.True(sink.CountdownEvent.Wait(DefaultTimeout)); + + Assert.Equal(1, buffers.Count); + Assert.Equal(true, created); + } + + private static Logger CreateLogger(AzureBlobSink sink) + { + var loggerConfiguration = new LoggerConfiguration(); + loggerConfiguration.WriteTo.Sink(sink); + var logger = loggerConfiguration.CreateLogger(); + return logger; + } + + private class TestAzureBlobSink: AzureBlobSink + { + public CountdownEvent CountdownEvent { get; } + + public TestAzureBlobSink(Func blob, int count):base( + blob, + "appname", + "filename", + new MessageTemplateTextFormatter("{Level} {Message}{NewLine}", CultureInfo.InvariantCulture), + 10, + TimeSpan.FromSeconds(0.1)) + { + CountdownEvent = new CountdownEvent(count); + } + + protected override async Task EmitBatchAsync(IEnumerable events) + { + await base.EmitBatchAsync(events); + CountdownEvent.Signal(events.Count()); + } + } + + private class TestMemoryStream : MemoryStream + { + public List Buffers { get; } + + public TestMemoryStream(List buffers) + { + Buffers = buffers; + } + + protected override void Dispose(bool disposing) + { + Buffers.Add(ToArray()); + base.Dispose(disposing); + } + } + } +}