diff --git a/src/Logging/Logging.AzureAppServices/Directory.Build.props b/src/Logging/Logging.AzureAppServices/Directory.Build.props
new file mode 100644
index 0000000000..68f87d4f24
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+ true
+
+
diff --git a/src/Logging/Logging.AzureAppServices/ref/Microsoft.Extensions.Logging.AzureAppServices.csproj b/src/Logging/Logging.AzureAppServices/ref/Microsoft.Extensions.Logging.AzureAppServices.csproj
new file mode 100644
index 0000000000..0132b21870
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/ref/Microsoft.Extensions.Logging.AzureAppServices.csproj
@@ -0,0 +1,15 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Logging/Logging.AzureAppServices/ref/Microsoft.Extensions.Logging.AzureAppServices.netstandard2.0.cs b/src/Logging/Logging.AzureAppServices/ref/Microsoft.Extensions.Logging.AzureAppServices.netstandard2.0.cs
new file mode 100644
index 0000000000..9b8f637cff
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/ref/Microsoft.Extensions.Logging.AzureAppServices.netstandard2.0.cs
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Extensions.Logging
+{
+ public static partial class AzureAppServicesLoggerFactoryExtensions
+ {
+ public static Microsoft.Extensions.Logging.ILoggingBuilder AddAzureWebAppDiagnostics(this Microsoft.Extensions.Logging.ILoggingBuilder builder) { throw null; }
+ }
+}
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ public partial class AzureBlobLoggerOptions : Microsoft.Extensions.Logging.AzureAppServices.BatchingLoggerOptions
+ {
+ public AzureBlobLoggerOptions() { }
+ public string BlobName { get { throw null; } set { } }
+ }
+ public partial class AzureFileLoggerOptions : Microsoft.Extensions.Logging.AzureAppServices.BatchingLoggerOptions
+ {
+ public AzureFileLoggerOptions() { }
+ public string FileName { get { throw null; } set { } }
+ public int? FileSizeLimit { get { throw null; } set { } }
+ public int? RetainedFileCountLimit { get { throw null; } set { } }
+ }
+ public partial class BatchingLoggerOptions
+ {
+ public BatchingLoggerOptions() { }
+ public int? BackgroundQueueSize { get { throw null; } set { } }
+ public int? BatchSize { get { throw null; } set { } }
+ public System.TimeSpan FlushPeriod { get { throw null; } set { } }
+ public bool IncludeScopes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public bool IsEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ }
+ public abstract partial class BatchingLoggerProvider : Microsoft.Extensions.Logging.ILoggerProvider, Microsoft.Extensions.Logging.ISupportExternalScope, System.IDisposable
+ {
+ internal BatchingLoggerProvider() { }
+ public bool IsEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
+ public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) { throw null; }
+ public void Dispose() { }
+ protected virtual System.Threading.Tasks.Task IntervalAsync(System.TimeSpan interval, System.Threading.CancellationToken cancellationToken) { throw null; }
+ void Microsoft.Extensions.Logging.ISupportExternalScope.SetScopeProvider(Microsoft.Extensions.Logging.IExternalScopeProvider scopeProvider) { }
+ }
+ [Microsoft.Extensions.Logging.ProviderAliasAttribute("AzureAppServicesBlob")]
+ public partial class BlobLoggerProvider : Microsoft.Extensions.Logging.AzureAppServices.BatchingLoggerProvider
+ {
+ public BlobLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor options) { }
+ }
+ [Microsoft.Extensions.Logging.ProviderAliasAttribute("AzureAppServicesFile")]
+ public partial class FileLoggerProvider : Microsoft.Extensions.Logging.AzureAppServices.BatchingLoggerProvider
+ {
+ public FileLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor options) { }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs b/src/Logging/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs
new file mode 100644
index 0000000000..9b680e9138
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging.AzureAppServices;
+using Microsoft.Extensions.Logging.Configuration;
+using Microsoft.Extensions.Options;
+using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
+
+namespace Microsoft.Extensions.Logging
+{
+ ///
+ /// Extension methods for adding Azure diagnostics logger.
+ ///
+ public static class AzureAppServicesLoggerFactoryExtensions
+ {
+ ///
+ /// Adds an Azure Web Apps diagnostics logger.
+ ///
+ /// The extension method argument
+ public static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder)
+ {
+ var context = WebAppContext.Default;
+
+ // Only add the provider if we're in Azure WebApp. That cannot change once the apps started
+ return AddAzureWebAppDiagnostics(builder, context);
+ }
+
+ internal static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder, IWebAppContext context)
+ {
+ if (!context.IsRunningInAzureWebApp)
+ {
+ return builder;
+ }
+
+ builder.AddConfiguration();
+
+ var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(context);
+ var services = builder.Services;
+
+ var addedFileLogger = TryAddEnumerable(services, Singleton());
+ var addedBlobLogger = TryAddEnumerable(services, Singleton());
+
+ if (addedFileLogger || addedBlobLogger)
+ {
+ services.AddSingleton(context);
+ services.AddSingleton>(
+ new ConfigurationChangeTokenSource(config));
+ }
+
+ if (addedFileLogger)
+ {
+ services.AddSingleton>(CreateFileFilterConfigureOptions(config));
+ services.AddSingleton>(new FileLoggerConfigureOptions(config, context));
+ services.AddSingleton>(
+ new ConfigurationChangeTokenSource(config));
+ LoggerProviderOptions.RegisterProviderOptions(builder.Services);
+ }
+
+ if (addedBlobLogger)
+ {
+ services.AddSingleton>(CreateBlobFilterConfigureOptions(config));
+ services.AddSingleton>(new BlobLoggerConfigureOptions(config, context));
+ services.AddSingleton>(
+ new ConfigurationChangeTokenSource(config));
+ LoggerProviderOptions.RegisterProviderOptions(builder.Services);
+ }
+
+ return builder;
+ }
+
+ private static bool TryAddEnumerable(IServiceCollection collection, ServiceDescriptor descriptor)
+ {
+ var beforeCount = collection.Count;
+ collection.TryAddEnumerable(descriptor);
+ return beforeCount != collection.Count;
+ }
+
+ private static ConfigurationBasedLevelSwitcher CreateBlobFilterConfigureOptions(IConfiguration config)
+ {
+ return new ConfigurationBasedLevelSwitcher(
+ configuration: config,
+ provider: typeof(BlobLoggerProvider),
+ levelKey: "AzureBlobTraceLevel");
+ }
+
+ private static ConfigurationBasedLevelSwitcher CreateFileFilterConfigureOptions(IConfiguration config)
+ {
+ return new ConfigurationBasedLevelSwitcher(
+ configuration: config,
+ provider: typeof(FileLoggerProvider),
+ levelKey: "AzureDriveTraceLevel");
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs b/src/Logging/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs
new file mode 100644
index 0000000000..1e1285b358
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Options for Azure diagnostics blob logging.
+ ///
+ public class AzureBlobLoggerOptions: BatchingLoggerOptions
+ {
+ private string _blobName = "applicationLog.txt";
+
+ ///
+ /// Gets or sets the last section of log blob name.
+ /// Defaults to "applicationLog.txt".
+ ///
+ public string BlobName
+ {
+ get { return _blobName; }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException(nameof(value), $"{nameof(BlobName)} must be non-empty string.");
+ }
+ _blobName = value;
+ }
+ }
+
+ internal string ContainerUrl { get; set; }
+
+ internal string ApplicationName { get; set; }
+
+ internal string ApplicationInstanceId { get; set; }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs b/src/Logging/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs
new file mode 100644
index 0000000000..af8b5a112e
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Options for Azure diagnostics file logging.
+ ///
+ public class AzureFileLoggerOptions: BatchingLoggerOptions
+ {
+ private int? _fileSizeLimit = 10 * 1024 * 1024;
+ private int? _retainedFileCountLimit = 2;
+ private string _fileName = "diagnostics-";
+
+ ///
+ /// 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 10MB.
+ ///
+ public int? FileSizeLimit
+ {
+ get { return _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 2.
+ ///
+ public int? RetainedFileCountLimit
+ {
+ get { return _retainedFileCountLimit; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive.");
+ }
+ _retainedFileCountLimit = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a string representing the prefix of the file name used to store the logging information.
+ /// The current date, in the format YYYYMMDD will be added after the given value.
+ /// Defaults to diagnostics-.
+ ///
+ public string FileName
+ {
+ get { return _fileName; }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException(nameof(value));
+ }
+ _fileName = value;
+ }
+ }
+
+ internal string LogDirectory { get; set; }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs b/src/Logging/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs
new file mode 100644
index 0000000000..8dc8727b3a
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class BatchLoggerConfigureOptions : IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly string _isEnabledKey;
+
+ public BatchLoggerConfigureOptions(IConfiguration configuration, string isEnabledKey)
+ {
+ _configuration = configuration;
+ _isEnabledKey = isEnabledKey;
+ }
+
+ public void Configure(BatchingLoggerOptions options)
+ {
+ options.IsEnabled = TextToBoolean(_configuration.GetSection(_isEnabledKey)?.Value);
+ }
+
+ private static bool TextToBoolean(string text)
+ {
+ if (string.IsNullOrEmpty(text) ||
+ !bool.TryParse(text, out var result))
+ {
+ result = false;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/BatchingLogger.cs b/src/Logging/Logging.AzureAppServices/src/BatchingLogger.cs
new file mode 100644
index 0000000000..bd192169f3
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/BatchingLogger.cs
@@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal 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)
+ {
+ return _provider.IsEnabled;
+ }
+
+ public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ {
+ return;
+ }
+
+ var 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);
+
+ var scopeProvider = _provider.ScopeProvider;
+ if (scopeProvider != null)
+ {
+ scopeProvider.ForEachScope((scope, stringBuilder) =>
+ {
+ stringBuilder.Append(" => ").Append(scope);
+ }, builder);
+
+ builder.AppendLine(":");
+ }
+ else
+ {
+ 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);
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/BatchingLoggerOptions.cs b/src/Logging/Logging.AzureAppServices/src/BatchingLoggerOptions.cs
new file mode 100644
index 0000000000..9fbd964800
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/BatchingLoggerOptions.cs
@@ -0,0 +1,80 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Options for a logger which batches up log messages.
+ ///
+ public class BatchingLoggerOptions
+ {
+ private int? _batchSize;
+ private int? _backgroundQueueSize = 1000;
+ private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1);
+
+ ///
+ /// Gets or sets the period after which logs will be flushed to the store.
+ ///
+ public TimeSpan FlushPeriod
+ {
+ get { return _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 null for no limit.
+ /// After maximum queue size is reached log event sink would start blocking.
+ /// Defaults to 1000.
+ ///
+ public int? BackgroundQueueSize
+ {
+ get { return _backgroundQueueSize; }
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BackgroundQueueSize)} must be non-negative.");
+ }
+ _backgroundQueueSize = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a maximum number of events to include in a single batch or null for no limit.
+ ///
+ /// Defaults to null.
+ public int? BatchSize
+ {
+ get { return _batchSize; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BatchSize)} must be positive.");
+ }
+ _batchSize = value;
+ }
+ }
+
+ ///
+ /// Gets or sets value indicating if logger accepts and queues writes.
+ ///
+ public bool IsEnabled { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether scopes should be included in the message.
+ /// Defaults to false.
+ ///
+ public bool IncludeScopes { get; set; } = false;
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/BatchingLoggerProvider.cs b/src/Logging/Logging.AzureAppServices/src/BatchingLoggerProvider.cs
new file mode 100644
index 0000000000..227a616f3b
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/BatchingLoggerProvider.cs
@@ -0,0 +1,208 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// A provider of instances.
+ ///
+ public abstract class BatchingLoggerProvider : ILoggerProvider, ISupportExternalScope
+ {
+ private readonly List _currentBatch = new List();
+ private readonly TimeSpan _interval;
+ private readonly int? _queueSize;
+ private readonly int? _batchSize;
+ private readonly IDisposable _optionsChangeToken;
+
+ private int _messagesDropped;
+
+ private BlockingCollection _messageQueue;
+ private Task _outputTask;
+ private CancellationTokenSource _cancellationTokenSource;
+
+ private bool _includeScopes;
+ private IExternalScopeProvider _scopeProvider;
+
+ internal IExternalScopeProvider ScopeProvider => _includeScopes ? _scopeProvider : null;
+
+ internal BatchingLoggerProvider(IOptionsMonitor options)
+ {
+ // NOTE: Only IsEnabled is monitored
+
+ var loggerOptions = options.CurrentValue;
+ 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;
+
+ _optionsChangeToken = options.OnChange(UpdateOptions);
+ UpdateOptions(options.CurrentValue);
+ }
+
+ ///
+ /// Checks if the queue is enabled.
+ ///
+ public bool IsEnabled { get; private set; }
+
+ private void UpdateOptions(BatchingLoggerOptions options)
+ {
+ var oldIsEnabled = IsEnabled;
+ IsEnabled = options.IsEnabled;
+ _includeScopes = options.IncludeScopes;
+
+ if (oldIsEnabled != IsEnabled)
+ {
+ if (IsEnabled)
+ {
+ Start();
+ }
+ else
+ {
+ Stop();
+ }
+ }
+
+ }
+
+ internal abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token);
+
+ private async Task ProcessLogQueue()
+ {
+ while (!_cancellationTokenSource.IsCancellationRequested)
+ {
+ var limit = _batchSize ?? int.MaxValue;
+
+ while (limit > 0 && _messageQueue.TryTake(out var message))
+ {
+ _currentBatch.Add(message);
+ limit--;
+ }
+
+ var messagesDropped = Interlocked.Exchange(ref _messagesDropped, 0);
+ if (messagesDropped != 0)
+ {
+ _currentBatch.Add(new LogMessage(DateTimeOffset.Now, $"{messagesDropped} message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this.{Environment.NewLine}"));
+ }
+
+ if (_currentBatch.Count > 0)
+ {
+ try
+ {
+ await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token);
+ }
+ catch
+ {
+ // ignored
+ }
+
+ _currentBatch.Clear();
+ }
+ else
+ {
+ await IntervalAsync(_interval, _cancellationTokenSource.Token);
+ }
+ }
+ }
+
+ ///
+ /// Wait for the given .
+ ///
+ /// The amount of time to wait.
+ /// A that can be used to cancel the delay.
+ /// A which completes when the has passed or the has been canceled.
+ protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
+ {
+ return Task.Delay(interval, cancellationToken);
+ }
+
+ internal void AddMessage(DateTimeOffset timestamp, string message)
+ {
+ if (!_messageQueue.IsAddingCompleted)
+ {
+ try
+ {
+ if (!_messageQueue.TryAdd(new LogMessage(timestamp, message), millisecondsTimeout: 0, cancellationToken: _cancellationTokenSource.Token))
+ {
+ Interlocked.Increment(ref _messagesDropped);
+ }
+ }
+ catch
+ {
+ //cancellation token canceled or CompleteAdding called
+ }
+ }
+ }
+
+ private void Start()
+ {
+ _messageQueue = _queueSize == null ?
+ new BlockingCollection(new ConcurrentQueue()) :
+ new BlockingCollection(new ConcurrentQueue(), _queueSize.Value);
+
+ _cancellationTokenSource = new CancellationTokenSource();
+ _outputTask = Task.Run(ProcessLogQueue);
+ }
+
+ private void Stop()
+ {
+ _cancellationTokenSource.Cancel();
+ _messageQueue.CompleteAdding();
+
+ try
+ {
+ _outputTask.Wait(_interval);
+ }
+ catch (TaskCanceledException)
+ {
+ }
+ catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException)
+ {
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ _optionsChangeToken?.Dispose();
+ if (IsEnabled)
+ {
+ Stop();
+ }
+ }
+
+ ///
+ /// Creates a with the given .
+ ///
+ /// The name of the category to create this logger with.
+ /// The that was created.
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new BatchingLogger(this, categoryName);
+ }
+
+ ///
+ /// Sets the scope on this provider.
+ ///
+ /// Provides the scope.
+ void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider)
+ {
+ _scopeProvider = scopeProvider;
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs b/src/Logging/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs
new file mode 100644
index 0000000000..e9805128b7
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ internal class BlobAppendReferenceWrapper : ICloudAppendBlob
+ {
+ private readonly Uri _fullUri;
+ private readonly HttpClient _client;
+ private readonly Uri _appendUri;
+
+ public BlobAppendReferenceWrapper(string containerUrl, string name, HttpClient client)
+ {
+ var uriBuilder = new UriBuilder(containerUrl);
+ uriBuilder.Path += "/" + name;
+ _fullUri = uriBuilder.Uri;
+
+ AppendBlockQuery(uriBuilder);
+ _appendUri = uriBuilder.Uri;
+ _client = client;
+ }
+
+ ///
+ public async Task AppendAsync(ArraySegment data, CancellationToken cancellationToken)
+ {
+ Task AppendDataAsync()
+ {
+ var message = new HttpRequestMessage(HttpMethod.Put, _appendUri)
+ {
+ Content = new ByteArrayContent(data.Array, data.Offset, data.Count)
+ };
+ AddCommonHeaders(message);
+
+ return _client.SendAsync(message, cancellationToken);
+ }
+
+ var response = await AppendDataAsync();
+
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ {
+ // If no blob exists try creating it
+ var message = new HttpRequestMessage(HttpMethod.Put, _fullUri)
+ {
+ // Set Content-Length to 0 to create "Append Blob"
+ Content = new ByteArrayContent(Array.Empty()),
+ Headers =
+ {
+ { "If-None-Match", "*" }
+ }
+ };
+
+ AddCommonHeaders(message);
+
+ response = await _client.SendAsync(message, cancellationToken);
+
+ // If result is 2** or 412 try to append again
+ if (response.IsSuccessStatusCode ||
+ response.StatusCode == HttpStatusCode.PreconditionFailed)
+ {
+ // Retry sending data after blob creation
+ response = await AppendDataAsync();
+ }
+ }
+
+ response.EnsureSuccessStatusCode();
+ }
+
+ private static void AddCommonHeaders(HttpRequestMessage message)
+ {
+ message.Headers.Add("x-ms-blob-type", "AppendBlob");
+ message.Headers.Add("x-ms-version", "2016-05-31");
+ message.Headers.Date = DateTimeOffset.UtcNow;
+ }
+
+ private static void AppendBlockQuery(UriBuilder uriBuilder)
+ {
+ // See https://msdn.microsoft.com/en-us/library/system.uribuilder.query.aspx for:
+ // Note: Do not append a string directly to Query property.
+ // If the length of Query is greater than 1, retrieve the property value
+ // as a string, remove the leading question mark, append the new query string,
+ // and set the property with the combined string.
+ var queryToAppend = "comp=appendblock";
+ if (uriBuilder.Query != null && uriBuilder.Query.Length > 1)
+ uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + queryToAppend;
+ else
+ uriBuilder.Query = queryToAppend;
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs b/src/Logging/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs
new file mode 100644
index 0000000000..f9a186872b
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class BlobLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly IWebAppContext _context;
+
+ public BlobLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context)
+ : base(configuration, "AzureBlobEnabled")
+ {
+ _configuration = configuration;
+ _context = context;
+ }
+
+ public void Configure(AzureBlobLoggerOptions options)
+ {
+ base.Configure(options);
+ options.ContainerUrl = _configuration.GetSection("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL")?.Value;
+ options.ApplicationName = _context.SiteName;
+ options.ApplicationInstanceId = _context.SiteInstanceId;
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/BlobLoggerProvider.cs b/src/Logging/Logging.AzureAppServices/src/BlobLoggerProvider.cs
new file mode 100644
index 0000000000..3d62ea2ac6
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/BlobLoggerProvider.cs
@@ -0,0 +1,92 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// The implementation that stores messages by appending them to Azure Blob in batches.
+ ///
+ [ProviderAlias("AzureAppServicesBlob")]
+ public class BlobLoggerProvider : BatchingLoggerProvider
+ {
+ private readonly string _appName;
+ private readonly string _fileName;
+ private readonly Func _blobReferenceFactory;
+ private readonly HttpClient _httpClient;
+
+ ///
+ /// Creates a new instance of
+ ///
+ /// The options to use when creating a provider.
+ public BlobLoggerProvider(IOptionsMonitor options)
+ : this(options, null)
+ {
+ _blobReferenceFactory = name => new BlobAppendReferenceWrapper(
+ options.CurrentValue.ContainerUrl,
+ name,
+ _httpClient);
+ }
+
+ ///
+ /// Creates a new instance of
+ ///
+ /// The container to store logs to.
+ /// Options to be used in creating a logger.
+ internal BlobLoggerProvider(
+ IOptionsMonitor options,
+ Func blobReferenceFactory) :
+ base(options)
+ {
+ var value = options.CurrentValue;
+ _appName = value.ApplicationName;
+ _fileName = value.ApplicationInstanceId + "_" + value.BlobName;
+ _blobReferenceFactory = blobReferenceFactory;
+ _httpClient = new HttpClient();
+ }
+
+ internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken)
+ {
+ var eventGroups = messages.GroupBy(GetBlobKey);
+ foreach (var eventGroup in eventGroups)
+ {
+ var key = eventGroup.Key;
+ var blobName = $"{_appName}/{key.Year}/{key.Month:00}/{key.Day:00}/{key.Hour:00}/{_fileName}";
+
+ var blob = _blobReferenceFactory(blobName);
+
+ using (var stream = new MemoryStream())
+ using (var writer = new StreamWriter(stream))
+ {
+ foreach (var logEvent in eventGroup)
+ {
+ writer.Write(logEvent.Message);
+ }
+
+ await writer.FlushAsync();
+ var tryGetBuffer = stream.TryGetBuffer(out var buffer);
+ Debug.Assert(tryGetBuffer);
+ await blob.AppendAsync(buffer, cancellationToken);
+ }
+ }
+ }
+
+ private (int Year, int Month, int Day, int Hour) GetBlobKey(LogMessage e)
+ {
+ return (e.Timestamp.Year,
+ e.Timestamp.Month,
+ e.Timestamp.Day,
+ e.Timestamp.Hour);
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs b/src/Logging/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs
new file mode 100644
index 0000000000..c62ccb2331
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class ConfigurationBasedLevelSwitcher: IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly Type _provider;
+ private readonly string _levelKey;
+
+ public ConfigurationBasedLevelSwitcher(IConfiguration configuration, Type provider, string levelKey)
+ {
+ _configuration = configuration;
+ _provider = provider;
+ _levelKey = levelKey;
+ }
+
+ public void Configure(LoggerFilterOptions options)
+ {
+ options.Rules.Add(new LoggerFilterRule(_provider.FullName, null, GetLogLevel(), null));
+ }
+
+ private LogLevel GetLogLevel()
+ {
+ return TextToLogLevel(_configuration.GetSection(_levelKey)?.Value);
+ }
+
+ private static LogLevel TextToLogLevel(string text)
+ {
+ switch (text?.ToUpperInvariant())
+ {
+ case "ERROR":
+ return LogLevel.Error;
+ case "WARNING":
+ return LogLevel.Warning;
+ case "INFORMATION":
+ return LogLevel.Information;
+ case "VERBOSE":
+ return LogLevel.Trace;
+ default:
+ return LogLevel.None;
+ }
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs b/src/Logging/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs
new file mode 100644
index 0000000000..8cd1f5eb91
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class FileLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions
+ {
+ private readonly IWebAppContext _context;
+
+ public FileLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context)
+ : base(configuration, "AzureDriveEnabled")
+ {
+ _context = context;
+ }
+
+ public void Configure(AzureFileLoggerOptions options)
+ {
+ base.Configure(options);
+ options.LogDirectory = Path.Combine(_context.HomeFolder, "LogFiles", "Application");
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/FileLoggerProvider.cs b/src/Logging/Logging.AzureAppServices/src/FileLoggerProvider.cs
new file mode 100644
index 0000000000..1143d38c07
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/FileLoggerProvider.cs
@@ -0,0 +1,89 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// A which writes out to a file.
+ ///
+ [ProviderAlias("AzureAppServicesFile")]
+ public class FileLoggerProvider : BatchingLoggerProvider
+ {
+ private readonly string _path;
+ private readonly string _fileName;
+ private readonly int? _maxFileSize;
+ private readonly int? _maxRetainedFiles;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The options to use when creating a provider.
+ public FileLoggerProvider(IOptionsMonitor options) : base(options)
+ {
+ var loggerOptions = options.CurrentValue;
+ _path = loggerOptions.LogDirectory;
+ _fileName = loggerOptions.FileName;
+ _maxFileSize = loggerOptions.FileSizeLimit;
+ _maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
+ }
+
+ internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken)
+ {
+ Directory.CreateDirectory(_path);
+
+ foreach (var group in messages.GroupBy(GetGrouping))
+ {
+ var fullName = GetFullName(group.Key);
+ var fileInfo = new FileInfo(fullName);
+ if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize)
+ {
+ return;
+ }
+
+ using (var streamWriter = File.AppendText(fullName))
+ {
+ foreach (var item in group)
+ {
+ await streamWriter.WriteAsync(item.Message);
+ }
+ }
+ }
+
+ RollFiles();
+ }
+
+ private string GetFullName((int Year, int Month, int Day) group)
+ {
+ return Path.Combine(_path, $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}.txt");
+ }
+
+ private (int Year, int Month, int Day) GetGrouping(LogMessage message)
+ {
+ return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day);
+ }
+
+ private void RollFiles()
+ {
+ if (_maxRetainedFiles > 0)
+ {
+ var files = new DirectoryInfo(_path)
+ .GetFiles(_fileName + "*")
+ .OrderByDescending(f => f.Name)
+ .Skip(_maxRetainedFiles.Value);
+
+ foreach (var item in files)
+ {
+ item.Delete();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/ICloudAppendBlob.cs b/src/Logging/Logging.AzureAppServices/src/ICloudAppendBlob.cs
new file mode 100644
index 0000000000..2f55bbb0d1
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/ICloudAppendBlob.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Represents an append blob, a type of blob where blocks of data are always committed to the end of the blob.
+ ///
+ internal 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 AppendAsync(ArraySegment data, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/IWebAppContext.cs b/src/Logging/Logging.AzureAppServices/src/IWebAppContext.cs
new file mode 100644
index 0000000000..f8c826ceb8
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/IWebAppContext.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Represents an Azure WebApp context
+ ///
+ internal interface IWebAppContext
+ {
+ ///
+ /// Gets the path to the home folder if running in Azure WebApp
+ ///
+ string HomeFolder { get; }
+
+ ///
+ /// Gets the name of site if running in Azure WebApp
+ ///
+ 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
+ ///
+ bool IsRunningInAzureWebApp { get; }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/LogMessage.cs b/src/Logging/Logging.AzureAppServices/src/LogMessage.cs
new file mode 100644
index 0000000000..4a1179ceb3
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/LogMessage.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal readonly struct LogMessage
+ {
+ public LogMessage(DateTimeOffset timestamp, string message)
+ {
+ Timestamp = timestamp;
+ Message = message;
+ }
+
+ public DateTimeOffset Timestamp { get; }
+ public string Message { get; }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj b/src/Logging/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj
new file mode 100644
index 0000000000..5bedde8c6d
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Logger implementation to support Azure App Services 'Diagnostics logs' and 'Log stream' features.
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Logging/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs b/src/Logging/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..7c7d332545
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+
+
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/Logging/Logging.AzureAppServices/src/SiteConfigurationProvider.cs b/src/Logging/Logging.AzureAppServices/src/SiteConfigurationProvider.cs
new file mode 100644
index 0000000000..452c936f93
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/SiteConfigurationProvider.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using Microsoft.Extensions.Configuration;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class SiteConfigurationProvider
+ {
+ public static IConfiguration GetAzureLoggingConfiguration(IWebAppContext context)
+ {
+ var settingsFolder = Path.Combine(context.HomeFolder, "site", "diagnostics");
+ var settingsFile = Path.Combine(settingsFolder, "settings.json");
+
+ return new ConfigurationBuilder()
+ .AddEnvironmentVariables()
+ .AddJsonFile(settingsFile, optional: true, reloadOnChange: true)
+ .Build();
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/src/WebAppContext.cs b/src/Logging/Logging.AzureAppServices/src/WebAppContext.cs
new file mode 100644
index 0000000000..8bdd3f1c76
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/src/WebAppContext.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Represents the default implementation of .
+ ///
+ internal class WebAppContext : IWebAppContext
+ {
+ ///
+ /// Gets the default instance of the WebApp context.
+ ///
+ public static WebAppContext Default { get; } = new WebAppContext();
+
+ private WebAppContext() { }
+
+ ///
+ public string HomeFolder { get; } = Environment.GetEnvironmentVariable("HOME");
+
+ ///
+ 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/Logging/Logging.AzureAppServices/test/AzureAppendBlobTests.cs b/src/Logging/Logging.AzureAppServices/test/AzureAppendBlobTests.cs
new file mode 100644
index 0000000000..2fd5955e86
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/test/AzureAppendBlobTests.cs
@@ -0,0 +1,186 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class AzureAppendBlobTests
+ {
+ public string _containerUrl = "https://host/container?query=1";
+ public string _blobName = "blob/path";
+
+ [Fact]
+ public async Task SendsDataAsStream()
+ {
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ AssertDefaultHeaders(message);
+
+ return new HttpResponseMessage(HttpStatusCode.OK);
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None);
+ }
+
+ private static void AssertDefaultHeaders(HttpRequestMessage message)
+ {
+ Assert.Equal(new[] {"AppendBlob"}, message.Headers.GetValues("x-ms-blob-type"));
+ Assert.Equal(new[] {"2016-05-31"}, message.Headers.GetValues("x-ms-version"));
+ Assert.NotNull(message.Headers.Date);
+ }
+
+ [Theory]
+ [InlineData(HttpStatusCode.Created)]
+ [InlineData(HttpStatusCode.PreconditionFailed)]
+ public async Task CreatesBlobIfNotExist(HttpStatusCode createStatusCode)
+ {
+ var stage = 0;
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ // First PUT request
+ if (stage == 0)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ }
+ // Create request
+ if (stage == 1)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString());
+ Assert.Equal(0, message.Content.Headers.ContentLength);
+ Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match"));
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(createStatusCode);
+ }
+ // First PUT request
+ if (stage == 2)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.Created);
+ }
+ throw new NotImplementedException();
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None);
+
+ Assert.Equal(3, stage);
+ }
+
+ [Fact]
+ public async Task ThrowsForUnknownStatus()
+ {
+ var stage = 0;
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ // First PUT request
+ if (stage == 0)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.InternalServerError);
+ }
+
+ throw new NotImplementedException();
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None));
+
+ Assert.Equal(1, stage);
+ }
+
+ [Fact]
+ public async Task ThrowsForUnknownStatusDuringCreation()
+ {
+ var stage = 0;
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ // First PUT request
+ if (stage == 0)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ }
+ // Create request
+ if (stage == 1)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString());
+ Assert.Equal(0, message.Content.Headers.ContentLength);
+ Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match"));
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.InternalServerError);
+ }
+
+ throw new NotImplementedException();
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None));
+
+ Assert.Equal(2, stage);
+ }
+
+
+ private class TestMessageHandler : HttpMessageHandler
+ {
+ private readonly Func> _callback;
+
+ public TestMessageHandler(Func> callback)
+ {
+ _callback = callback;
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return await _callback(request);
+ }
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/test/AzureBlobSinkTests.cs b/src/Logging/Logging.AzureAppServices/test/AzureBlobSinkTests.cs
new file mode 100644
index 0000000000..4d9125335a
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/test/AzureBlobSinkTests.cs
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class AzureBlobSinkTests
+ {
+ DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
+
+ [Fact]
+ public async Task WritesMessagesInBatches()
+ {
+ var blob = new Mock();
+ var buffers = new List();
+ blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny()))
+ .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s)))
+ .Returns(Task.CompletedTask);
+
+ var sink = new TestBlobSink(name => blob.Object);
+ var logger = (BatchingLogger)sink.CreateLogger("Cat");
+
+ await sink.IntervalControl.Pause;
+
+ for (int i = 0; i < 5; i++)
+ {
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state);
+ }
+
+ sink.IntervalControl.Resume();
+ await sink.IntervalControl.Pause;
+
+ Assert.Single(buffers);
+ Assert.Equal(
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 0" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 1" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 2" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 3" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 4" + Environment.NewLine,
+ Encoding.UTF8.GetString(buffers[0]));
+ }
+
+ [Fact]
+ public async Task GroupsByHour()
+ {
+ var blob = new Mock();
+ var buffers = new List();
+ var names = new List();
+
+ blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny()))
+ .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s)))
+ .Returns(Task.CompletedTask);
+
+ var sink = new TestBlobSink(name =>
+ {
+ names.Add(name);
+ return blob.Object;
+ });
+ var logger = (BatchingLogger)sink.CreateLogger("Cat");
+
+ await sink.IntervalControl.Pause;
+
+ var startDate = _timestampOne;
+ for (int i = 0; i < 3; i++)
+ {
+ logger.Log(startDate, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state);
+
+ startDate = startDate.AddHours(1);
+ }
+
+ sink.IntervalControl.Resume();
+ await sink.IntervalControl.Pause;
+
+ Assert.Equal(3, buffers.Count);
+
+ Assert.Equal("appname/2016/05/04/03/42_filename", names[0]);
+ Assert.Equal("appname/2016/05/04/04/42_filename", names[1]);
+ Assert.Equal("appname/2016/05/04/05/42_filename", names[2]);
+ }
+
+ private byte[] ToArray(ArraySegment inputStream)
+ {
+ return inputStream.Array
+ .Skip(inputStream.Offset)
+ .Take(inputStream.Count)
+ .ToArray();
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs b/src/Logging/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs
new file mode 100644
index 0000000000..00d7dcd58d
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs
@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class AzureDiagnosticsConfigurationProviderTests
+ {
+ [Fact]
+ public void NoConfigFile()
+ {
+ var tempFolder = Path.Combine(Path.GetTempPath(), "AzureWebAppLoggerThisFolderShouldNotExist");
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder)
+ .Returns(tempFolder);
+
+ var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object);
+
+ Assert.NotNull(config);
+ }
+
+ [Fact]
+ public void ReadsSettingsFileAndEnvironment()
+ {
+ var tempFolder = Path.Combine(Path.GetTempPath(), "WebAppLoggerConfigurationDisabledInSettingsFile");
+
+ try
+ {
+ var settingsFolder = Path.Combine(tempFolder, "site", "diagnostics");
+ var settingsFile = Path.Combine(settingsFolder, "settings.json");
+
+ if (!Directory.Exists(settingsFolder))
+ {
+ Directory.CreateDirectory(settingsFolder);
+ }
+ Environment.SetEnvironmentVariable("RANDOM_ENVIRONMENT_VARIABLE", "USEFUL_VALUE");
+ File.WriteAllText(settingsFile, @"{ ""key"":""test value"" }");
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder)
+ .Returns(tempFolder);
+
+ var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object);
+
+ Assert.Equal("test value", config["key"]);
+ Assert.Equal("USEFUL_VALUE", config["RANDOM_ENVIRONMENT_VARIABLE"]);
+ }
+ finally
+ {
+ if (Directory.Exists(tempFolder))
+ {
+ try
+ {
+ Directory.Delete(tempFolder, recursive: true);
+ }
+ catch
+ {
+ // Don't break the test if temp folder deletion fails.
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs b/src/Logging/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs
new file mode 100644
index 0000000000..9ab0c0cb45
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class BatchingLoggerProviderTests
+ {
+ private DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
+ private string _nl = Environment.NewLine;
+ private Regex _timeStampRegex = new Regex(@"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} .\d{2}:\d{2} ");
+
+ [Fact]
+ public async Task LogsInIntervals()
+ {
+ var provider = new TestBatchingLoggingProvider();
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
+ Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[0][1].Message);
+ }
+
+ [Fact]
+ public async Task IncludesScopes()
+ {
+ var provider = new TestBatchingLoggingProvider(includeScopes: true);
+ var factory = new LoggerFactory(new [] { provider });
+ var logger = factory.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ using (logger.BeginScope("Scope"))
+ {
+ using (logger.BeginScope("Scope2"))
+ {
+ logger.Log(LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ }
+ }
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Matches(_timeStampRegex, provider.Batches[0][0].Message);
+ Assert.EndsWith(
+ " [Information] Cat => Scope => Scope2:" + _nl +
+ "Info message" + _nl,
+ provider.Batches[0][0].Message);
+ }
+
+ [Fact]
+ public async Task RespectsBatchSize()
+ {
+ var provider = new TestBatchingLoggingProvider(maxBatchSize: 1);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(2, provider.Batches.Count);
+ Assert.Single(provider.Batches[0]);
+ Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
+
+ Assert.Single(provider.Batches[1]);
+ Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[1][0].Message);
+ }
+
+ [Fact]
+ public async Task DropsMessagesWhenReachingMaxQueue()
+ {
+ var provider = new TestBatchingLoggingProvider(maxQueueSize: 1);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(2, provider.Batches[0].Length);
+ Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
+ Assert.Equal("1 message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this." + _nl, provider.Batches[0][1].Message);
+ }
+
+ private class TestBatchingLoggingProvider: BatchingLoggerProvider
+ {
+ public List Batches { get; } = new List();
+ public ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl();
+
+ public TestBatchingLoggingProvider(TimeSpan? interval = null, int? maxBatchSize = null, int? maxQueueSize = null, bool includeScopes = false)
+ : base(new OptionsWrapperMonitor(new BatchingLoggerOptions
+ {
+ FlushPeriod = interval ?? TimeSpan.FromSeconds(1),
+ BatchSize = maxBatchSize,
+ BackgroundQueueSize = maxQueueSize,
+ IsEnabled = true,
+ IncludeScopes = includeScopes
+ }))
+ {
+ }
+
+ internal override Task WriteMessagesAsync(IEnumerable messages, CancellationToken token)
+ {
+ Batches.Add(messages.ToArray());
+ return Task.CompletedTask;
+ }
+
+ protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
+ {
+ return IntervalControl.IntervalAsync();
+ }
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/test/ConfigureOptionsTests.cs b/src/Logging/Logging.AzureAppServices/test/ConfigureOptionsTests.cs
new file mode 100644
index 0000000000..46b72c7a0d
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/test/ConfigureOptionsTests.cs
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Extensions.Configuration;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class ConfigureOptionsTests
+ {
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ [InlineData(null)]
+ public void InitializesIsEnabled(bool? enabled)
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
+ {
+ new KeyValuePair("IsEnabledKey", Convert.ToString(enabled))
+ }).Build();
+
+ var options = new BatchingLoggerOptions();
+ new BatchLoggerConfigureOptions(configuration, "IsEnabledKey").Configure(options);
+
+ Assert.Equal(enabled ?? false, options.IsEnabled);
+ }
+
+ [Fact]
+ public void InitializesLogDirectory()
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
+ {
+ new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
+ }).Build();
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder).Returns("Home");
+
+ var options = new AzureFileLoggerOptions();
+ new FileLoggerConfigureOptions(configuration, contextMock.Object).Configure(options);
+
+ Assert.Equal(Path.Combine("Home", "LogFiles", "Application"), options.LogDirectory);
+ }
+
+ [Fact]
+ public void InitializesBlobUriSiteInstanceAndName()
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new []
+ {
+ new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
+ }).Build();
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder).Returns("Home");
+ contextMock.SetupGet(c => c.SiteInstanceId).Returns("InstanceId");
+ contextMock.SetupGet(c => c.SiteName).Returns("Name");
+
+ var options = new AzureBlobLoggerOptions();
+ new BlobLoggerConfigureOptions(configuration, contextMock.Object).Configure(options);
+
+ Assert.Equal("http://container/url", options.ContainerUrl);
+ Assert.Equal("InstanceId", options.ApplicationInstanceId);
+ Assert.Equal("Name", options.ApplicationName);
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/test/FileLoggerTests.cs b/src/Logging/Logging.AzureAppServices/test/FileLoggerTests.cs
new file mode 100644
index 0000000000..a3fcd2587d
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/test/FileLoggerTests.cs
@@ -0,0 +1,122 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class FileLoggerTests: IDisposable
+ {
+ DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
+
+ public FileLoggerTests()
+ {
+ TempPath = Path.GetTempFileName() + "_";
+ }
+
+ public string TempPath { get; }
+
+ public void Dispose()
+ {
+ try
+ {
+ if (Directory.Exists(TempPath))
+ {
+ Directory.Delete(TempPath, true);
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ [Fact]
+ public async Task WritesToTextFile()
+ {
+ var provider = new TestFileLoggerProvider(TempPath);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine +
+ "2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine,
+ File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt")));
+ }
+
+ [Fact]
+ public async Task RollsTextFile()
+ {
+ var provider = new TestFileLoggerProvider(TempPath);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddDays(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine,
+ File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt")));
+
+ Assert.Equal(
+ "2016-05-05 03:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine,
+ File.ReadAllText(Path.Combine(TempPath, "LogFile.20160505.txt")));
+ }
+
+ [Fact]
+ public async Task RespectsMaxFileCount()
+ {
+ Directory.CreateDirectory(TempPath);
+ File.WriteAllText(Path.Combine(TempPath, "randomFile.txt"), "Text");
+
+ var provider = new TestFileLoggerProvider(TempPath, maxRetainedFiles: 5);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+ var timestamp = _timestampOne;
+
+ for (int i = 0; i < 10; i++)
+ {
+ logger.Log(timestamp, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(timestamp.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ timestamp = timestamp.AddDays(1);
+ }
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ var actualFiles = new DirectoryInfo(TempPath)
+ .GetFiles()
+ .Select(f => f.Name)
+ .OrderBy(f => f)
+ .ToArray();
+
+ Assert.Equal(6, actualFiles.Length);
+ Assert.Equal(new[] {
+ "LogFile.20160509.txt",
+ "LogFile.20160510.txt",
+ "LogFile.20160511.txt",
+ "LogFile.20160512.txt",
+ "LogFile.20160513.txt",
+ "randomFile.txt"
+ }, actualFiles);
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs b/src/Logging/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs
new file mode 100644
index 0000000000..cf8bede1a5
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class LoggerBuilderExtensionsTests
+ {
+ private IWebAppContext _appContext;
+
+ public LoggerBuilderExtensionsTests()
+ {
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.IsRunningInAzureWebApp).Returns(true);
+ contextMock.SetupGet(c => c.HomeFolder).Returns(".");
+ _appContext = contextMock.Object;
+ }
+
+ [Fact]
+ public void BuilderExtensionAddsSingleSetOfServicesWhenCalledTwice()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+ var count = serviceCollection.Count;
+
+ Assert.NotEqual(0, count);
+
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ Assert.Equal(count, serviceCollection.Count);
+ }
+
+ [Fact]
+ public void BuilderExtensionAddsConfigurationChangeTokenSource()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build()));
+
+ // Tracking for main configuration
+ Assert.Equal(1, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource)));
+
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ // Make sure we add another config change token for azure diagnostic configuration
+ Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource)));
+ }
+
+ [Fact]
+ public void BuilderExtensionAddsIConfigureOptions()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build()));
+
+ // Tracking for main configuration
+ Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions)));
+
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ Assert.Equal(4, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions)));
+ }
+
+ [Fact]
+ public void LoggerProviderIsResolvable()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+ var loggerFactory = serviceProvider.GetService();
+ }
+ }
+}
diff --git a/src/Logging/Logging.AzureAppServices/test/ManualIntervalControl.cs b/src/Logging/Logging.AzureAppServices/test/ManualIntervalControl.cs
new file mode 100644
index 0000000000..29cc883a28
--- /dev/null
+++ b/src/Logging/Logging.AzureAppServices/test/ManualIntervalControl.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ internal class ManualIntervalControl
+ {
+
+ private TaskCompletionSource