Reorganize source code in preparation to move into aspnet/Extensions

Prior to reorganization, this source code was found in 8270c54522
This commit is contained in:
Nate McMaster 2018-11-06 13:11:45 -08:00
commit 018907bec0
79 changed files with 6255 additions and 0 deletions

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<IsProductComponent>true</IsProductComponent>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,161 @@
// 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.AzureAppServices
{
/// <summary>
/// Settings for Azure diagnostics logging.
/// </summary>
public class AzureAppServicesDiagnosticsSettings
{
private TimeSpan _blobCommitPeriod = TimeSpan.FromSeconds(5);
private int _blobBatchSize = 32;
private string _outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}";
private int _retainedFileCountLimit = 2;
private int _fileSizeLimit = 10 * 1024 * 1024;
private string _blobName = "applicationLog.txt";
private TimeSpan? _fileFlushPeriod = TimeSpan.FromSeconds(1);
private int _backgroundQueueSize;
/// <summary>
/// Gets or sets a strictly positive value representing the maximum log size in bytes.
/// Once the log is full, no more messages will be appended.
/// Defaults to <c>10MB</c>.
/// </summary>
public int FileSizeLimit
{
get { return _fileSizeLimit; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileSizeLimit)} must be positive.");
}
_fileSizeLimit = value;
}
}
/// <summary>
/// Gets or sets a strictly positive value representing the maximum retained file count.
/// Defaults to <c>2</c>.
/// </summary>
public int RetainedFileCountLimit
{
get { return _retainedFileCountLimit; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive.");
}
_retainedFileCountLimit = value;
}
}
/// <summary>
/// Gets or sets a message template describing the output messages.
/// Defaults to <c>"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}"</c>.
/// </summary>
public string OutputTemplate
{
get { return _outputTemplate; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(nameof(value), $"{nameof(OutputTemplate)} must be non-empty string.");
}
_outputTemplate = value;
}
}
/// <summary>
/// Gets or sets a maximum number of events to include in a single blob append batch.
/// Defaults to <c>32</c>.
/// </summary>
public int BlobBatchSize
{
get { return _blobBatchSize; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BlobBatchSize)} must be positive.");
}
_blobBatchSize = value;
}
}
/// <summary>
/// Gets or sets a time to wait between checking for blob log batches.
/// Defaults to 5 seconds.
/// </summary>
public TimeSpan BlobCommitPeriod
{
get { return _blobCommitPeriod; }
set
{
if (value < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BlobCommitPeriod)} must be positive.");
}
_blobCommitPeriod = value;
}
}
/// <summary>
/// Gets or sets the last section of log blob name.
/// Defaults to <c>"applicationLog.txt"</c>.
/// </summary>
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;
}
}
/// <summary>
/// Gets or sets the maximum size of the background log message queue or 0 for no limit.
/// After maximum queue size is reached log event sink would start blocking.
/// Defaults to <c>0</c>.
/// </summary>
public int BackgroundQueueSize
{
get { return _backgroundQueueSize; }
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BackgroundQueueSize)} must be non-negative.");
}
_backgroundQueueSize = value;
}
}
/// <summary>
/// Gets or sets the period after which logs will be flushed to disk or
/// <c>null</c> if auto flushing is not required.
/// Defaults to 1 second.
/// </summary>
public TimeSpan? FileFlushPeriod
{
get { return _fileFlushPeriod; }
set
{
if (value < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileFlushPeriod)} must be positive.");
}
_fileFlushPeriod = value;
}
}
}
}

View File

@ -0,0 +1,189 @@
// 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.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.AzureAppServices;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
using Microsoft.Extensions.Options;
using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
namespace Microsoft.Extensions.Logging
{
/// <summary>
/// Extension methods for adding Azure diagnostics logger.
/// </summary>
public static class AzureAppServicesLoggerFactoryExtensions
{
/// <summary>
/// Adds an Azure Web Apps diagnostics logger.
/// </summary>
/// <param name="builder">The extension method argument</param>
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;
}
var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(context);
var services = builder.Services;
var addedFileLogger = TryAddEnumerable(services, Singleton<ILoggerProvider, FileLoggerProvider>());
var addedBlobLogger = TryAddEnumerable(services, Singleton<ILoggerProvider, BlobLoggerProvider>());
if (addedFileLogger || addedBlobLogger)
{
services.AddSingleton(context);
services.AddSingleton<IOptionsChangeTokenSource<LoggerFilterOptions>>(
new ConfigurationChangeTokenSource<LoggerFilterOptions>(config));
}
if (addedFileLogger)
{
services.AddSingleton<IConfigureOptions<LoggerFilterOptions>>(CreateFileFilterConfigureOptions(config));
services.AddSingleton<IConfigureOptions<AzureFileLoggerOptions>>(new FileLoggerConfigureOptions(config, context));
services.AddSingleton<IOptionsChangeTokenSource<AzureFileLoggerOptions>>(
new ConfigurationChangeTokenSource<AzureFileLoggerOptions>(config));
}
if (addedBlobLogger)
{
services.AddSingleton<IConfigureOptions<LoggerFilterOptions>>(CreateBlobFilterConfigureOptions(config));
services.AddSingleton<IConfigureOptions<AzureBlobLoggerOptions>>(new BlobLoggerConfigureOptions(config, context));
services.AddSingleton<IOptionsChangeTokenSource<AzureBlobLoggerOptions>>(
new ConfigurationChangeTokenSource<AzureBlobLoggerOptions>(config));
}
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");
}
/// <summary>
/// Adds an Azure Web Apps diagnostics logger.
/// </summary>
/// <param name="factory">The extension method argument</param>
public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory)
{
return AddAzureWebAppDiagnostics(factory, new AzureAppServicesDiagnosticsSettings());
}
/// <summary>
/// Adds an Azure Web Apps diagnostics logger.
/// </summary>
/// <param name="factory">The extension method argument</param>
/// <param name="settings">The setting object to configure loggers.</param>
public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory, AzureAppServicesDiagnosticsSettings settings)
{
var context = WebAppContext.Default;
if (!context.IsRunningInAzureWebApp)
{
return factory;
}
var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(context);
// Only add the provider if we're in Azure WebApp. That cannot change once the apps started
var fileOptions = new OptionsMonitor<AzureFileLoggerOptions>(
new OptionsFactory<AzureFileLoggerOptions>(
new IConfigureOptions<AzureFileLoggerOptions>[]
{
new FileLoggerConfigureOptions(config, context),
new ConfigureOptions<AzureFileLoggerOptions>(options =>
{
options.FileSizeLimit = settings.FileSizeLimit;
options.RetainedFileCountLimit = settings.RetainedFileCountLimit;
options.BackgroundQueueSize = settings.BackgroundQueueSize == 0 ? (int?) null : settings.BackgroundQueueSize;
if (settings.FileFlushPeriod != null)
{
options.FlushPeriod = settings.FileFlushPeriod.Value;
}
})
},
new IPostConfigureOptions<AzureFileLoggerOptions>[0]
),
new[]
{
new ConfigurationChangeTokenSource<AzureFileLoggerOptions>(config)
},
new OptionsCache<AzureFileLoggerOptions>()
);
var blobOptions = new OptionsMonitor<AzureBlobLoggerOptions>(
new OptionsFactory<AzureBlobLoggerOptions>(
new IConfigureOptions<AzureBlobLoggerOptions>[] {
new BlobLoggerConfigureOptions(config, context),
new ConfigureOptions<AzureBlobLoggerOptions>(options =>
{
options.BlobName = settings.BlobName;
options.FlushPeriod = settings.BlobCommitPeriod;
options.BatchSize = settings.BlobBatchSize;
options.BackgroundQueueSize = settings.BackgroundQueueSize == 0 ? (int?) null : settings.BackgroundQueueSize;
})
},
new IPostConfigureOptions<AzureBlobLoggerOptions>[0]
),
new[]
{
new ConfigurationChangeTokenSource<AzureBlobLoggerOptions>(config)
},
new OptionsCache<AzureBlobLoggerOptions>()
);
var filterOptions = new OptionsMonitor<LoggerFilterOptions>(
new OptionsFactory<LoggerFilterOptions>(
new[]
{
CreateFileFilterConfigureOptions(config),
CreateBlobFilterConfigureOptions(config)
},
new IPostConfigureOptions<LoggerFilterOptions>[0]),
new [] { new ConfigurationChangeTokenSource<LoggerFilterOptions>(config) },
new OptionsCache<LoggerFilterOptions>());
factory.AddProvider(new ForwardingLoggerProvider(
new LoggerFactory(
new ILoggerProvider[]
{
new FileLoggerProvider(fileOptions),
new BlobLoggerProvider(blobOptions)
},
filterOptions
)
));
return factory;
}
}
}

View File

@ -0,0 +1,44 @@
// 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 Microsoft.Extensions.Logging.AzureAppServices.Internal;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <summary>
/// Options for Azure diagnostics blob logging.
/// </summary>
public class AzureBlobLoggerOptions: BatchingLoggerOptions
{
public AzureBlobLoggerOptions()
{
BatchSize = 32;
}
private string _blobName = "applicationLog.txt";
/// <summary>
/// Gets or sets the last section of log blob name.
/// Defaults to <c>"applicationLog.txt"</c>.
/// </summary>
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; }
}
}

View File

@ -0,0 +1,72 @@
// 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 Microsoft.Extensions.Logging.AzureAppServices.Internal;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <summary>
/// Options for Azure diagnostics file logging.
/// </summary>
public class AzureFileLoggerOptions: BatchingLoggerOptions
{
private int? _fileSizeLimit = 10 * 1024 * 1024;
private int? _retainedFileCountLimit = 2;
private string _fileName = "diagnostics-";
/// <summary>
/// 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 <c>10MB</c>.
/// </summary>
public int? FileSizeLimit
{
get { return _fileSizeLimit; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileSizeLimit)} must be positive.");
}
_fileSizeLimit = value;
}
}
/// <summary>
/// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit.
/// Defaults to <c>2</c>.
/// </summary>
public int? RetainedFileCountLimit
{
get { return _retainedFileCountLimit; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive.");
}
_retainedFileCountLimit = value;
}
}
/// <summary>
/// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit.
/// Defaults to <c>2</c>.
/// </summary>
public string FileName
{
get { return _fileName; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(nameof(value));
}
_fileName = value;
}
}
internal string LogDirectory { get; set; }
}
}

View File

@ -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 Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
public class BatchLoggerConfigureOptions : IConfigureOptions<BatchingLoggerOptions>
{
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;
}
}
}

View File

@ -0,0 +1,59 @@
// 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.Text;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
public class BatchingLogger : ILogger
{
private readonly BatchingLoggerProvider _provider;
private readonly string _category;
public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName)
{
_provider = loggerProvider;
_category = categoryName;
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return _provider.IsEnabled;
}
public void Log<TState>(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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);
builder.Append(": ");
builder.AppendLine(formatter(state, exception));
if (exception != null)
{
builder.AppendLine(exception.ToString());
}
_provider.AddMessage(timestamp, builder.ToString());
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter);
}
}
}

View File

@ -0,0 +1,69 @@
// 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.AzureAppServices.Internal
{
public class BatchingLoggerOptions
{
private int? _batchSize = 32;
private int? _backgroundQueueSize;
private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1);
/// <summary>
/// Gets or sets the period after which logs will be flushed to the store.
/// </summary>
public TimeSpan FlushPeriod
{
get { return _flushPeriod; }
set
{
if (value <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FlushPeriod)} must be positive.");
}
_flushPeriod = value;
}
}
/// <summary>
/// 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 <c>null</c>.
/// </summary>
public int? BackgroundQueueSize
{
get { return _backgroundQueueSize; }
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BackgroundQueueSize)} must be non-negative.");
}
_backgroundQueueSize = value;
}
}
/// <summary>
/// Gets or sets a maximum number of events to include in a single batch or null for no limit.
/// </summary>
public int? BatchSize
{
get { return _batchSize; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BatchSize)} must be positive.");
}
_batchSize = value;
}
}
/// <summary>
/// Gets or sets value indicating if logger accepts and queues writes.
/// </summary>
public bool IsEnabled { get; set; }
}
}

View File

@ -0,0 +1,163 @@
// 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.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
public abstract class BatchingLoggerProvider: ILoggerProvider
{
private readonly List<LogMessage> _currentBatch = new List<LogMessage>();
private readonly TimeSpan _interval;
private readonly int? _queueSize;
private readonly int? _batchSize;
private readonly IDisposable _optionsChangeToken;
private BlockingCollection<LogMessage> _messageQueue;
private Task _outputTask;
private CancellationTokenSource _cancellationTokenSource;
protected BatchingLoggerProvider(IOptionsMonitor<BatchingLoggerOptions> 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);
}
public bool IsEnabled { get; private set; }
private void UpdateOptions(BatchingLoggerOptions options)
{
var oldIsEnabled = IsEnabled;
IsEnabled = options.IsEnabled;
if (oldIsEnabled != IsEnabled)
{
if (IsEnabled)
{
Start();
}
else
{
Stop();
}
}
}
protected abstract Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token);
private async Task ProcessLogQueue(object state)
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
var limit = _batchSize ?? int.MaxValue;
while (limit > 0 && _messageQueue.TryTake(out var message))
{
_currentBatch.Add(message);
limit--;
}
if (_currentBatch.Count > 0)
{
try
{
await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token);
}
catch
{
// ignored
}
_currentBatch.Clear();
}
await IntervalAsync(_interval, _cancellationTokenSource.Token);
}
}
protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return Task.Delay(interval, cancellationToken);
}
internal void AddMessage(DateTimeOffset timestamp, string message)
{
if (!_messageQueue.IsAddingCompleted)
{
try
{
_messageQueue.Add(new LogMessage { Message = message, Timestamp = timestamp }, _cancellationTokenSource.Token);
}
catch
{
//cancellation token canceled or CompleteAdding called
}
}
}
private void Start()
{
_messageQueue = _queueSize == null ?
new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>()) :
new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>(), _queueSize.Value);
_cancellationTokenSource = new CancellationTokenSource();
_outputTask = Task.Factory.StartNew<Task>(
ProcessLogQueue,
null,
TaskCreationOptions.LongRunning);
}
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();
}
}
public ILogger CreateLogger(string categoryName)
{
return new BatchingLogger(this, categoryName);
}
}
}

View File

@ -0,0 +1,96 @@
// 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.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
/// <inheritdoc />
public 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;
}
/// <inheritdoc />
public async Task AppendAsync(ArraySegment<byte> data, CancellationToken cancellationToken)
{
Task<HttpResponseMessage> 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<byte>()),
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;
}
}
}

View File

@ -0,0 +1,29 @@
// 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.Configuration;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
public class BlobLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions<AzureBlobLoggerOptions>
{
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;
}
}
}

View File

@ -0,0 +1,91 @@
// 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.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.Internal
{
/// <summary>
/// The <see cref="ILoggerProvider"/> implementation that stores messages by appending them to Azure Blob in batches.
/// </summary>
[ProviderAlias("AzureAppServicesBlob")]
public class BlobLoggerProvider : BatchingLoggerProvider
{
private readonly string _appName;
private readonly string _fileName;
private readonly Func<string, ICloudAppendBlob> _blobReferenceFactory;
private readonly HttpClient _httpClient;
/// <summary>
/// Creates a new instance of <see cref="BlobLoggerProvider"/>
/// </summary>
/// <param name="options"></param>
public BlobLoggerProvider(IOptionsMonitor<AzureBlobLoggerOptions> options)
: this(options, null)
{
_blobReferenceFactory = name => new BlobAppendReferenceWrapper(
options.CurrentValue.ContainerUrl,
name,
_httpClient);
}
/// <summary>
/// Creates a new instance of <see cref="BlobLoggerProvider"/>
/// </summary>
/// <param name="blobReferenceFactory">The container to store logs to.</param>
/// <param name="options"></param>
public BlobLoggerProvider(
IOptionsMonitor<AzureBlobLoggerOptions> options,
Func<string, ICloudAppendBlob> blobReferenceFactory) :
base(options)
{
var value = options.CurrentValue;
_appName = value.ApplicationName;
_fileName = value.ApplicationInstanceId + "_" + value.BlobName;
_blobReferenceFactory = blobReferenceFactory;
_httpClient = new HttpClient();
}
protected override async Task WriteMessagesAsync(IEnumerable<LogMessage> 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);
}
}
}

View File

@ -0,0 +1,50 @@
// 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 Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
public class ConfigurationBasedLevelSwitcher: IConfigureOptions<LoggerFilterOptions>
{
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;
}
}
}
}

View File

@ -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 Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
public class FileLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions<AzureFileLoggerOptions>
{
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");
}
}
}

View File

@ -0,0 +1,81 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
[ProviderAlias("AzureAppServicesFile")]
public class FileLoggerProvider : BatchingLoggerProvider
{
private readonly string _path;
private readonly string _fileName;
private readonly int? _maxFileSize;
private readonly int? _maxRetainedFiles;
public FileLoggerProvider(IOptionsMonitor<AzureFileLoggerOptions> options) : base(options)
{
var loggerOptions = options.CurrentValue;
_path = loggerOptions.LogDirectory;
_fileName = loggerOptions.FileName;
_maxFileSize = loggerOptions.FileSizeLimit;
_maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
}
protected override async Task WriteMessagesAsync(IEnumerable<LogMessage> 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");
}
public (int Year, int Month, int Day) GetGrouping(LogMessage message)
{
return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day);
}
protected 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();
}
}
}
}
}

View File

@ -0,0 +1,25 @@
// 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.
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
internal class ForwardingLoggerProvider : ILoggerProvider
{
private readonly ILoggerFactory _loggerFactory;
public ForwardingLoggerProvider(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public void Dispose()
{
_loggerFactory.Dispose();
}
public ILogger CreateLogger(string categoryName)
{
return _loggerFactory.CreateLogger(categoryName);
}
}
}

View File

@ -0,0 +1,22 @@
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
/// <summary>
/// Represents an append blob, a type of blob where blocks of data are always committed to the end of the blob.
/// </summary>
public interface ICloudAppendBlob
{
/// <summary>
/// Initiates an asynchronous operation to open a stream for writing to the blob.
/// </summary>
/// <returns>A <see cref="T:System.Threading.Tasks.Task`1" /> object of type <see cref="Stream" /> that represents the asynchronous operation.</returns>
Task AppendAsync(ArraySegment<byte> data, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,31 @@
// 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.
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
/// <summary>
/// Represents an Azure WebApp context
/// </summary>
public interface IWebAppContext
{
/// <summary>
/// Gets the path to the home folder if running in Azure WebApp
/// </summary>
string HomeFolder { get; }
/// <summary>
/// Gets the name of site if running in Azure WebApp
/// </summary>
string SiteName { get; }
/// <summary>
/// Gets the id of site if running in Azure WebApp
/// </summary>
string SiteInstanceId { get; }
/// <summary>
/// Gets a value indicating whether or new we're in an Azure WebApp
/// </summary>
bool IsRunningInAzureWebApp { get; }
}
}

View File

@ -0,0 +1,13 @@
// 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.AzureAppServices.Internal
{
public struct LogMessage
{
public DateTimeOffset Timestamp { get; set; }
public string Message { get; set; }
}
}

View File

@ -0,0 +1,22 @@
// 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 Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.Logging.AzureAppServices.Internal
{
public 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();
}
}
}

View File

@ -0,0 +1,33 @@
// 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.AzureAppServices.Internal
{
/// <summary>
/// Represents the default implementation of <see cref="IWebAppContext"/>.
/// </summary>
public class WebAppContext : IWebAppContext
{
/// <summary>
/// Gets the default instance of the WebApp context.
/// </summary>
public static WebAppContext Default { get; } = new WebAppContext();
private WebAppContext() { }
/// <inheritdoc />
public string HomeFolder { get; } = Environment.GetEnvironmentVariable("HOME");
/// <inheritdoc />
public string SiteName { get; } = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME");
/// <inheritdoc />
public string SiteInstanceId { get; } = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID");
/// <inheritdoc />
public bool IsRunningInAzureWebApp => !string.IsNullOrEmpty(HomeFolder) &&
!string.IsNullOrEmpty(SiteName);
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Logger implementation to support Azure App Services 'Diagnostics logs' and 'Log stream' features.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<Reference Include="Microsoft.Extensions.Configuration.Json" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Logging.Configuration" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="System.ValueTuple" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
// 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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.Extensions.Logging.AzureAppServices.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,368 @@
{
"AssemblyIdentity": "Microsoft.Extensions.Logging.AzureAppServices, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
"Types": [
{
"Name": "Microsoft.Extensions.Logging.AzureAppServicesLoggerFactoryExtensions",
"Visibility": "Public",
"Kind": "Class",
"Abstract": true,
"Static": true,
"Sealed": true,
"ImplementedInterfaces": [],
"Members": [
{
"Kind": "Method",
"Name": "AddAzureWebAppDiagnostics",
"Parameters": [
{
"Name": "builder",
"Type": "Microsoft.Extensions.Logging.ILoggingBuilder"
}
],
"ReturnType": "Microsoft.Extensions.Logging.ILoggingBuilder",
"Static": true,
"Extension": true,
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "AddAzureWebAppDiagnostics",
"Parameters": [
{
"Name": "factory",
"Type": "Microsoft.Extensions.Logging.ILoggerFactory"
}
],
"ReturnType": "Microsoft.Extensions.Logging.ILoggerFactory",
"Static": true,
"Extension": true,
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "AddAzureWebAppDiagnostics",
"Parameters": [
{
"Name": "factory",
"Type": "Microsoft.Extensions.Logging.ILoggerFactory"
},
{
"Name": "settings",
"Type": "Microsoft.Extensions.Logging.AzureAppServices.AzureAppServicesDiagnosticsSettings"
}
],
"ReturnType": "Microsoft.Extensions.Logging.ILoggerFactory",
"Static": true,
"Extension": true,
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
},
{
"Name": "Microsoft.Extensions.Logging.AzureAppServices.AzureAppServicesDiagnosticsSettings",
"Visibility": "Public",
"Kind": "Class",
"ImplementedInterfaces": [],
"Members": [
{
"Kind": "Method",
"Name": "get_FileSizeLimit",
"Parameters": [],
"ReturnType": "System.Int32",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_FileSizeLimit",
"Parameters": [
{
"Name": "value",
"Type": "System.Int32"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_RetainedFileCountLimit",
"Parameters": [],
"ReturnType": "System.Int32",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_RetainedFileCountLimit",
"Parameters": [
{
"Name": "value",
"Type": "System.Int32"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_OutputTemplate",
"Parameters": [],
"ReturnType": "System.String",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_OutputTemplate",
"Parameters": [
{
"Name": "value",
"Type": "System.String"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_BlobBatchSize",
"Parameters": [],
"ReturnType": "System.Int32",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_BlobBatchSize",
"Parameters": [
{
"Name": "value",
"Type": "System.Int32"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_BlobCommitPeriod",
"Parameters": [],
"ReturnType": "System.TimeSpan",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_BlobCommitPeriod",
"Parameters": [
{
"Name": "value",
"Type": "System.TimeSpan"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_BlobName",
"Parameters": [],
"ReturnType": "System.String",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_BlobName",
"Parameters": [
{
"Name": "value",
"Type": "System.String"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_BackgroundQueueSize",
"Parameters": [],
"ReturnType": "System.Int32",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_BackgroundQueueSize",
"Parameters": [
{
"Name": "value",
"Type": "System.Int32"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_FileFlushPeriod",
"Parameters": [],
"ReturnType": "System.Nullable<System.TimeSpan>",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_FileFlushPeriod",
"Parameters": [
{
"Name": "value",
"Type": "System.Nullable<System.TimeSpan>"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Constructor",
"Name": ".ctor",
"Parameters": [],
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
},
{
"Name": "Microsoft.Extensions.Logging.AzureAppServices.AzureBlobLoggerOptions",
"Visibility": "Public",
"Kind": "Class",
"BaseType": "Microsoft.Extensions.Logging.AzureAppServices.Internal.BatchingLoggerOptions",
"ImplementedInterfaces": [],
"Members": [
{
"Kind": "Method",
"Name": "get_BlobName",
"Parameters": [],
"ReturnType": "System.String",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_BlobName",
"Parameters": [
{
"Name": "value",
"Type": "System.String"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Constructor",
"Name": ".ctor",
"Parameters": [],
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
},
{
"Name": "Microsoft.Extensions.Logging.AzureAppServices.AzureFileLoggerOptions",
"Visibility": "Public",
"Kind": "Class",
"BaseType": "Microsoft.Extensions.Logging.AzureAppServices.Internal.BatchingLoggerOptions",
"ImplementedInterfaces": [],
"Members": [
{
"Kind": "Method",
"Name": "get_FileSizeLimit",
"Parameters": [],
"ReturnType": "System.Nullable<System.Int32>",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_FileSizeLimit",
"Parameters": [
{
"Name": "value",
"Type": "System.Nullable<System.Int32>"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_RetainedFileCountLimit",
"Parameters": [],
"ReturnType": "System.Nullable<System.Int32>",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_RetainedFileCountLimit",
"Parameters": [
{
"Name": "value",
"Type": "System.Nullable<System.Int32>"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_FileName",
"Parameters": [],
"ReturnType": "System.String",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_FileName",
"Parameters": [
{
"Name": "value",
"Type": "System.String"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Constructor",
"Name": ".ctor",
"Parameters": [],
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
}
]
}

View File

@ -0,0 +1,186 @@
// 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.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
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<byte>(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<byte>(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<HttpRequestException>(() => blob.AppendAsync(new ArraySegment<byte>(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<HttpRequestException>(() => blob.AppendAsync(new ArraySegment<byte>(new byte[] { 0, 2, 3 }), CancellationToken.None));
Assert.Equal(2, stage);
}
private class TestMessageHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, Task<HttpResponseMessage>> _callback;
public TestMessageHandler(Func<HttpRequestMessage, Task<HttpResponseMessage>> callback)
{
_callback = callback;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _callback(request);
}
}
}
}

View File

@ -0,0 +1,98 @@
// 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.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Xunit;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
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<ICloudAppendBlob>();
var buffers = new List<byte[]>();
blob.Setup(b => b.AppendAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))
.Callback((ArraySegment<byte> 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<ICloudAppendBlob>();
var buffers = new List<byte[]>();
var names = new List<string>();
blob.Setup(b => b.AppendAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))
.Callback((ArraySegment<byte> 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<byte> inputStream)
{
return inputStream.Array
.Skip(inputStream.Offset)
.Take(inputStream.Count)
.ToArray();
}
}
}

View File

@ -0,0 +1,70 @@
// 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.IO;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
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<IWebAppContext>();
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<IWebAppContext>();
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.
}
}
}
}
}
}

View File

@ -0,0 +1,111 @@
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class BatchingLoggerProviderTests
{
DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
string _nl = Environment.NewLine;
[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 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.Single(provider.Batches);
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);
provider.IntervalControl.Resume();
await provider.IntervalControl.Pause;
Assert.Equal(2, provider.Batches.Count);
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 BlocksWhenReachingMaxQueue()
{
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);
var task = Task.Run(() => logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state));
Assert.False(task.Wait(1000));
provider.IntervalControl.Resume();
await provider.IntervalControl.Pause;
Assert.True(task.Wait(1000));
}
private class TestBatchingLoggingProvider: BatchingLoggerProvider
{
public List<LogMessage[]> Batches { get; } = new List<LogMessage[]>();
public ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl();
public TestBatchingLoggingProvider(TimeSpan? interval = null, int? maxBatchSize = null, int? maxQueueSize = null)
: base(new OptionsWrapperMonitor<BatchingLoggerOptions>(new BatchingLoggerOptions
{
FlushPeriod = interval ?? TimeSpan.FromSeconds(1),
BatchSize = maxBatchSize,
BackgroundQueueSize = maxQueueSize,
IsEnabled = true
}))
{
}
protected override Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token)
{
Batches.Add(messages.ToArray());
return Task.CompletedTask;
}
protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return IntervalControl.IntervalAsync();
}
}
}
}

View File

@ -0,0 +1,71 @@
// 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 Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
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<string, string>("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<string, string>("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
}).Build();
var contextMock = new Mock<IWebAppContext>();
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<string, string>("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
}).Build();
var contextMock = new Mock<IWebAppContext>();
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);
}
}
}

View File

@ -0,0 +1,122 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
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);
}
}
}

View File

@ -0,0 +1,69 @@
// 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.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
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<IWebAppContext>();
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<LoggerFilterOptions>)));
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<LoggerFilterOptions>)));
}
[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<LoggerFilterOptions>)));
serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
Assert.Equal(4, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions<LoggerFilterOptions>)));
}
}
}

View File

@ -0,0 +1,30 @@
// 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.Threading.Tasks;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
internal class ManualIntervalControl
{
private TaskCompletionSource<object> _pauseCompletionSource = new TaskCompletionSource<object>();
private TaskCompletionSource<object> _resumeCompletionSource;
public Task Pause => _pauseCompletionSource.Task;
public void Resume()
{
_pauseCompletionSource = new TaskCompletionSource<object>();
_resumeCompletionSource.SetResult(null);
}
public async Task IntervalAsync()
{
_resumeCompletionSource = new TaskCompletionSource<object>();
_pauseCompletionSource.SetResult(null);
await _resumeCompletionSource.Task;
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.Logging.AzureAppServices" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,25 @@
// 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 Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
internal class OptionsWrapperMonitor<T> : IOptionsMonitor<T>
{
public OptionsWrapperMonitor(T currentValue)
{
CurrentValue = currentValue;
}
public IDisposable OnChange(Action<T, string> listener)
{
return null;
}
public T Get(string name) => CurrentValue;
public T CurrentValue { get; }
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
internal class TestBlobSink : BlobLoggerProvider
{
internal ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl();
public TestBlobSink(Func<string, ICloudAppendBlob> blobReferenceFactory) : base(
new OptionsWrapperMonitor<AzureBlobLoggerOptions>(new AzureBlobLoggerOptions()
{
ApplicationInstanceId = "42",
ApplicationName = "appname",
BlobName = "filename",
IsEnabled = true
}),
blobReferenceFactory)
{
}
protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return IntervalControl.IntervalAsync();
}
}
}

View File

@ -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;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
internal class TestFileLoggerProvider : FileLoggerProvider
{
internal ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl();
public TestFileLoggerProvider(
string path,
string fileName = "LogFile.",
int maxFileSize = 32_000,
int maxRetainedFiles = 100)
: base(new OptionsWrapperMonitor<AzureFileLoggerOptions>(new AzureFileLoggerOptions()
{
LogDirectory = path,
FileName = fileName,
FileSizeLimit = maxFileSize,
RetainedFileCountLimit = maxRetainedFiles,
IsEnabled = true
}))
{
}
protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return IntervalControl.IntervalAsync();
}
}
}

View File

@ -0,0 +1,40 @@
// 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.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.AzureAppServices.Internal;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class WebConfigurationLevelSwitchTests
{
[Theory]
[InlineData("Error", LogLevel.Error)]
[InlineData("Warning", LogLevel.Warning)]
[InlineData("Information", LogLevel.Information)]
[InlineData("Verbose", LogLevel.Trace)]
[InlineData("ABCD", LogLevel.None)]
public void AddsRuleWithCorrectLevel(string levelValue, LogLevel expectedLevel)
{
var configuration = new ConfigurationBuilder().AddInMemoryCollection(
new[]
{
new KeyValuePair<string, string>("levelKey", levelValue),
})
.Build();
var levelSwitcher = new ConfigurationBasedLevelSwitcher(configuration, typeof(TestFileLoggerProvider), "levelKey");
var filterConfiguration = new LoggerFilterOptions();
levelSwitcher.Configure(filterConfiguration);
Assert.Equal(1, filterConfiguration.Rules.Count);
var rule = filterConfiguration.Rules[0];
Assert.Equal(typeof(TestFileLoggerProvider).FullName, rule.ProviderName);
Assert.Equal(expectedLevel, rule.LogLevel);
}
}
}

View File

@ -0,0 +1,305 @@
// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.Extensions.Logging.Testing
{
public class AssemblyTestLog : IDisposable
{
public static readonly string OutputDirectoryEnvironmentVariableName = "ASPNETCORE_TEST_LOG_DIR";
private static readonly string MaxPathLengthEnvironmentVariableName = "ASPNETCORE_TEST_LOG_MAXPATH";
private static readonly string LogFileExtension = ".log";
private static readonly int MaxPathLength = GetMaxPathLength();
private static char[] InvalidFileChars = new char[]
{
'\"', '<', '>', '|', '\0',
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
(char)31, ':', '*', '?', '\\', '/', ' ', (char)127
};
private static readonly object _lock = new object();
private static readonly Dictionary<Assembly, AssemblyTestLog> _logs = new Dictionary<Assembly, AssemblyTestLog>();
private readonly ILoggerFactory _globalLoggerFactory;
private readonly ILogger _globalLogger;
private readonly string _baseDirectory;
private readonly string _assemblyName;
private readonly IServiceProvider _serviceProvider;
private static int GetMaxPathLength()
{
var maxPathString = Environment.GetEnvironmentVariable(MaxPathLengthEnvironmentVariableName);
var defaultMaxPath = 245;
return string.IsNullOrEmpty(maxPathString) ? defaultMaxPath : int.Parse(maxPathString);
}
private AssemblyTestLog(ILoggerFactory globalLoggerFactory, ILogger globalLogger, string baseDirectory, string assemblyName, IServiceProvider serviceProvider)
{
_globalLoggerFactory = globalLoggerFactory;
_globalLogger = globalLogger;
_baseDirectory = baseDirectory;
_assemblyName = assemblyName;
_serviceProvider = serviceProvider;
}
public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) =>
StartTestLog(output, className, out loggerFactory, LogLevel.Debug, testName);
public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, LogLevel minLogLevel, [CallerMemberName] string testName = null) =>
StartTestLog(output, className, out loggerFactory, minLogLevel, out var _, testName);
internal IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, LogLevel minLogLevel, out string resolvedTestName, [CallerMemberName] string testName = null)
{
var serviceProvider = CreateLoggerServices(output, className, minLogLevel, out resolvedTestName, testName);
var factory = serviceProvider.GetRequiredService<ILoggerFactory>();
loggerFactory = factory;
var logger = loggerFactory.CreateLogger("TestLifetime");
var stopwatch = Stopwatch.StartNew();
var scope = logger.BeginScope("Test: {testName}", testName);
_globalLogger.LogInformation("Starting test {testName}", testName);
logger.LogInformation("Starting test {testName}", testName);
return new Disposable(() =>
{
stopwatch.Stop();
_globalLogger.LogInformation("Finished test {testName} in {duration}s", testName, stopwatch.Elapsed.TotalSeconds);
logger.LogInformation("Finished test {testName} in {duration}s", testName, stopwatch.Elapsed.TotalSeconds);
scope.Dispose();
factory.Dispose();
(serviceProvider as IDisposable)?.Dispose();
});
}
public ILoggerFactory CreateLoggerFactory(ITestOutputHelper output, string className, [CallerMemberName] string testName = null) =>
CreateLoggerFactory(output, className, LogLevel.Trace, testName);
public ILoggerFactory CreateLoggerFactory(ITestOutputHelper output, string className, LogLevel minLogLevel, [CallerMemberName] string testName = null)
{
return CreateLoggerServices(output, className, minLogLevel, out var _, testName).GetRequiredService<ILoggerFactory>();
}
public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string className, LogLevel minLogLevel, out string normalizedTestName, [CallerMemberName] string testName = null)
{
normalizedTestName = string.Empty;
// Try to shorten the class name using the assembly name
if (className.StartsWith(_assemblyName + "."))
{
className = className.Substring(_assemblyName.Length + 1);
}
SerilogLoggerProvider serilogLoggerProvider = null;
if (!string.IsNullOrEmpty(_baseDirectory))
{
var testOutputDirectory = Path.Combine(GetAssemblyBaseDirectory(_assemblyName, _baseDirectory), className);
testName = RemoveIllegalFileChars(testName);
if (testOutputDirectory.Length + testName.Length + LogFileExtension.Length >= MaxPathLength)
{
_globalLogger.LogWarning($"Test name {testName} is too long. Please shorten test name.");
// Shorten the test name by removing the middle portion of the testname
var testNameLength = MaxPathLength - testOutputDirectory.Length - LogFileExtension.Length;
if (testNameLength <= 0)
{
throw new InvalidOperationException("Output file path could not be constructed due to max path length restrictions. Please shorten test assembly, class or method names.");
}
testName = testName.Substring(0, testNameLength / 2) + testName.Substring(testName.Length - testNameLength / 2, testNameLength / 2);
_globalLogger.LogWarning($"To prevent long paths test name was shortened to {testName}.");
}
var testOutputFile = Path.Combine(testOutputDirectory, $"{testName}{LogFileExtension}");
if (File.Exists(testOutputFile))
{
_globalLogger.LogWarning($"Output log file {testOutputFile} already exists. Please try to keep log file names unique.");
for (var i = 0; i < 1000; i++)
{
testOutputFile = Path.Combine(testOutputDirectory, $"{testName}.{i}{LogFileExtension}");
if (!File.Exists(testOutputFile))
{
_globalLogger.LogWarning($"To resolve log file collision, the enumerated file {testOutputFile} will be used.");
testName = $"{testName}.{i}";
break;
}
}
}
normalizedTestName = testName;
serilogLoggerProvider = ConfigureFileLogging(testOutputFile);
}
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(builder =>
{
builder.SetMinimumLevel(minLogLevel);
if (output != null)
{
builder.AddXunit(output, minLogLevel);
}
if (serilogLoggerProvider != null)
{
// Use a factory so that the container will dispose it
builder.Services.AddSingleton<ILoggerProvider>(_ => serilogLoggerProvider);
}
});
return serviceCollection.BuildServiceProvider();
}
public static AssemblyTestLog Create(string assemblyName, string baseDirectory)
{
SerilogLoggerProvider serilogLoggerProvider = null;
var globalLogDirectory = GetAssemblyBaseDirectory(assemblyName, baseDirectory);
if (!string.IsNullOrEmpty(globalLogDirectory))
{
var globalLogFileName = Path.Combine(globalLogDirectory, "global.log");
serilogLoggerProvider = ConfigureFileLogging(globalLogFileName);
}
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(builder =>
{
// Global logging, when it's written, is expected to be outputted. So set the log level to minimum.
builder.SetMinimumLevel(LogLevel.Trace);
if (serilogLoggerProvider != null)
{
// Use a factory so that the container will dispose it
builder.Services.AddSingleton<ILoggerProvider>(_ => serilogLoggerProvider);
}
});
var serviceProvider = serviceCollection.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("GlobalTestLog");
logger.LogInformation($"Global Test Logging initialized. Set the '{OutputDirectoryEnvironmentVariableName}' Environment Variable in order to create log files on disk.");
return new AssemblyTestLog(loggerFactory, logger, baseDirectory, assemblyName, serviceProvider);
}
public static AssemblyTestLog ForAssembly(Assembly assembly)
{
lock (_lock)
{
if (!_logs.TryGetValue(assembly, out var log))
{
var assemblyName = assembly.GetName().Name;
var baseDirectory = Environment.GetEnvironmentVariable(OutputDirectoryEnvironmentVariableName);
log = Create(assemblyName, baseDirectory);
_logs[assembly] = log;
// Try to clear previous logs
var assemblyBaseDirectory = GetAssemblyBaseDirectory(assemblyName, baseDirectory);
if (Directory.Exists(assemblyBaseDirectory))
{
try
{
Directory.Delete(assemblyBaseDirectory, recursive: true);
}
catch {}
}
}
return log;
}
}
private static string GetAssemblyBaseDirectory(string assemblyName, string baseDirectory)
{
if (!string.IsNullOrEmpty(baseDirectory))
{
return Path.Combine(baseDirectory, assemblyName, RuntimeInformation.FrameworkDescription.TrimStart('.'));
}
return string.Empty;
}
private static SerilogLoggerProvider ConfigureFileLogging(string fileName)
{
var dir = Path.GetDirectoryName(fileName);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
if (File.Exists(fileName))
{
File.Delete(fileName);
}
var serilogger = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Verbose()
.WriteTo.File(fileName, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{SourceContext}] [{Level}] {Message}{NewLine}{Exception}", flushToDiskInterval: TimeSpan.FromSeconds(1), shared: true)
.CreateLogger();
return new SerilogLoggerProvider(serilogger, dispose: true);
}
private static string RemoveIllegalFileChars(string s)
{
var sb = new StringBuilder();
foreach (var c in s)
{
if (InvalidFileChars.Contains(c))
{
if (sb.Length > 0 && sb[sb.Length - 1] != '_')
{
sb.Append('_');
}
}
else
{
sb.Append(c);
}
}
return sb.ToString();
}
public void Dispose()
{
(_serviceProvider as IDisposable)?.Dispose();
_globalLoggerFactory.Dispose();
}
private class Disposable : IDisposable
{
private Action _action;
public Disposable(Action action)
{
_action = action;
}
public void Dispose()
{
_action();
}
}
}
}

View File

@ -0,0 +1,12 @@
// 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.
namespace Microsoft.Extensions.Logging.Testing
{
public class BeginScopeContext
{
public object Scope { get; set; }
public string LoggerName { get; set; }
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Concurrent;
namespace Microsoft.Extensions.Logging.Testing
{
public interface ITestSink
{
Func<WriteContext, bool> WriteEnabled { get; set; }
Func<BeginScopeContext, bool> BeginEnabled { get; set; }
IProducerConsumerCollection<BeginScopeContext> Scopes { get; set; }
IProducerConsumerCollection<WriteContext> Writes { get; set; }
void Write(WriteContext context);
void Begin(BeginScopeContext context);
}
}

View File

@ -0,0 +1,80 @@
// 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.Linq;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public static class LogValuesAssert
{
/// <summary>
/// Asserts that the given key and value are present in the actual values.
/// </summary>
/// <param name="key">The key of the item to be found.</param>
/// <param name="value">The value of the item to be found.</param>
/// <param name="actualValues">The actual values.</param>
public static void Contains(
string key,
object value,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
Contains(new[] { new KeyValuePair<string, object>(key, value) }, actualValues);
}
/// <summary>
/// Asserts that all the expected values are present in the actual values by ignoring
/// the order of values.
/// </summary>
/// <param name="expectedValues">Expected subset of values</param>
/// <param name="actualValues">Actual set of values</param>
public static void Contains(
IEnumerable<KeyValuePair<string, object>> expectedValues,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
if (expectedValues == null)
{
throw new ArgumentNullException(nameof(expectedValues));
}
if (actualValues == null)
{
throw new ArgumentNullException(nameof(actualValues));
}
var comparer = new LogValueComparer();
foreach (var expectedPair in expectedValues)
{
if (!actualValues.Contains(expectedPair, comparer))
{
throw new EqualException(
expected: GetString(expectedValues),
actual: GetString(actualValues));
}
}
}
private static string GetString(IEnumerable<KeyValuePair<string, object>> logValues)
{
return string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]"));
}
private class LogValueComparer : IEqualityComparer<KeyValuePair<string, object>>
{
public bool Equals(KeyValuePair<string, object> x, KeyValuePair<string, object> y)
{
return string.Equals(x.Key, y.Key) && object.Equals(x.Value, y.Value);
}
public int GetHashCode(KeyValuePair<string, object> obj)
{
// We are never going to put this KeyValuePair in a hash table,
// so this is ok.
throw new NotImplementedException();
}
}
}
}

View File

@ -0,0 +1,45 @@
// 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.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Microsoft.Extensions.Logging.Testing
{
public abstract class LoggedTest
{
// Obsolete but keeping for back compat
public LoggedTest(ITestOutputHelper output = null)
{
TestOutputHelper = output;
}
// Internal for testing
internal string ResolvedTestMethodName { get; set; }
// Internal for testing
internal string ResolvedTestClassName { get; set; }
public ILogger Logger { get; set; }
public ILoggerFactory LoggerFactory { get; set; }
public ITestOutputHelper TestOutputHelper { get; set; }
public ITestSink TestSink { get; set; }
public void AddTestLogging(IServiceCollection services) => services.AddSingleton(LoggerFactory);
public IDisposable StartLog(out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) => StartLog(out loggerFactory, LogLevel.Information, testName);
public IDisposable StartLog(out ILoggerFactory loggerFactory, LogLevel minLogLevel, [CallerMemberName] string testName = null)
{
return AssemblyTestLog.ForAssembly(GetType().GetTypeInfo().Assembly).StartTestLog(TestOutputHelper, GetType().FullName, out loggerFactory, minLogLevel, testName);
}
public virtual void AdditionalSetup() { }
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Helpers for writing tests that use Microsoft.Extensions.Logging. Contains null implementations of the abstractions that do nothing, as well as test implementations that are observable.</Description>
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<PackageTags>$(PackageTags);testing</PackageTags>
<EnableApiCheck>false</EnableApiCheck>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Testing" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Serilog.Extensions.Logging" />
<Reference Include="Serilog.Sinks.File" />
<Reference Include="xunit.abstractions" />
<Reference Include="xunit.assert" />
<Reference Include="xunit.extensibility.execution" />
</ItemGroup>
<ItemGroup>
<Content Include="build\**\*.props" PackagePath="%(Identity)" />
</ItemGroup>
</Project>

View File

@ -0,0 +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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.Extensions.Logging.Testing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,12 @@
// 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.Testing
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false)]
public class ShortClassNameAttribute : Attribute
{
}
}

View File

@ -0,0 +1,76 @@
// 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.Testing
{
public class TestLogger : ILogger
{
private object _scope;
private readonly ITestSink _sink;
private readonly string _name;
private readonly Func<LogLevel, bool> _filter;
public TestLogger(string name, ITestSink sink, bool enabled)
: this(name, sink, _ => enabled)
{
}
public TestLogger(string name, ITestSink sink, Func<LogLevel, bool> filter)
{
_sink = sink;
_name = name;
_filter = filter;
}
public string Name { get; set; }
public IDisposable BeginScope<TState>(TState state)
{
_scope = state;
_sink.Begin(new BeginScopeContext()
{
LoggerName = _name,
Scope = state,
});
return TestDisposable.Instance;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
_sink.Write(new WriteContext()
{
LogLevel = logLevel,
EventId = eventId,
State = state,
Exception = exception,
Formatter = (s, e) => formatter((TState)s, e),
LoggerName = _name,
Scope = _scope
});
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None && _filter(logLevel);
}
private class TestDisposable : IDisposable
{
public static readonly TestDisposable Instance = new TestDisposable();
public void Dispose()
{
// intentionally does nothing
}
}
}
}

View File

@ -0,0 +1,32 @@
// 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.Testing
{
public class TestLoggerFactory : ILoggerFactory
{
private readonly ITestSink _sink;
private readonly bool _enabled;
public TestLoggerFactory(ITestSink sink, bool enabled)
{
_sink = sink;
_enabled = enabled;
}
public ILogger CreateLogger(string name)
{
return new TestLogger(name, _sink, _enabled);
}
public void AddProvider(ILoggerProvider provider)
{
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,37 @@
// 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.Testing
{
public class TestLogger<T> : ILogger
{
private readonly ILogger _logger;
public TestLogger(TestLoggerFactory factory)
{
_logger = factory.CreateLogger<T>();
}
public IDisposable BeginScope<TState>(TState state)
{
return _logger.BeginScope(state);
}
public bool IsEnabled(LogLevel logLevel)
{
return _logger.IsEnabled(logLevel);
}
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
{
_logger.Log(logLevel, eventId, state, exception, formatter);
}
}
}

View File

@ -0,0 +1,24 @@
// 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.
namespace Microsoft.Extensions.Logging.Testing
{
public class TestLoggerProvider : ILoggerProvider
{
private readonly ITestSink _sink;
public TestLoggerProvider(ITestSink sink)
{
_sink = sink;
}
public ILogger CreateLogger(string categoryName)
{
return new TestLogger(categoryName, _sink, enabled: true);
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,59 @@
// 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.Concurrent;
namespace Microsoft.Extensions.Logging.Testing
{
public class TestSink : ITestSink
{
private ConcurrentQueue<BeginScopeContext> _scopes;
private ConcurrentQueue<WriteContext> _writes;
public TestSink(
Func<WriteContext, bool> writeEnabled = null,
Func<BeginScopeContext, bool> beginEnabled = null)
{
WriteEnabled = writeEnabled;
BeginEnabled = beginEnabled;
_scopes = new ConcurrentQueue<BeginScopeContext>();
_writes = new ConcurrentQueue<WriteContext>();
}
public Func<WriteContext, bool> WriteEnabled { get; set; }
public Func<BeginScopeContext, bool> BeginEnabled { get; set; }
public IProducerConsumerCollection<BeginScopeContext> Scopes { get => _scopes; set => _scopes = new ConcurrentQueue<BeginScopeContext>(value); }
public IProducerConsumerCollection<WriteContext> Writes { get => _writes; set => _writes = new ConcurrentQueue<WriteContext>(value); }
public void Write(WriteContext context)
{
if (WriteEnabled == null || WriteEnabled(context))
{
_writes.Enqueue(context);
}
}
public void Begin(BeginScopeContext context)
{
if (BeginEnabled == null || BeginEnabled(context))
{
_scopes.Enqueue(context);
}
}
public static bool EnableWithTypeName<T>(WriteContext context)
{
return context.LoggerName.Equals(typeof(T).FullName);
}
public static bool EnableWithTypeName<T>(BeginScopeContext context)
{
return context.LoggerName.Equals(typeof(T).FullName);
}
}
}

View File

@ -0,0 +1,32 @@
// 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.Testing
{
public class WriteContext
{
public LogLevel LogLevel { get; set; }
public EventId EventId { get; set; }
public object State { get; set; }
public Exception Exception { get; set; }
public Func<object, Exception, string> Formatter { get; set; }
public object Scope { get; set; }
public string LoggerName { get; set; }
public string Message
{
get
{
return Formatter(State, Exception);
}
}
}
}

View File

@ -0,0 +1,18 @@
// 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.Testing
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class LogLevelAttribute : Attribute
{
public LogLevelAttribute(LogLevel logLevel)
{
LogLevel = logLevel;
}
public LogLevel LogLevel { get; }
}
}

View File

@ -0,0 +1,28 @@
// 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.AspNetCore.Testing.xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedConditionalFactDiscoverer : LoggedFactDiscoverer
{
private readonly IMessageSink _diagnosticMessageSink;
public LoggedConditionalFactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink)
{
_diagnosticMessageSink = diagnosticMessageSink;
}
protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
{
var skipReason = testMethod.EvaluateSkipConditions();
return skipReason != null
? new SkippedTestCase(skipReason, _diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod)
: base.CreateTestCase(discoveryOptions, testMethod, factAttribute);
}
}
}

View File

@ -0,0 +1,41 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedConditionalTheoryDiscoverer : LoggedTheoryDiscoverer
{
public LoggedConditionalTheoryDiscoverer(IMessageSink diagnosticMessageSink)
: base(diagnosticMessageSink)
{
}
protected override IEnumerable<IXunitTestCase> CreateTestCasesForTheory(
ITestFrameworkDiscoveryOptions discoveryOptions,
ITestMethod testMethod,
IAttributeInfo theoryAttribute)
{
var skipReason = testMethod.EvaluateSkipConditions();
return skipReason != null
? new[] { new SkippedTestCase(skipReason, DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) }
: base.CreateTestCasesForTheory(discoveryOptions, testMethod, theoryAttribute);
}
protected override IEnumerable<IXunitTestCase> CreateTestCasesForDataRow(
ITestFrameworkDiscoveryOptions discoveryOptions,
ITestMethod testMethod, IAttributeInfo theoryAttribute,
object[] dataRow)
{
var skipReason = testMethod.EvaluateSkipConditions();
return skipReason != null
? base.CreateTestCasesForSkippedDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow, skipReason)
: base.CreateTestCasesForDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow);
}
}
}

View File

@ -0,0 +1,18 @@
// 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 Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedFactDiscoverer : FactDiscoverer
{
public LoggedFactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink)
{
}
protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
=> new LoggedTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod);
}
}

View File

@ -0,0 +1,31 @@
// 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.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestAssemblyRunner : XunitTestAssemblyRunner
{
public LoggedTestAssemblyRunner(
ITestAssembly testAssembly,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
: base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions)
{
}
protected override Task<RunSummary> RunTestCollectionAsync(
IMessageBus messageBus,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
CancellationTokenSource cancellationTokenSource)
=> new LoggedTestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();
}
}

View File

@ -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;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestCase : XunitTestCase
{
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
public LoggedTestCase() : base()
{
}
public LoggedTestCase(
IMessageSink diagnosticMessageSink,
TestMethodDisplay defaultMethodDisplay,
ITestMethod testMethod,
object[] testMethodArguments = null)
: base(diagnosticMessageSink, defaultMethodDisplay, testMethod, testMethodArguments)
{
}
public override Task<RunSummary> RunAsync(
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
object[] constructorArguments,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
=> new LoggedTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource).RunAsync();
}
}

View File

@ -0,0 +1,42 @@
// 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.Reflection;
using System.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestCaseRunner : XunitTestCaseRunner
{
public LoggedTestCaseRunner(
IXunitTestCase testCase,
string displayName,
string skipReason,
object[] constructorArguments,
object[] testMethodArguments,
IMessageBus messageBus,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(testCase, displayName, skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource)
{
}
protected override XunitTestRunner CreateTestRunner(
ITest test,
IMessageBus messageBus,
Type testClass,
object[] constructorArguments,
MethodInfo testMethod,
object[] testMethodArguments,
string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
=> new LoggedTestRunner(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments,
skipReason, beforeAfterAttributes, new ExceptionAggregator(aggregator), cancellationTokenSource);
}
}

View File

@ -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;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestClassRunner : XunitTestClassRunner
{
public LoggedTestClassRunner(
ITestClass testClass,
IReflectionTypeInfo @class,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
ITestCaseOrderer testCaseOrderer,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
IDictionary<Type, object> collectionFixtureMappings)
: base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource, collectionFixtureMappings)
{
}
protected override Task<RunSummary> RunTestMethodAsync(
ITestMethod testMethod,
IReflectionMethodInfo method,
IEnumerable<IXunitTestCase> testCases,
object[] constructorArguments)
=> new LoggedTestMethodRunner(testMethod, Class, method, testCases, DiagnosticMessageSink, MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource, constructorArguments).RunAsync();
}
}

View File

@ -0,0 +1,33 @@
// 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.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestCollectionRunner : XunitTestCollectionRunner
{
private readonly IMessageSink _diagnosticMessageSink;
public LoggedTestCollectionRunner(
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
ITestCaseOrderer testCaseOrderer,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
{
// Base class doesn't expose this, so capture it here.
_diagnosticMessageSink = diagnosticMessageSink;
}
protected override Task<RunSummary> RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable<IXunitTestCase> testCases)
=> new LoggedTestClassRunner(testClass, @class, testCases, _diagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, CollectionFixtureMappings).RunAsync();
}
}

View File

@ -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.Reflection;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestFramework : XunitTestFramework
{
public LoggedTestFramework(IMessageSink messageSink) : base(messageSink)
{
}
protected override ITestFrameworkDiscoverer CreateDiscoverer(IAssemblyInfo assemblyInfo)
{
return new LoggedTestFrameworkDiscoverer(assemblyInfo, SourceInformationProvider, DiagnosticMessageSink);
}
protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
{
return new LoggedTestFrameworkExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink);
}
}
}

View File

@ -0,0 +1,80 @@
// 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.Linq;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestFrameworkDiscoverer : XunitTestFrameworkDiscoverer
{
private IDictionary<Type, IXunitTestCaseDiscoverer> Discoverers { get; }
public LoggedTestFrameworkDiscoverer(
IAssemblyInfo assemblyInfo,
ISourceInformationProvider sourceProvider,
IMessageSink diagnosticMessageSink,
IXunitTestCollectionFactory collectionFactory = null)
: base(assemblyInfo, sourceProvider, diagnosticMessageSink, collectionFactory)
{
Discoverers = new Dictionary<Type, IXunitTestCaseDiscoverer>()
{
{ typeof(ConditionalTheoryAttribute), new LoggedConditionalTheoryDiscoverer(diagnosticMessageSink) },
{ typeof(ConditionalFactAttribute), new LoggedConditionalFactDiscoverer(diagnosticMessageSink) },
{ typeof(TheoryAttribute), new LoggedTheoryDiscoverer(diagnosticMessageSink) },
{ typeof(FactAttribute), new LoggedFactDiscoverer(diagnosticMessageSink) }
};
}
protected override bool FindTestsForMethod(
ITestMethod testMethod,
bool includeSourceInformation,
IMessageBus messageBus,
ITestFrameworkDiscoveryOptions discoveryOptions)
{
if (typeof(LoggedTest).IsAssignableFrom(testMethod.TestClass.Class.ToRuntimeType()))
{
var factAttributes = testMethod.Method.GetCustomAttributes(typeof(FactAttribute));
if (factAttributes.Count() > 1)
{
var message = $"Test method '{testMethod.TestClass.Class.Name}.{testMethod.Method.Name}' has multiple [Fact]-derived attributes";
var testCase = new ExecutionErrorTestCase(DiagnosticMessageSink, TestMethodDisplay.ClassAndMethod, testMethod, message);
return ReportDiscoveredTestCase(testCase, includeSourceInformation, messageBus);
}
var factAttribute = factAttributes.FirstOrDefault();
if (factAttribute == null)
{
return true;
}
var factAttributeType = (factAttribute as IReflectionAttributeInfo)?.Attribute.GetType();
if (!Discoverers.TryGetValue(factAttributeType, out var discoverer))
{
return base.FindTestsForMethod(testMethod, includeSourceInformation, messageBus, discoveryOptions);
}
else
{
foreach (var testCase in discoverer.Discover(discoveryOptions, testMethod, factAttribute))
{
if (!ReportDiscoveredTestCase(testCase, includeSourceInformation, messageBus))
{
return false;
}
}
return true;
}
}
else
{
return base.FindTestsForMethod(testMethod, includeSourceInformation, messageBus, discoveryOptions);
}
}
}
}

View File

@ -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.Collections.Generic;
using System.Reflection;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestFrameworkExecutor : XunitTestFrameworkExecutor
{
public LoggedTestFrameworkExecutor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink)
: base(assemblyName, sourceInformationProvider, diagnosticMessageSink)
{
}
protected override async void RunTestCases(IEnumerable<IXunitTestCase> testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
{
using (var assemblyRunner = new LoggedTestAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions))
{
await assemblyRunner.RunAsync();
}
}
}
}

View File

@ -0,0 +1,100 @@
// 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.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestInvoker : XunitTestInvoker
{
private TestOutputHelper _output;
public LoggedTestInvoker(
ITest test,
IMessageBus messageBus,
Type testClass,
object[] constructorArguments,
MethodInfo testMethod,
object[] testMethodArguments,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator, cancellationTokenSource)
{
}
protected override Task BeforeTestMethodInvokedAsync()
{
if (_output != null)
{
_output.Initialize(MessageBus, Test);
}
return base.BeforeTestMethodInvokedAsync();
}
protected override async Task AfterTestMethodInvokedAsync()
{
await base.AfterTestMethodInvokedAsync();
if (_output != null)
{
_output.Uninitialize();
}
}
protected override object CreateTestClass()
{
var testClass = base.CreateTestClass();
if (testClass is LoggedTest loggedTestClass)
{
var classType = loggedTestClass.GetType();
var logLevelAttribute = TestMethod.GetCustomAttribute<LogLevelAttribute>() as LogLevelAttribute;
var testName = TestMethodArguments.Aggregate(TestMethod.Name, (a, b) => $"{a}-{(b ?? "null")}");
// Try resolving ITestOutputHelper from constructor arguments
loggedTestClass.TestOutputHelper = ConstructorArguments?.SingleOrDefault(a => typeof(ITestOutputHelper).IsAssignableFrom(a.GetType())) as ITestOutputHelper;
var useShortClassName = TestMethod.DeclaringType.GetCustomAttribute<ShortClassNameAttribute>()
?? TestMethod.DeclaringType.Assembly.GetCustomAttribute<ShortClassNameAttribute>();
var resolvedClassName = useShortClassName == null ? classType.FullName : classType.Name;
// None resolved so create a new one and retain a reference to it for initialization/uninitialization
if (loggedTestClass.TestOutputHelper == null)
{
loggedTestClass.TestOutputHelper = _output = new TestOutputHelper();
}
AssemblyTestLog
.ForAssembly(classType.GetTypeInfo().Assembly)
.StartTestLog(
loggedTestClass.TestOutputHelper,
resolvedClassName,
out var loggerFactory,
logLevelAttribute?.LogLevel ?? LogLevel.Trace,
out var resolvedTestName,
testName);
// internal for testing
loggedTestClass.ResolvedTestMethodName = resolvedTestName;
loggedTestClass.ResolvedTestClassName = resolvedClassName;
loggedTestClass.LoggerFactory = loggerFactory;
loggedTestClass.Logger = loggerFactory.CreateLogger(classType);
loggedTestClass.TestSink = new TestSink();
loggerFactory.AddProvider(new TestLoggerProvider(loggedTestClass.TestSink));
loggedTestClass.AdditionalSetup();
}
return testClass;
}
}
}

View File

@ -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.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestMethodRunner : XunitTestMethodRunner
{
private IMessageSink DiagnosticMessageSink { get; }
private object[] ConstructorArguments { get; }
public LoggedTestMethodRunner(
ITestMethod testMethod,
IReflectionTypeInfo @class,
IReflectionMethodInfo method,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
object[] constructorArguments)
: base(testMethod, @class, method, testCases, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource, constructorArguments)
{
DiagnosticMessageSink = diagnosticMessageSink;
ConstructorArguments = constructorArguments;
}
protected override Task<RunSummary> RunTestCaseAsync(IXunitTestCase testCase)
=> testCase.RunAsync(DiagnosticMessageSink, MessageBus, ConstructorArguments, new ExceptionAggregator(Aggregator), CancellationTokenSource);
}
}

View File

@ -0,0 +1,33 @@
// 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.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTestRunner : XunitTestRunner
{
public LoggedTestRunner(
ITest test,
IMessageBus messageBus,
Type testClass,
object[] constructorArguments,
MethodInfo testMethod, object[]
testMethodArguments, string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource)
{
}
protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
=> new LoggedTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync();
}
}

View File

@ -0,0 +1,29 @@
// 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.Collections.Generic;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTheoryDiscoverer : TheoryDiscoverer
{
public LoggedTheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink)
{
}
protected override IEnumerable<IXunitTestCase> CreateTestCasesForDataRow(
ITestFrameworkDiscoveryOptions discoveryOptions,
ITestMethod testMethod,
IAttributeInfo theoryAttribute,
object[] dataRow)
=> new[] { new LoggedTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow) };
protected override IEnumerable<IXunitTestCase> CreateTestCasesForTheory(
ITestFrameworkDiscoveryOptions discoveryOptions,
ITestMethod testMethod,
IAttributeInfo theoryAttribute)
=> new[] { new LoggedTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) };
}
}

View File

@ -0,0 +1,35 @@
// 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.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTheoryTestCase : XunitTheoryTestCase
{
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
public LoggedTheoryTestCase() : base()
{
}
public LoggedTheoryTestCase(
IMessageSink diagnosticMessageSink,
TestMethodDisplay defaultMethodDisplay,
ITestMethod testMethod)
: base(diagnosticMessageSink, defaultMethodDisplay, testMethod)
{
}
public override Task<RunSummary> RunAsync(
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
object[] constructorArguments,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
=> new LoggedTheoryTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource).RunAsync();
}
}

View File

@ -0,0 +1,41 @@
// 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.Reflection;
using System.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public class LoggedTheoryTestCaseRunner : XunitTheoryTestCaseRunner
{
public LoggedTheoryTestCaseRunner(
IXunitTestCase testCase,
string displayName,
string skipReason,
object[] constructorArguments,
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(testCase, displayName, skipReason, constructorArguments, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource)
{
}
protected override XunitTestRunner CreateTestRunner(
ITest test,
IMessageBus messageBus,
Type testClass,
object[] constructorArguments,
MethodInfo testMethod,
object[] testMethodArguments,
string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
=> new LoggedTestRunner(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, new ExceptionAggregator(aggregator), cancellationTokenSource);
}
}

View File

@ -0,0 +1,37 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Testing;
using Xunit.Abstractions;
namespace Microsoft.Extensions.Logging
{
public static class XunitLoggerFactoryExtensions
{
public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output)
{
builder.Services.AddSingleton<ILoggerProvider>(new XunitLoggerProvider(output));
return builder;
}
public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel)
{
builder.Services.AddSingleton<ILoggerProvider>(new XunitLoggerProvider(output, minLevel));
return builder;
}
public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output)
{
loggerFactory.AddProvider(new XunitLoggerProvider(output));
return loggerFactory;
}
public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel)
{
loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel));
return loggerFactory;
}
}
}

View File

@ -0,0 +1,116 @@
// 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.Linq;
using System.Text;
using Xunit.Abstractions;
namespace Microsoft.Extensions.Logging.Testing
{
public class XunitLoggerProvider : ILoggerProvider
{
private readonly ITestOutputHelper _output;
private readonly LogLevel _minLevel;
public XunitLoggerProvider(ITestOutputHelper output)
: this(output, LogLevel.Trace)
{
}
public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel)
{
_output = output;
_minLevel = minLevel;
}
public ILogger CreateLogger(string categoryName)
{
return new XunitLogger(_output, categoryName, _minLevel);
}
public void Dispose()
{
}
}
public class XunitLogger : ILogger
{
private static readonly string[] NewLineChars = new[] { Environment.NewLine };
private readonly string _category;
private readonly LogLevel _minLogLevel;
private readonly ITestOutputHelper _output;
public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel)
{
_minLogLevel = minLogLevel;
_category = category;
_output = output;
}
public void Log<TState>(
LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
// Buffer the message into a single string in order to avoid shearing the message when running across multiple threads.
var messageBuilder = new StringBuilder();
var timestamp = DateTime.Now.ToString("s");
var firstLinePrefix = $"| [{timestamp}] {_category} {logLevel}: ";
var lines = formatter(state, exception).Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries);
messageBuilder.AppendLine(firstLinePrefix + lines.FirstOrDefault() ?? string.Empty);
var additionalLinePrefix = "|" + new string(' ', firstLinePrefix.Length - 1);
foreach (var line in lines.Skip(1))
{
messageBuilder.AppendLine(additionalLinePrefix + line);
}
if (exception != null)
{
lines = exception.ToString().Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries);
additionalLinePrefix = "| ";
foreach (var line in lines)
{
messageBuilder.AppendLine(additionalLinePrefix + line);
}
}
// Remove the last line-break, because ITestOutputHelper only has WriteLine.
var message = messageBuilder.ToString();
if (message.EndsWith(Environment.NewLine))
{
message = message.Substring(0, message.Length - Environment.NewLine.Length);
}
try
{
_output.WriteLine(message);
}
catch (Exception)
{
// We could fail because we're on a background thread and our captured ITestOutputHelper is
// busted (if the test "completed" before the background thread fired).
// So, ignore this. There isn't really anything we can do but hope the
// caller has additional loggers registered
}
}
public bool IsEnabled(LogLevel logLevel)
=> logLevel >= _minLogLevel;
public IDisposable BeginScope<TState>(TState state)
=> new NullScope();
private class NullScope : IDisposable
{
public void Dispose()
{
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
<Project>
<ItemGroup>
<AssemblyAttribute Include="Xunit.TestFramework">
<_Parameter1>Microsoft.Extensions.Logging.Testing.LoggedTestFramework</_Parameter1>
<_Parameter2>Microsoft.Extensions.Logging.Testing</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Project>

View File

@ -0,0 +1,207 @@
// 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.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.Extensions.Logging.Testing.Tests
{
public class AssemblyTestLogTests : LoggedTest
{
private static readonly Assembly ThisAssembly = typeof(AssemblyTestLog).GetTypeInfo().Assembly;
[Fact]
public void FullClassNameUsedWhenShortClassNameAttributeNotSpecified()
{
Assert.Equal(GetType().FullName, ResolvedTestClassName);
}
[Fact]
public void ForAssembly_ReturnsSameInstanceForSameAssembly()
{
Assert.Same(
AssemblyTestLog.ForAssembly(ThisAssembly),
AssemblyTestLog.ForAssembly(ThisAssembly));
}
[Fact]
public void TestLogWritesToITestOutputHelper()
{
var output = new TestTestOutputHelper();
var assemblyLog = AssemblyTestLog.Create("NonExistant.Test.Assembly", baseDirectory: null);
using (assemblyLog.StartTestLog(output, "NonExistant.Test.Class", out var loggerFactory))
{
var logger = loggerFactory.CreateLogger("TestLogger");
logger.LogInformation("Information!");
// Trace is disabled by default
logger.LogTrace("Trace!");
}
Assert.Equal(@"[TIMESTAMP] TestLifetime Information: Starting test TestLogWritesToITestOutputHelper
[TIMESTAMP] TestLogger Information: Information!
[TIMESTAMP] TestLifetime Information: Finished test TestLogWritesToITestOutputHelper in DURATION
", MakeConsistent(output.Output), ignoreLineEndingDifferences: true);
}
[Fact]
private Task TestLogEscapesIllegalFileNames() =>
RunTestLogFunctionalTest((tempDir) =>
{
var illegalTestName = "Testing-https://localhost:5000";
var escapedTestName = "Testing-https_localhost_5000";
using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", baseDirectory: tempDir))
using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, resolvedTestName: out var resolvedTestname, testName: illegalTestName))
{
Assert.Equal(escapedTestName, resolvedTestname);
}
});
[Fact]
public Task TestLogWritesToGlobalLogFile() =>
RunTestLogFunctionalTest((tempDir) =>
{
// Because this test writes to a file, it is a functional test and should be logged
// but it's also testing the test logging facility. So this is pretty meta ;)
var logger = LoggerFactory.CreateLogger("Test");
using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir))
{
logger.LogInformation("Created test log in {baseDirectory}", tempDir);
using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName"))
{
var testLogger = testLoggerFactory.CreateLogger("TestLogger");
testLogger.LogInformation("Information!");
testLogger.LogTrace("Trace!");
}
}
logger.LogInformation("Finished test log in {baseDirectory}", tempDir);
var globalLogPath = Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "global.log");
var testLog = Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.log");
Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist");
Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist");
var globalLogContent = MakeConsistent(File.ReadAllText(globalLogPath));
var testLogContent = MakeConsistent(File.ReadAllText(testLog));
Assert.Equal(@"[GlobalTestLog] [Information] Global Test Logging initialized. Set the 'ASPNETCORE_TEST_LOG_DIR' Environment Variable in order to create log files on disk.
[GlobalTestLog] [Information] Starting test ""FakeTestName""
[GlobalTestLog] [Information] Finished test ""FakeTestName"" in DURATION
", globalLogContent, ignoreLineEndingDifferences: true);
Assert.Equal(@"[TestLifetime] [Information] Starting test ""FakeTestName""
[TestLogger] [Information] Information!
[TestLogger] [Verbose] Trace!
[TestLifetime] [Information] Finished test ""FakeTestName"" in DURATION
", testLogContent, ignoreLineEndingDifferences: true);
});
[Fact]
public Task TestLogTruncatesTestNameToAvoidLongPaths() =>
RunTestLogFunctionalTest((tempDir) =>
{
var longTestName = new string('0', 50) + new string('1', 50) + new string('2', 50) + new string('3', 50) + new string('4', 50);
var logger = LoggerFactory.CreateLogger("Test");
using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir))
{
logger.LogInformation("Created test log in {baseDirectory}", tempDir);
using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: longTestName))
{
testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!");
}
}
logger.LogInformation("Finished test log in {baseDirectory}", tempDir);
var testLogFiles = new DirectoryInfo(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass")).EnumerateFiles();
var testLog = Assert.Single(testLogFiles);
var testFileName = Path.GetFileNameWithoutExtension(testLog.Name);
// The first half of the file comes from the beginning of the test name passed to the logger
Assert.Equal(longTestName.Substring(0, testFileName.Length / 2), testFileName.Substring(0, testFileName.Length / 2));
// The last half of the file comes from the ending of the test name passed to the logger
Assert.Equal(longTestName.Substring(longTestName.Length - testFileName.Length / 2, testFileName.Length / 2), testFileName.Substring(testFileName.Length - testFileName.Length / 2, testFileName.Length / 2));
});
[Fact]
public Task TestLogEnumerateFilenamesToAvoidCollisions() =>
RunTestLogFunctionalTest((tempDir) =>
{
var logger = LoggerFactory.CreateLogger("Test");
using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir))
{
logger.LogInformation("Created test log in {baseDirectory}", tempDir);
for (var i = 0; i < 10; i++)
{
using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName"))
{
testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!");
}
}
}
logger.LogInformation("Finished test log in {baseDirectory}", tempDir);
// The first log file exists
Assert.True(File.Exists(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.log")));
// Subsequent files exist
for (var i = 0; i < 9; i++)
{
Assert.True(File.Exists(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.{i}.log")));
}
});
private static readonly Regex TimestampRegex = new Regex(@"\d+-\d+-\d+T\d+:\d+:\d+");
private static readonly Regex DurationRegex = new Regex(@"[^ ]+s$");
private async Task RunTestLogFunctionalTest(Action<string> action, [CallerMemberName] string testName = null)
{
var tempDir = Path.Combine(Path.GetTempPath(), $"TestLogging_{Guid.NewGuid().ToString("N")}");
try
{
action(tempDir);
}
finally
{
if (Directory.Exists(tempDir))
{
try
{
Directory.Delete(tempDir, recursive: true);
}
catch
{
await Task.Delay(100);
Directory.Delete(tempDir, recursive: true);
}
}
}
}
private static string MakeConsistent(string input)
{
return string.Join(Environment.NewLine, input.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
.Select(line =>
{
var strippedPrefix = line.IndexOf("[") >= 0 ? line.Substring(line.IndexOf("[")) : line;
var strippedDuration =
DurationRegex.Replace(strippedPrefix, "DURATION");
var strippedTimestamp = TimestampRegex.Replace(strippedDuration, "TIMESTAMP");
return strippedTimestamp;
}));
}
}
}

View File

@ -0,0 +1,221 @@
// 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.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing.Tests
{
public class LogValuesAssertTest
{
public static TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>> ExpectedValues_SubsetOf_ActualValuesData
{
get
{
return new TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>>()
{
{
new KeyValuePair<string,object>[] { },
new KeyValuePair<string,object>[] { }
},
{
// subset
new KeyValuePair<string,object>[] { },
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
}
},
{
// subset
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
},
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteConstraint", "Something")
}
},
{
// equal number of values
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
},
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id"),
}
}
};
}
}
[Theory]
[MemberData(nameof(ExpectedValues_SubsetOf_ActualValuesData))]
public void Asserts_Success_ExpectedValues_SubsetOf_ActualValues(
IEnumerable<KeyValuePair<string, object>> expectedValues,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
// Act && Assert
LogValuesAssert.Contains(expectedValues, actualValues);
}
public static TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>> ExpectedValues_MoreThan_ActualValuesData
{
get
{
return new TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>>()
{
{
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
},
new KeyValuePair<string,object>[] { }
},
{
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteConstraint", "Something")
},
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
}
}
};
}
}
[Theory]
[MemberData(nameof(ExpectedValues_MoreThan_ActualValuesData))]
public void Asserts_Failure_ExpectedValues_MoreThan_ActualValues(
IEnumerable<KeyValuePair<string, object>> expectedValues,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
// Act && Assert
var equalException = Assert.Throws<EqualException>(
() => LogValuesAssert.Contains(expectedValues, actualValues));
Assert.Equal(GetString(expectedValues), equalException.Expected);
Assert.Equal(GetString(actualValues), equalException.Actual);
}
[Fact]
public void Asserts_Success_IgnoringOrderOfItems()
{
// Arrange
var expectedLogValues = new[]
{
new KeyValuePair<string, object>("RouteConstraint", "Something"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
};
var actualLogValues = new[]
{
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteConstraint", "Something"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
};
// Act && Assert
LogValuesAssert.Contains(expectedLogValues, actualLogValues);
}
[Fact]
public void Asserts_Success_OnSpecifiedKeyAndValue()
{
// Arrange
var actualLogValues = new[]
{
new KeyValuePair<string, object>("RouteConstraint", "Something"),
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
};
// Act && Assert
LogValuesAssert.Contains("RouteKey", "id", actualLogValues);
}
public static TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>> CaseSensitivityComparisionData
{
get
{
return new TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>>()
{
{
new[]
{
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
},
new[]
{
new KeyValuePair<string, object>("ROUTEKEY", "id"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
}
},
{
new[]
{
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
},
new[]
{
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteValue", "FAILURE"),
}
}
};
}
}
[Theory]
[MemberData(nameof(CaseSensitivityComparisionData))]
public void DefaultComparer_Performs_CaseSensitiveComparision(
IEnumerable<KeyValuePair<string, object>> expectedValues,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
// Act && Assert
var equalException = Assert.Throws<EqualException>(
() => LogValuesAssert.Contains(expectedValues, actualValues));
Assert.Equal(GetString(expectedValues), equalException.Expected);
Assert.Equal(GetString(actualValues), equalException.Actual);
}
private string GetString(IEnumerable<KeyValuePair<string, object>> logValues)
{
return logValues == null ?
"Null" :
string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]"));
}
}
}

View File

@ -0,0 +1,142 @@
// 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.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.Extensions.Logging.Testing.Tests
{
[ShortClassName]
public class LoggedTestXunitTests : TestLoggedTest
{
private readonly ITestOutputHelper _output;
public LoggedTestXunitTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void ShortClassNameUsedWhenShortClassNameAttributeSpecified()
{
Assert.Equal(GetType().Name, ResolvedTestClassName);
}
[Fact]
public void LoggedTestTestOutputHelperSameInstanceAsInjectedConstructorArg()
{
Assert.Same(_output, TestOutputHelper);
}
[Fact]
public void LoggedFactInitializesLoggedTestProperties()
{
Assert.NotNull(Logger);
Assert.NotNull(LoggerFactory);
Assert.NotNull(TestSink);
Assert.NotNull(TestOutputHelper);
}
[Theory]
[InlineData("Hello world")]
public void LoggedTheoryInitializesLoggedTestProperties(string argument)
{
Assert.NotNull(Logger);
Assert.NotNull(LoggerFactory);
Assert.NotNull(TestSink);
Assert.NotNull(TestOutputHelper);
// Use the test argument
Assert.NotNull(argument);
}
[ConditionalFact]
public void ConditionalLoggedFactGetsInitializedLoggerFactory()
{
Assert.NotNull(Logger);
Assert.NotNull(LoggerFactory);
Assert.NotNull(TestSink);
Assert.NotNull(TestOutputHelper);
}
[ConditionalTheory]
[InlineData("Hello world")]
public void LoggedConditionalTheoryInitializesLoggedTestProperties(string argument)
{
Assert.NotNull(Logger);
Assert.NotNull(LoggerFactory);
Assert.NotNull(TestSink);
Assert.NotNull(TestOutputHelper);
// Use the test argument
Assert.NotNull(argument);
}
[Fact]
[LogLevel(LogLevel.Information)]
public void LoggedFactFilteredByLogLevel()
{
Logger.LogInformation("Information");
Logger.LogDebug("Debug");
var message = Assert.Single(TestSink.Writes);
Assert.Equal(LogLevel.Information, message.LogLevel);
Assert.Equal("Information", message.Formatter(message.State, null));
}
[Theory]
[InlineData("Hello world")]
[LogLevel(LogLevel.Information)]
public void LoggedTheoryFilteredByLogLevel(string argument)
{
Logger.LogInformation("Information");
Logger.LogDebug("Debug");
var message = Assert.Single(TestSink.Writes);
Assert.Equal(LogLevel.Information, message.LogLevel);
Assert.Equal("Information", message.Formatter(message.State, null));
// Use the test argument
Assert.NotNull(argument);
}
[Fact]
public void AddTestLoggingUpdatedWhenLoggerFactoryIsSet()
{
var loggerFactory = new LoggerFactory();
var serviceCollection = new ServiceCollection();
LoggerFactory = loggerFactory;
AddTestLogging(serviceCollection);
Assert.Same(loggerFactory, serviceCollection.BuildServiceProvider().GetRequiredService<ILoggerFactory>());
}
[ConditionalTheory]
[EnvironmentVariableSkipCondition("ASPNETCORE_TEST_LOG_DIR", "")] // The test name is only generated when logging is enabled via the environment variable
[InlineData(null)]
public void LoggedTheoryNullArgumentsAreEscaped(string argument)
{
Assert.NotNull(LoggerFactory);
Assert.Equal($"{nameof(LoggedTheoryNullArgumentsAreEscaped)}_null", ResolvedTestMethodName);
// Use the test argument
Assert.Null(argument);
}
[Fact]
public void AdditionalSetupInvoked()
{
Assert.True(SetupInvoked);
}
}
public class TestLoggedTest : LoggedTest
{
public bool SetupInvoked { get; private set; } = false;
public override void AdditionalSetup()
{
SetupInvoked = true;
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(MSBuildThisFileDirectory)..\..\Logging.Testing\src\build\Microsoft.Extensions.Logging.Testing.props" />
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Logging.Testing" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.Extensions.Logging.Tests" />
</ItemGroup>
</Project>

View File

@ -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;
using System.Text;
using Xunit.Abstractions;
namespace Microsoft.Extensions.Logging.Testing.Tests
{
public class TestTestOutputHelper : ITestOutputHelper
{
private StringBuilder _output = new StringBuilder();
public bool Throw { get; set; }
public string Output => _output.ToString();
public void WriteLine(string message)
{
if (Throw)
{
throw new Exception("Boom!");
}
_output.AppendLine(message);
}
public void WriteLine(string format, params object[] args)
{
if (Throw)
{
throw new Exception("Boom!");
}
_output.AppendLine(string.Format(format, args));
}
}
}

View File

@ -0,0 +1,87 @@
// 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.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Test;
using Xunit;
namespace Microsoft.Extensions.Logging.Testing.Tests
{
public class XunitLoggerProviderTest
{
[Fact]
public void LoggerProviderWritesToTestOutputHelper()
{
var testTestOutputHelper = new TestTestOutputHelper();
var loggerFactory = TestLoggerBuilder.Create(builder => builder
.SetMinimumLevel(LogLevel.Trace)
.AddXunit(testTestOutputHelper));
var logger = loggerFactory.CreateLogger("TestCategory");
logger.LogInformation("This is some great information");
logger.LogTrace("This is some unimportant information");
var expectedOutput =
"| [TIMESTAMP] TestCategory Information: This is some great information" + Environment.NewLine +
"| [TIMESTAMP] TestCategory Trace: This is some unimportant information" + Environment.NewLine;
Assert.Equal(expectedOutput, MakeConsistent(testTestOutputHelper.Output));
}
[Fact]
public void LoggerProviderDoesNotWriteLogMessagesBelowMinimumLevel()
{
var testTestOutputHelper = new TestTestOutputHelper();
var loggerFactory = TestLoggerBuilder.Create(builder => builder
.AddXunit(testTestOutputHelper, LogLevel.Warning));
var logger = loggerFactory.CreateLogger("TestCategory");
logger.LogInformation("This is some great information");
logger.LogError("This is a bad error");
Assert.Equal("| [TIMESTAMP] TestCategory Error: This is a bad error" + Environment.NewLine, MakeConsistent(testTestOutputHelper.Output));
}
[Fact]
public void LoggerProviderPrependsPrefixToEachLine()
{
var testTestOutputHelper = new TestTestOutputHelper();
var loggerFactory = TestLoggerBuilder.Create(builder => builder
.AddXunit(testTestOutputHelper));
var logger = loggerFactory.CreateLogger("TestCategory");
logger.LogInformation("This is a" + Environment.NewLine + "multi-line" + Environment.NewLine + "message");
// The lines after the first one are indented more because the indentation was calculated based on the timestamp's actual length.
var expectedOutput =
"| [TIMESTAMP] TestCategory Information: This is a" + Environment.NewLine +
"| multi-line" + Environment.NewLine +
"| message" + Environment.NewLine;
Assert.Equal(expectedOutput, MakeConsistent(testTestOutputHelper.Output));
}
[Fact]
public void LoggerProviderDoesNotThrowIfOutputHelperThrows()
{
var testTestOutputHelper = new TestTestOutputHelper();
var loggerFactory = TestLoggerBuilder.Create(builder => builder
.AddXunit(testTestOutputHelper));
testTestOutputHelper.Throw = true;
var logger = loggerFactory.CreateLogger("TestCategory");
logger.LogInformation("This is a" + Environment.NewLine + "multi-line" + Environment.NewLine + "message");
Assert.Equal(0, testTestOutputHelper.Output.Length);
}
private static readonly Regex TimestampRegex = new Regex(@"\d+-\d+-\d+T\d+:\d+:\d+");
private string MakeConsistent(string input) => TimestampRegex.Replace(input, "TIMESTAMP");
}
}