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:
commit
018907bec0
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsProductComponent>true</IsProductComponent>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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")]
|
||||
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() { }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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")]
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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) };
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue