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