Merge pull request #20421 from dotnet/johluo/migrate-more-extensions-logging

Johluo/migrate more extensions logging
This commit is contained in:
John Luo 2020-04-03 11:49:22 -07:00 committed by GitHub
commit 8fac70750e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 3037 additions and 21 deletions

View File

@ -152,6 +152,7 @@
$(RepoRoot)src\SiteExtensions\LoggingAggregate\test\**\*.csproj;
$(RepoRoot)src\Shared\**\*.*proj;
$(RepoRoot)src\Tools\**\*.*proj;
$(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj;
$(RepoRoot)src\Middleware\**\*.csproj;
$(RepoRoot)src\Razor\**\*.*proj;
$(RepoRoot)src\Mvc\**\*.*proj;
@ -191,6 +192,7 @@
$(RepoRoot)src\Security\**\src\*.csproj;
$(RepoRoot)src\SiteExtensions\**\src\*.csproj;
$(RepoRoot)src\Tools\**\src\*.csproj;
$(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj;
$(RepoRoot)src\Middleware\**\src\*.csproj;
$(RepoRoot)src\Razor\**\src\*.csproj;
$(RepoRoot)src\Mvc\**\src\*.csproj;

View File

@ -119,8 +119,6 @@ and are generated based on the last package release.
<LatestPackageReference Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftEntityFrameworkCorePackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="$(MicrosoftExtensionsCachingSqlServerPackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="$(MicrosoftExtensionsCachingStackExchangeRedisPackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="$(MicrosoftExtensionsLoggingAzureAppServicesPackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
<LatestPackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="$(MicrosoftIdentityModelClientsActiveDirectoryPackageVersion)" />
<LatestPackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="$(MicrosoftIdentityModelProtocolsOpenIdConnectPackageVersion)" />
<LatestPackageReference Include="Microsoft.IdentityModel.Protocols.WsFederation" Version="$(MicrosoftIdentityModelProtocolsWsFederationPackageVersion)" />

View File

@ -32,6 +32,7 @@
<ProjectReferenceProvider Include="Microsoft.Extensions.ApiDescription.Server" ProjectPath="$(RepoRoot)src\Tools\Extensions.ApiDescription.Server\src\Microsoft.Extensions.ApiDescription.Server.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DeveloperCertificates.XPlat" ProjectPath="$(RepoRoot)src\Tools\FirstRunCertGenerator\src\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj" />
<ProjectReferenceProvider Include="GetDocument.Insider" ProjectPath="$(RepoRoot)src\Tools\GetDocumentInsider\src\GetDocumentInsider.csproj" />
<ProjectReferenceProvider Include="Microsoft.Extensions.Logging.AzureAppServices" ProjectPath="$(RepoRoot)src\Logging.AzureAppServices\src\Microsoft.Extensions.Logging.AzureAppServices.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ConcurrencyLimiter" ProjectPath="$(RepoRoot)src\Middleware\ConcurrencyLimiter\src\Microsoft.AspNetCore.ConcurrencyLimiter.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" ProjectPath="$(RepoRoot)src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.HeaderPropagation" ProjectPath="$(RepoRoot)src\Middleware\HeaderPropagation\src\Microsoft.AspNetCore.HeaderPropagation.csproj" />

View File

@ -161,10 +161,6 @@
<Uri>https://github.com/dotnet/extensions</Uri>
<Sha>03c40031d618f923aa88da125cb078aabde9ebb1</Sha>
</Dependency>
<Dependency Name="Microsoft.Extensions.Logging.AzureAppServices" Version="5.0.0-preview.4.20201.2" CoherentParentDependency="Microsoft.AspNetCore.Razor.Language">
<Uri>https://github.com/dotnet/extensions</Uri>
<Sha>03c40031d618f923aa88da125cb078aabde9ebb1</Sha>
</Dependency>
<Dependency Name="Microsoft.Extensions.Logging.Configuration" Version="5.0.0-preview.4.20201.2" CoherentParentDependency="Microsoft.AspNetCore.Razor.Language">
<Uri>https://github.com/dotnet/extensions</Uri>
<Sha>03c40031d618f923aa88da125cb078aabde9ebb1</Sha>
@ -189,10 +185,6 @@
<Uri>https://github.com/dotnet/extensions</Uri>
<Sha>03c40031d618f923aa88da125cb078aabde9ebb1</Sha>
</Dependency>
<Dependency Name="Microsoft.Extensions.Logging.Testing" Version="5.0.0-preview.4.20201.2" CoherentParentDependency="Microsoft.AspNetCore.Razor.Language">
<Uri>https://github.com/dotnet/extensions</Uri>
<Sha>03c40031d618f923aa88da125cb078aabde9ebb1</Sha>
</Dependency>
<Dependency Name="Microsoft.Extensions.Logging" Version="5.0.0-preview.4.20201.2" CoherentParentDependency="Microsoft.AspNetCore.Razor.Language">
<Uri>https://github.com/dotnet/extensions</Uri>
<Sha>03c40031d618f923aa88da125cb078aabde9ebb1</Sha>

View File

@ -123,14 +123,12 @@
<MicrosoftExtensionsHostingPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsHostingPackageVersion>
<MicrosoftExtensionsHttpPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsHttpPackageVersion>
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingAzureAppServicesPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingAzureAppServicesPackageVersion>
<MicrosoftExtensionsLoggingConfigurationPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingConfigurationPackageVersion>
<MicrosoftExtensionsLoggingConsolePackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingConsolePackageVersion>
<MicrosoftExtensionsLoggingDebugPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingDebugPackageVersion>
<MicrosoftExtensionsLoggingEventSourcePackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingEventSourcePackageVersion>
<MicrosoftExtensionsLoggingEventLogPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingEventLogPackageVersion>
<MicrosoftExtensionsLoggingPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingPackageVersion>
<MicrosoftExtensionsLoggingTestingPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingTestingPackageVersion>
<MicrosoftExtensionsLoggingTraceSourcePackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsLoggingTraceSourcePackageVersion>
<MicrosoftExtensionsOptionsConfigurationExtensionsPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsOptionsConfigurationExtensionsPackageVersion>
<MicrosoftExtensionsOptionsDataAnnotationsPackageVersion>5.0.0-preview.4.20201.2</MicrosoftExtensionsOptionsDataAnnotationsPackageVersion>

View File

@ -14,8 +14,8 @@
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
<Reference Include="Microsoft.AspNetCore.Testing" />
<Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
</ItemGroup>
<ItemGroup>

View File

@ -23,7 +23,6 @@
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
<Reference Include="Microsoft.NETCore.Windows.ApiSets" />
<Reference Include="Serilog.Extensions.Logging" />
<Reference Include="Serilog.Sinks.File" />

View File

@ -0,0 +1,8 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<!-- These projects depend on a 3rd party source -->
<ExcludeFromSourceBuild>true</ExcludeFromSourceBuild>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,98 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.AzureAppServices;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Options;
using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
namespace Microsoft.Extensions.Logging
{
/// <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;
}
builder.AddConfiguration();
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));
LoggerProviderOptions.RegisterProviderOptions<AzureFileLoggerOptions, FileLoggerProvider>(builder.Services);
}
if (addedBlobLogger)
{
services.AddSingleton<IConfigureOptions<LoggerFilterOptions>>(CreateBlobFilterConfigureOptions(config));
services.AddSingleton<IConfigureOptions<AzureBlobLoggerOptions>>(new BlobLoggerConfigureOptions(config, context));
services.AddSingleton<IOptionsChangeTokenSource<AzureBlobLoggerOptions>>(
new ConfigurationChangeTokenSource<AzureBlobLoggerOptions>(config));
LoggerProviderOptions.RegisterProviderOptions<AzureBlobLoggerOptions, BlobLoggerProvider>(builder.Services);
}
return builder;
}
private static bool TryAddEnumerable(IServiceCollection collection, ServiceDescriptor descriptor)
{
var beforeCount = collection.Count;
collection.TryAddEnumerable(descriptor);
return beforeCount != collection.Count;
}
private static ConfigurationBasedLevelSwitcher CreateBlobFilterConfigureOptions(IConfiguration config)
{
return new ConfigurationBasedLevelSwitcher(
configuration: config,
provider: typeof(BlobLoggerProvider),
levelKey: "AzureBlobTraceLevel");
}
private static ConfigurationBasedLevelSwitcher CreateFileFilterConfigureOptions(IConfiguration config)
{
return new ConfigurationBasedLevelSwitcher(
configuration: config,
provider: typeof(FileLoggerProvider),
levelKey: "AzureDriveTraceLevel");
}
}
}

View File

@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <summary>
/// Options for Azure diagnostics blob logging.
/// </summary>
public class AzureBlobLoggerOptions: BatchingLoggerOptions
{
private string _blobName = "applicationLog.txt";
/// <summary>
/// Gets or sets the last section of log blob name.
/// Defaults to <c>"applicationLog.txt"</c>.
/// </summary>
public string BlobName
{
get { return _blobName; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(nameof(value), $"{nameof(BlobName)} must be non-empty string.");
}
_blobName = value;
}
}
internal string ContainerUrl { get; set; }
internal string ApplicationName { get; set; }
internal string ApplicationInstanceId { get; set; }
}
}

View File

@ -0,0 +1,73 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <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 string representing the prefix of the file name used to store the logging information.
/// The current date, in the format YYYYMMDD will be added after the given value.
/// Defaults to <c>diagnostics-</c>.
/// </summary>
public string FileName
{
get { return _fileName; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(nameof(value));
}
_fileName = value;
}
}
internal string LogDirectory { get; set; }
}
}

View File

@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
internal class BatchLoggerConfigureOptions : IConfigureOptions<BatchingLoggerOptions>
{
private readonly IConfiguration _configuration;
private readonly string _isEnabledKey;
public BatchLoggerConfigureOptions(IConfiguration configuration, string isEnabledKey)
{
_configuration = configuration;
_isEnabledKey = isEnabledKey;
}
public void Configure(BatchingLoggerOptions options)
{
options.IsEnabled = TextToBoolean(_configuration.GetSection(_isEnabledKey)?.Value);
}
private static bool TextToBoolean(string text)
{
if (string.IsNullOrEmpty(text) ||
!bool.TryParse(text, out var result))
{
result = false;
}
return result;
}
}
}

View File

@ -0,0 +1,75 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Text;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
internal class BatchingLogger : ILogger
{
private readonly BatchingLoggerProvider _provider;
private readonly string _category;
public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName)
{
_provider = loggerProvider;
_category = categoryName;
}
public IDisposable BeginScope<TState>(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);
var scopeProvider = _provider.ScopeProvider;
if (scopeProvider != null)
{
scopeProvider.ForEachScope((scope, stringBuilder) =>
{
stringBuilder.Append(" => ").Append(scope);
}, builder);
builder.AppendLine(":");
}
else
{
builder.Append(": ");
}
builder.AppendLine(formatter(state, exception));
if (exception != null)
{
builder.AppendLine(exception.ToString());
}
_provider.AddMessage(timestamp, builder.ToString());
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter);
}
}
}

View File

@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <summary>
/// Options for a logger which batches up log messages.
/// </summary>
public class BatchingLoggerOptions
{
private int? _batchSize;
private int? _backgroundQueueSize = 1000;
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>1000</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>
/// Defaults to <c>null</c>.
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; }
/// <summary>
/// Gets or sets a value indicating whether scopes should be included in the message.
/// Defaults to <c>false</c>.
/// </summary>
public bool IncludeScopes { get; set; } = false;
}
}

View File

@ -0,0 +1,208 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <summary>
/// A provider of <see cref="BatchingLogger"/> instances.
/// </summary>
public abstract class BatchingLoggerProvider : ILoggerProvider, ISupportExternalScope
{
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 int _messagesDropped;
private BlockingCollection<LogMessage> _messageQueue;
private Task _outputTask;
private CancellationTokenSource _cancellationTokenSource;
private bool _includeScopes;
private IExternalScopeProvider _scopeProvider;
internal IExternalScopeProvider ScopeProvider => _includeScopes ? _scopeProvider : null;
internal 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);
}
/// <summary>
/// Checks if the queue is enabled.
/// </summary>
public bool IsEnabled { get; private set; }
private void UpdateOptions(BatchingLoggerOptions options)
{
var oldIsEnabled = IsEnabled;
IsEnabled = options.IsEnabled;
_includeScopes = options.IncludeScopes;
if (oldIsEnabled != IsEnabled)
{
if (IsEnabled)
{
Start();
}
else
{
Stop();
}
}
}
internal abstract Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token);
private async Task ProcessLogQueue()
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
var limit = _batchSize ?? int.MaxValue;
while (limit > 0 && _messageQueue.TryTake(out var message))
{
_currentBatch.Add(message);
limit--;
}
var messagesDropped = Interlocked.Exchange(ref _messagesDropped, 0);
if (messagesDropped != 0)
{
_currentBatch.Add(new LogMessage(DateTimeOffset.Now, $"{messagesDropped} message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this.{Environment.NewLine}"));
}
if (_currentBatch.Count > 0)
{
try
{
await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token);
}
catch
{
// ignored
}
_currentBatch.Clear();
}
else
{
await IntervalAsync(_interval, _cancellationTokenSource.Token);
}
}
}
/// <summary>
/// Wait for the given <see cref="TimeSpan"/>.
/// </summary>
/// <param name="interval">The amount of time to wait.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the delay.</param>
/// <returns>A <see cref="Task"/> which completes when the <paramref name="interval"/> has passed or the <paramref name="cancellationToken"/> has been canceled.</returns>
protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return Task.Delay(interval, cancellationToken);
}
internal void AddMessage(DateTimeOffset timestamp, string message)
{
if (!_messageQueue.IsAddingCompleted)
{
try
{
if (!_messageQueue.TryAdd(new LogMessage(timestamp, message), millisecondsTimeout: 0, cancellationToken: _cancellationTokenSource.Token))
{
Interlocked.Increment(ref _messagesDropped);
}
}
catch
{
//cancellation token canceled or CompleteAdding called
}
}
}
private void Start()
{
_messageQueue = _queueSize == null ?
new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>()) :
new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>(), _queueSize.Value);
_cancellationTokenSource = new CancellationTokenSource();
_outputTask = Task.Run(ProcessLogQueue);
}
private void Stop()
{
_cancellationTokenSource.Cancel();
_messageQueue.CompleteAdding();
try
{
_outputTask.Wait(_interval);
}
catch (TaskCanceledException)
{
}
catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException)
{
}
}
/// <inheritdoc/>
public void Dispose()
{
_optionsChangeToken?.Dispose();
if (IsEnabled)
{
Stop();
}
}
/// <summary>
/// Creates a <see cref="BatchingLogger"/> with the given <paramref name="categoryName"/>.
/// </summary>
/// <param name="categoryName">The name of the category to create this logger with.</param>
/// <returns>The <see cref="BatchingLogger"/> that was created.</returns>
public ILogger CreateLogger(string categoryName)
{
return new BatchingLogger(this, categoryName);
}
/// <summary>
/// Sets the scope on this provider.
/// </summary>
/// <param name="scopeProvider">Provides the scope.</param>
void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider)
{
_scopeProvider = scopeProvider;
}
}
}

View File

@ -0,0 +1,97 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <inheritdoc />
internal class BlobAppendReferenceWrapper : ICloudAppendBlob
{
private readonly Uri _fullUri;
private readonly HttpClient _client;
private readonly Uri _appendUri;
public BlobAppendReferenceWrapper(string containerUrl, string name, HttpClient client)
{
var uriBuilder = new UriBuilder(containerUrl);
uriBuilder.Path += "/" + name;
_fullUri = uriBuilder.Uri;
AppendBlockQuery(uriBuilder);
_appendUri = uriBuilder.Uri;
_client = client;
}
/// <inheritdoc />
public async Task AppendAsync(ArraySegment<byte> data, CancellationToken cancellationToken)
{
Task<HttpResponseMessage> AppendDataAsync()
{
var message = new HttpRequestMessage(HttpMethod.Put, _appendUri)
{
Content = new ByteArrayContent(data.Array, data.Offset, data.Count)
};
AddCommonHeaders(message);
return _client.SendAsync(message, cancellationToken);
}
var response = await AppendDataAsync();
if (response.StatusCode == HttpStatusCode.NotFound)
{
// If no blob exists try creating it
var message = new HttpRequestMessage(HttpMethod.Put, _fullUri)
{
// Set Content-Length to 0 to create "Append Blob"
Content = new ByteArrayContent(Array.Empty<byte>()),
Headers =
{
{ "If-None-Match", "*" }
}
};
AddCommonHeaders(message);
response = await _client.SendAsync(message, cancellationToken);
// If result is 2** or 412 try to append again
if (response.IsSuccessStatusCode ||
response.StatusCode == HttpStatusCode.PreconditionFailed)
{
// Retry sending data after blob creation
response = await AppendDataAsync();
}
}
response.EnsureSuccessStatusCode();
}
private static void AddCommonHeaders(HttpRequestMessage message)
{
message.Headers.Add("x-ms-blob-type", "AppendBlob");
message.Headers.Add("x-ms-version", "2016-05-31");
message.Headers.Date = DateTimeOffset.UtcNow;
}
private static void AppendBlockQuery(UriBuilder uriBuilder)
{
// See https://msdn.microsoft.com/en-us/library/system.uribuilder.query.aspx for:
// Note: Do not append a string directly to Query property.
// If the length of Query is greater than 1, retrieve the property value
// as a string, remove the leading question mark, append the new query string,
// and set the property with the combined string.
var queryToAppend = "comp=appendblock";
if (uriBuilder.Query != null && uriBuilder.Query.Length > 1)
uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + queryToAppend;
else
uriBuilder.Query = queryToAppend;
}
}
}

View File

@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
internal class BlobLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions<AzureBlobLoggerOptions>
{
private readonly IConfiguration _configuration;
private readonly IWebAppContext _context;
public BlobLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context)
: base(configuration, "AzureBlobEnabled")
{
_configuration = configuration;
_context = context;
}
public void Configure(AzureBlobLoggerOptions options)
{
base.Configure(options);
options.ContainerUrl = _configuration.GetSection("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL")?.Value;
options.ApplicationName = _context.SiteName;
options.ApplicationInstanceId = _context.SiteInstanceId;
}
}
}

View File

@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <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">The options to use when creating a provider.</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">Options to be used in creating a logger.</param>
internal 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();
}
internal override async Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken cancellationToken)
{
var eventGroups = messages.GroupBy(GetBlobKey);
foreach (var eventGroup in eventGroups)
{
var key = eventGroup.Key;
var blobName = $"{_appName}/{key.Year}/{key.Month:00}/{key.Day:00}/{key.Hour:00}/{_fileName}";
var blob = _blobReferenceFactory(blobName);
using (var stream = new MemoryStream())
using (var writer = new StreamWriter(stream))
{
foreach (var logEvent in eventGroup)
{
writer.Write(logEvent.Message);
}
await writer.FlushAsync();
var tryGetBuffer = stream.TryGetBuffer(out var buffer);
Debug.Assert(tryGetBuffer);
await blob.AppendAsync(buffer, cancellationToken);
}
}
}
private (int Year, int Month, int Day, int Hour) GetBlobKey(LogMessage e)
{
return (e.Timestamp.Year,
e.Timestamp.Month,
e.Timestamp.Day,
e.Timestamp.Hour);
}
}
}

View File

@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
internal class ConfigurationBasedLevelSwitcher: IConfigureOptions<LoggerFilterOptions>
{
private readonly IConfiguration _configuration;
private readonly Type _provider;
private readonly string _levelKey;
public ConfigurationBasedLevelSwitcher(IConfiguration configuration, Type provider, string levelKey)
{
_configuration = configuration;
_provider = provider;
_levelKey = levelKey;
}
public void Configure(LoggerFilterOptions options)
{
options.Rules.Add(new LoggerFilterRule(_provider.FullName, null, GetLogLevel(), null));
}
private LogLevel GetLogLevel()
{
return TextToLogLevel(_configuration.GetSection(_levelKey)?.Value);
}
private static LogLevel TextToLogLevel(string text)
{
switch (text?.ToUpperInvariant())
{
case "ERROR":
return LogLevel.Error;
case "WARNING":
return LogLevel.Warning;
case "INFORMATION":
return LogLevel.Information;
case "VERBOSE":
return LogLevel.Trace;
default:
return LogLevel.None;
}
}
}
}

View File

@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
internal class FileLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions<AzureFileLoggerOptions>
{
private readonly IWebAppContext _context;
public FileLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context)
: base(configuration, "AzureDriveEnabled")
{
_context = context;
}
public void Configure(AzureFileLoggerOptions options)
{
base.Configure(options);
options.LogDirectory = Path.Combine(_context.HomeFolder, "LogFiles", "Application");
}
}
}

View File

@ -0,0 +1,89 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <summary>
/// A <see cref="BatchingLoggerProvider"/> which writes out to a file.
/// </summary>
[ProviderAlias("AzureAppServicesFile")]
public class FileLoggerProvider : BatchingLoggerProvider
{
private readonly string _path;
private readonly string _fileName;
private readonly int? _maxFileSize;
private readonly int? _maxRetainedFiles;
/// <summary>
/// Creates a new instance of <see cref="FileLoggerProvider"/>.
/// </summary>
/// <param name="options">The options to use when creating a provider.</param>
public FileLoggerProvider(IOptionsMonitor<AzureFileLoggerOptions> options) : base(options)
{
var loggerOptions = options.CurrentValue;
_path = loggerOptions.LogDirectory;
_fileName = loggerOptions.FileName;
_maxFileSize = loggerOptions.FileSizeLimit;
_maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
}
internal override async Task WriteMessagesAsync(IEnumerable<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");
}
private (int Year, int Month, int Day) GetGrouping(LogMessage message)
{
return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day);
}
private void RollFiles()
{
if (_maxRetainedFiles > 0)
{
var files = new DirectoryInfo(_path)
.GetFiles(_fileName + "*")
.OrderByDescending(f => f.Name)
.Skip(_maxRetainedFiles.Value);
foreach (var item in files)
{
item.Delete();
}
}
}
}
}

View File

@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <summary>
/// Represents an append blob, a type of blob where blocks of data are always committed to the end of the blob.
/// </summary>
internal interface ICloudAppendBlob
{
/// <summary>
/// Initiates an asynchronous operation to open a stream for writing to the blob.
/// </summary>
/// <returns>A <see cref="T:System.Threading.Tasks.Task`1" /> object of type <see cref="Stream" /> that represents the asynchronous operation.</returns>
Task AppendAsync(ArraySegment<byte> data, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <summary>
/// Represents an Azure WebApp context
/// </summary>
internal interface IWebAppContext
{
/// <summary>
/// Gets the path to the home folder if running in Azure WebApp
/// </summary>
string HomeFolder { get; }
/// <summary>
/// Gets the name of site if running in Azure WebApp
/// </summary>
string SiteName { get; }
/// <summary>
/// Gets the id of site if running in Azure WebApp
/// </summary>
string SiteInstanceId { get; }
/// <summary>
/// Gets a value indicating whether or new we're in an Azure WebApp
/// </summary>
bool IsRunningInAzureWebApp { get; }
}
}

View File

@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
internal readonly struct LogMessage
{
public LogMessage(DateTimeOffset timestamp, string message)
{
Timestamp = timestamp;
Message = message;
}
public DateTimeOffset Timestamp { get; }
public string Message { get; }
}
}

View File

@ -0,0 +1,24 @@
<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>
<IsPackable>true</IsPackable>
<IsShipping>true</IsShipping>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Extensions.Logging.AzureAppServices.Tests" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<Reference Include="Microsoft.Extensions.Configuration.Json" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Logging.Configuration" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="System.ValueTuple" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.IO;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
internal class SiteConfigurationProvider
{
public static IConfiguration GetAzureLoggingConfiguration(IWebAppContext context)
{
var settingsFolder = Path.Combine(context.HomeFolder, "site", "diagnostics");
var settingsFile = Path.Combine(settingsFolder, "settings.json");
return new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddJsonFile(settingsFile, optional: true, reloadOnChange: true)
.Build();
}
}
}

View File

@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.Extensions.Logging.AzureAppServices
{
/// <summary>
/// Represents the default implementation of <see cref="IWebAppContext"/>.
/// </summary>
internal class WebAppContext : IWebAppContext
{
/// <summary>
/// Gets the default instance of the WebApp context.
/// </summary>
public static WebAppContext Default { get; } = new WebAppContext();
private WebAppContext() { }
/// <inheritdoc />
public string HomeFolder { get; } = Environment.GetEnvironmentVariable("HOME");
/// <inheritdoc />
public string SiteName { get; } = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME");
/// <inheritdoc />
public string SiteInstanceId { get; } = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID");
/// <inheritdoc />
public bool IsRunningInAzureWebApp => !string.IsNullOrEmpty(HomeFolder) &&
!string.IsNullOrEmpty(SiteName);
}
}

View File

@ -0,0 +1,186 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class AzureAppendBlobTests
{
public string _containerUrl = "https://host/container?query=1";
public string _blobName = "blob/path";
[Fact]
public async Task SendsDataAsStream()
{
var testMessageHandler = new TestMessageHandler(async message =>
{
Assert.Equal(HttpMethod.Put, message.Method);
Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
AssertDefaultHeaders(message);
return new HttpResponseMessage(HttpStatusCode.OK);
});
var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
await blob.AppendAsync(new ArraySegment<byte>(new byte[] { 0, 2, 3 }), CancellationToken.None);
}
private static void AssertDefaultHeaders(HttpRequestMessage message)
{
Assert.Equal(new[] {"AppendBlob"}, message.Headers.GetValues("x-ms-blob-type"));
Assert.Equal(new[] {"2016-05-31"}, message.Headers.GetValues("x-ms-version"));
Assert.NotNull(message.Headers.Date);
}
[Theory]
[InlineData(HttpStatusCode.Created)]
[InlineData(HttpStatusCode.PreconditionFailed)]
public async Task CreatesBlobIfNotExist(HttpStatusCode createStatusCode)
{
var stage = 0;
var testMessageHandler = new TestMessageHandler(async message =>
{
// First PUT request
if (stage == 0)
{
Assert.Equal(HttpMethod.Put, message.Method);
Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
Assert.Equal(3, message.Content.Headers.ContentLength);
AssertDefaultHeaders(message);
stage++;
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
// Create request
if (stage == 1)
{
Assert.Equal(HttpMethod.Put, message.Method);
Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString());
Assert.Equal(0, message.Content.Headers.ContentLength);
Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match"));
AssertDefaultHeaders(message);
stage++;
return new HttpResponseMessage(createStatusCode);
}
// First PUT request
if (stage == 2)
{
Assert.Equal(HttpMethod.Put, message.Method);
Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
Assert.Equal(3, message.Content.Headers.ContentLength);
AssertDefaultHeaders(message);
stage++;
return new HttpResponseMessage(HttpStatusCode.Created);
}
throw new NotImplementedException();
});
var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
await blob.AppendAsync(new ArraySegment<byte>(new byte[] { 0, 2, 3 }), CancellationToken.None);
Assert.Equal(3, stage);
}
[Fact]
public async Task ThrowsForUnknownStatus()
{
var stage = 0;
var testMessageHandler = new TestMessageHandler(async message =>
{
// First PUT request
if (stage == 0)
{
Assert.Equal(HttpMethod.Put, message.Method);
Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
Assert.Equal(3, message.Content.Headers.ContentLength);
AssertDefaultHeaders(message);
stage++;
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
throw new NotImplementedException();
});
var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
await Assert.ThrowsAsync<HttpRequestException>(() => blob.AppendAsync(new ArraySegment<byte>(new byte[] { 0, 2, 3 }), CancellationToken.None));
Assert.Equal(1, stage);
}
[Fact]
public async Task ThrowsForUnknownStatusDuringCreation()
{
var stage = 0;
var testMessageHandler = new TestMessageHandler(async message =>
{
// First PUT request
if (stage == 0)
{
Assert.Equal(HttpMethod.Put, message.Method);
Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
Assert.Equal(3, message.Content.Headers.ContentLength);
AssertDefaultHeaders(message);
stage++;
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
// Create request
if (stage == 1)
{
Assert.Equal(HttpMethod.Put, message.Method);
Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString());
Assert.Equal(0, message.Content.Headers.ContentLength);
Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match"));
AssertDefaultHeaders(message);
stage++;
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
throw new NotImplementedException();
});
var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
await Assert.ThrowsAsync<HttpRequestException>(() => blob.AppendAsync(new ArraySegment<byte>(new byte[] { 0, 2, 3 }), CancellationToken.None));
Assert.Equal(2, stage);
}
private class TestMessageHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, Task<HttpResponseMessage>> _callback;
public TestMessageHandler(Func<HttpRequestMessage, Task<HttpResponseMessage>> callback)
{
_callback = callback;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _callback(request);
}
}
}
}

View File

@ -0,0 +1,98 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class AzureBlobSinkTests
{
DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
[Fact]
public async Task WritesMessagesInBatches()
{
var blob = new Mock<ICloudAppendBlob>();
var buffers = new List<byte[]>();
blob.Setup(b => b.AppendAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))
.Callback((ArraySegment<byte> s, CancellationToken ct) => buffers.Add(ToArray(s)))
.Returns(Task.CompletedTask);
var sink = new TestBlobSink(name => blob.Object);
var logger = (BatchingLogger)sink.CreateLogger("Cat");
await sink.IntervalControl.Pause;
for (int i = 0; i < 5; i++)
{
logger.Log(_timestampOne, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state);
}
sink.IntervalControl.Resume();
await sink.IntervalControl.Pause;
Assert.Single(buffers);
Assert.Equal(
"2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 0" + Environment.NewLine +
"2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 1" + Environment.NewLine +
"2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 2" + Environment.NewLine +
"2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 3" + Environment.NewLine +
"2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 4" + Environment.NewLine,
Encoding.UTF8.GetString(buffers[0]));
}
[Fact]
public async Task GroupsByHour()
{
var blob = new Mock<ICloudAppendBlob>();
var buffers = new List<byte[]>();
var names = new List<string>();
blob.Setup(b => b.AppendAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))
.Callback((ArraySegment<byte> s, CancellationToken ct) => buffers.Add(ToArray(s)))
.Returns(Task.CompletedTask);
var sink = new TestBlobSink(name =>
{
names.Add(name);
return blob.Object;
});
var logger = (BatchingLogger)sink.CreateLogger("Cat");
await sink.IntervalControl.Pause;
var startDate = _timestampOne;
for (int i = 0; i < 3; i++)
{
logger.Log(startDate, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state);
startDate = startDate.AddHours(1);
}
sink.IntervalControl.Resume();
await sink.IntervalControl.Pause;
Assert.Equal(3, buffers.Count);
Assert.Equal("appname/2016/05/04/03/42_filename", names[0]);
Assert.Equal("appname/2016/05/04/04/42_filename", names[1]);
Assert.Equal("appname/2016/05/04/05/42_filename", names[2]);
}
private byte[] ToArray(ArraySegment<byte> inputStream)
{
return inputStream.Array
.Skip(inputStream.Offset)
.Take(inputStream.Count)
.ToArray();
}
}
}

View File

@ -0,0 +1,70 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Moq;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class AzureDiagnosticsConfigurationProviderTests
{
[Fact]
public void NoConfigFile()
{
var tempFolder = Path.Combine(Path.GetTempPath(), "AzureWebAppLoggerThisFolderShouldNotExist");
var contextMock = new Mock<IWebAppContext>();
contextMock.SetupGet(c => c.HomeFolder)
.Returns(tempFolder);
var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object);
Assert.NotNull(config);
}
[Fact]
public void ReadsSettingsFileAndEnvironment()
{
var tempFolder = Path.Combine(Path.GetTempPath(), "WebAppLoggerConfigurationDisabledInSettingsFile");
try
{
var settingsFolder = Path.Combine(tempFolder, "site", "diagnostics");
var settingsFile = Path.Combine(settingsFolder, "settings.json");
if (!Directory.Exists(settingsFolder))
{
Directory.CreateDirectory(settingsFolder);
}
Environment.SetEnvironmentVariable("RANDOM_ENVIRONMENT_VARIABLE", "USEFUL_VALUE");
File.WriteAllText(settingsFile, @"{ ""key"":""test value"" }");
var contextMock = new Mock<IWebAppContext>();
contextMock.SetupGet(c => c.HomeFolder)
.Returns(tempFolder);
var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object);
Assert.Equal("test value", config["key"]);
Assert.Equal("USEFUL_VALUE", config["RANDOM_ENVIRONMENT_VARIABLE"]);
}
finally
{
if (Directory.Exists(tempFolder))
{
try
{
Directory.Delete(tempFolder, recursive: true);
}
catch
{
// Don't break the test if temp folder deletion fails.
}
}
}
}
}
}

View File

@ -0,0 +1,136 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class BatchingLoggerProviderTests
{
private DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
private string _nl = Environment.NewLine;
private Regex _timeStampRegex = new Regex(@"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} .\d{2}:\d{2} ");
[Fact]
public async Task LogsInIntervals()
{
var provider = new TestBatchingLoggingProvider();
var logger = (BatchingLogger)provider.CreateLogger("Cat");
await provider.IntervalControl.Pause;
logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
provider.IntervalControl.Resume();
await provider.IntervalControl.Pause;
Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[0][1].Message);
}
[Fact]
public async Task IncludesScopes()
{
var provider = new TestBatchingLoggingProvider(includeScopes: true);
var factory = new LoggerFactory(new [] { provider });
var logger = factory.CreateLogger("Cat");
await provider.IntervalControl.Pause;
using (logger.BeginScope("Scope"))
{
using (logger.BeginScope("Scope2"))
{
logger.Log(LogLevel.Information, 0, "Info message", null, (state, ex) => state);
}
}
provider.IntervalControl.Resume();
await provider.IntervalControl.Pause;
Assert.Matches(_timeStampRegex, provider.Batches[0][0].Message);
Assert.EndsWith(
" [Information] Cat => Scope => Scope2:" + _nl +
"Info message" + _nl,
provider.Batches[0][0].Message);
}
[Fact]
public async Task RespectsBatchSize()
{
var provider = new TestBatchingLoggingProvider(maxBatchSize: 1);
var logger = (BatchingLogger)provider.CreateLogger("Cat");
await provider.IntervalControl.Pause;
logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
provider.IntervalControl.Resume();
await provider.IntervalControl.Pause;
Assert.Equal(2, provider.Batches.Count);
Assert.Single(provider.Batches[0]);
Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
Assert.Single(provider.Batches[1]);
Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[1][0].Message);
}
[Fact]
public async Task DropsMessagesWhenReachingMaxQueue()
{
var provider = new TestBatchingLoggingProvider(maxQueueSize: 1);
var logger = (BatchingLogger)provider.CreateLogger("Cat");
await provider.IntervalControl.Pause;
logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
provider.IntervalControl.Resume();
await provider.IntervalControl.Pause;
Assert.Equal(2, provider.Batches[0].Length);
Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
Assert.Equal("1 message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this." + _nl, provider.Batches[0][1].Message);
}
private class TestBatchingLoggingProvider: BatchingLoggerProvider
{
public List<LogMessage[]> Batches { get; } = new List<LogMessage[]>();
public ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl();
public TestBatchingLoggingProvider(TimeSpan? interval = null, int? maxBatchSize = null, int? maxQueueSize = null, bool includeScopes = false)
: base(new OptionsWrapperMonitor<BatchingLoggerOptions>(new BatchingLoggerOptions
{
FlushPeriod = interval ?? TimeSpan.FromSeconds(1),
BatchSize = maxBatchSize,
BackgroundQueueSize = maxQueueSize,
IsEnabled = true,
IncludeScopes = includeScopes
}))
{
}
internal override Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token)
{
Batches.Add(messages.ToArray());
return Task.CompletedTask;
}
protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return IntervalControl.IntervalAsync();
}
}
}
}

View File

@ -0,0 +1,71 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Extensions.Configuration;
using Moq;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class ConfigureOptionsTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
[InlineData(null)]
public void InitializesIsEnabled(bool? enabled)
{
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("IsEnabledKey", Convert.ToString(enabled))
}).Build();
var options = new BatchingLoggerOptions();
new BatchLoggerConfigureOptions(configuration, "IsEnabledKey").Configure(options);
Assert.Equal(enabled ?? false, options.IsEnabled);
}
[Fact]
public void InitializesLogDirectory()
{
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
}).Build();
var contextMock = new Mock<IWebAppContext>();
contextMock.SetupGet(c => c.HomeFolder).Returns("Home");
var options = new AzureFileLoggerOptions();
new FileLoggerConfigureOptions(configuration, contextMock.Object).Configure(options);
Assert.Equal(Path.Combine("Home", "LogFiles", "Application"), options.LogDirectory);
}
[Fact]
public void InitializesBlobUriSiteInstanceAndName()
{
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new []
{
new KeyValuePair<string, string>("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
}).Build();
var contextMock = new Mock<IWebAppContext>();
contextMock.SetupGet(c => c.HomeFolder).Returns("Home");
contextMock.SetupGet(c => c.SiteInstanceId).Returns("InstanceId");
contextMock.SetupGet(c => c.SiteName).Returns("Name");
var options = new AzureBlobLoggerOptions();
new BlobLoggerConfigureOptions(configuration, contextMock.Object).Configure(options);
Assert.Equal("http://container/url", options.ContainerUrl);
Assert.Equal("InstanceId", options.ApplicationInstanceId);
Assert.Equal("Name", options.ApplicationName);
}
}
}

View File

@ -0,0 +1,122 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class FileLoggerTests: IDisposable
{
DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
public FileLoggerTests()
{
TempPath = Path.GetTempFileName() + "_";
}
public string TempPath { get; }
public void Dispose()
{
try
{
if (Directory.Exists(TempPath))
{
Directory.Delete(TempPath, true);
}
}
catch
{
// ignored
}
}
[Fact]
public async Task WritesToTextFile()
{
var provider = new TestFileLoggerProvider(TempPath);
var logger = (BatchingLogger)provider.CreateLogger("Cat");
await provider.IntervalControl.Pause;
logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
provider.IntervalControl.Resume();
await provider.IntervalControl.Pause;
Assert.Equal(
"2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine +
"2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine,
File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt")));
}
[Fact]
public async Task RollsTextFile()
{
var provider = new TestFileLoggerProvider(TempPath);
var logger = (BatchingLogger)provider.CreateLogger("Cat");
await provider.IntervalControl.Pause;
logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
logger.Log(_timestampOne.AddDays(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
provider.IntervalControl.Resume();
await provider.IntervalControl.Pause;
Assert.Equal(
"2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine,
File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt")));
Assert.Equal(
"2016-05-05 03:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine,
File.ReadAllText(Path.Combine(TempPath, "LogFile.20160505.txt")));
}
[Fact]
public async Task RespectsMaxFileCount()
{
Directory.CreateDirectory(TempPath);
File.WriteAllText(Path.Combine(TempPath, "randomFile.txt"), "Text");
var provider = new TestFileLoggerProvider(TempPath, maxRetainedFiles: 5);
var logger = (BatchingLogger)provider.CreateLogger("Cat");
await provider.IntervalControl.Pause;
var timestamp = _timestampOne;
for (int i = 0; i < 10; i++)
{
logger.Log(timestamp, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
logger.Log(timestamp.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
timestamp = timestamp.AddDays(1);
}
provider.IntervalControl.Resume();
await provider.IntervalControl.Pause;
var actualFiles = new DirectoryInfo(TempPath)
.GetFiles()
.Select(f => f.Name)
.OrderBy(f => f)
.ToArray();
Assert.Equal(6, actualFiles.Length);
Assert.Equal(new[] {
"LogFile.20160509.txt",
"LogFile.20160510.txt",
"LogFile.20160511.txt",
"LogFile.20160512.txt",
"LogFile.20160513.txt",
"randomFile.txt"
}, actualFiles);
}
}
}

View File

@ -0,0 +1,79 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class LoggerBuilderExtensionsTests
{
private IWebAppContext _appContext;
public LoggerBuilderExtensionsTests()
{
var contextMock = new Mock<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>)));
}
[Fact]
public void LoggerProviderIsResolvable()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
var serviceProvider = serviceCollection.BuildServiceProvider();
var loggerFactory = serviceProvider.GetService<ILoggerProvider>();
}
}
}

View File

@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
internal class ManualIntervalControl
{
private TaskCompletionSource<object> _pauseCompletionSource = new TaskCompletionSource<object>();
private TaskCompletionSource<object> _resumeCompletionSource;
public Task Pause => _pauseCompletionSource.Task;
public void Resume()
{
_pauseCompletionSource = new TaskCompletionSource<object>();
_resumeCompletionSource.SetResult(null);
}
public async Task IntervalAsync()
{
_resumeCompletionSource = new TaskCompletionSource<object>();
_pauseCompletionSource.SetResult(null);
await _resumeCompletionSource.Task;
}
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(DefaultNetCoreTargetFramework);net472</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Configuration.Abstractions" />
<Reference Include="Microsoft.Extensions.Configuration.Binder" />
<Reference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<Reference Include="Microsoft.Extensions.Configuration.FileExtensions" />
<Reference Include="Microsoft.Extensions.Configuration.Json" />
<Reference Include="Microsoft.Extensions.Configuration" />
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
<Reference Include="Microsoft.Extensions.FileProviders.Physical" />
<Reference Include="Microsoft.Extensions.FileSystemGlobbing" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Logging.AzureAppServices" />
<Reference Include="Microsoft.Extensions.Logging.Configuration" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.Extensions.Options" />
<Reference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<Reference Include="Microsoft.Extensions.Primitives" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
internal class OptionsWrapperMonitor<T> : IOptionsMonitor<T>
{
public OptionsWrapperMonitor(T currentValue)
{
CurrentValue = currentValue;
}
public IDisposable OnChange(Action<T, string> listener)
{
return null;
}
public T Get(string name) => CurrentValue;
public T CurrentValue { get; }
}
}

View File

@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
internal class TestBlobSink : BlobLoggerProvider
{
internal ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl();
public TestBlobSink(Func<string, ICloudAppendBlob> blobReferenceFactory) : base(
new OptionsWrapperMonitor<AzureBlobLoggerOptions>(new AzureBlobLoggerOptions()
{
ApplicationInstanceId = "42",
ApplicationName = "appname",
BlobName = "filename",
IsEnabled = true
}),
blobReferenceFactory)
{
}
protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return IntervalControl.IntervalAsync();
}
}
}

View File

@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
internal class TestFileLoggerProvider : FileLoggerProvider
{
internal ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl();
public TestFileLoggerProvider(
string path,
string fileName = "LogFile.",
int maxFileSize = 32_000,
int maxRetainedFiles = 100)
: base(new OptionsWrapperMonitor<AzureFileLoggerOptions>(new AzureFileLoggerOptions()
{
LogDirectory = path,
FileName = fileName,
FileSizeLimit = maxFileSize,
RetainedFileCountLimit = maxRetainedFiles,
IsEnabled = true
}))
{
}
protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return IntervalControl.IntervalAsync();
}
}
}

View File

@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Xunit;
namespace Microsoft.Extensions.Logging.AzureAppServices.Test
{
public class WebConfigurationLevelSwitchTests
{
[Theory]
[InlineData("Error", LogLevel.Error)]
[InlineData("Warning", LogLevel.Warning)]
[InlineData("Information", LogLevel.Information)]
[InlineData("Verbose", LogLevel.Trace)]
[InlineData("ABCD", LogLevel.None)]
public void AddsRuleWithCorrectLevel(string levelValue, LogLevel expectedLevel)
{
var configuration = new ConfigurationBuilder().AddInMemoryCollection(
new[]
{
new KeyValuePair<string, string>("levelKey", levelValue),
})
.Build();
var levelSwitcher = new ConfigurationBasedLevelSwitcher(configuration, typeof(TestFileLoggerProvider), "levelKey");
var filterConfiguration = new LoggerFilterOptions();
levelSwitcher.Configure(filterConfiguration);
Assert.Equal(1, filterConfiguration.Rules.Count);
var rule = filterConfiguration.Rules[0];
Assert.Equal(typeof(TestFileLoggerProvider).FullName, rule.ProviderName);
Assert.Equal(expectedLevel, rule.LogLevel);
}
}
}

View File

@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
}
catch (Exception ex)
{
Logger.LogError($"Certificate is invalid. Issuer name: {cert.Issuer}");
Logger.LogError($"Certificate is invalid. Issuer name: {cert?.Issuer}");
using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
{
Logger.LogError($"List of current certificates in root store:");

View File

@ -7,8 +7,8 @@
<ItemGroup>
<ProjectReference Include="$(RepoRoot)src\Servers\IIS\IntegrationTesting.IIS\src\Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj" />
<Reference Include="Microsoft.AspNetCore.Hosting" />
<Reference Include="Microsoft.AspNetCore.Testing" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
<Reference Include="System.Diagnostics.EventLog" />
</ItemGroup>

View File

@ -20,7 +20,5 @@
<ItemGroup Condition=" '$(IsTestProject)' == 'true' ">
<None Include="$(MSBuildThisFileDirectory)xunit.runner.json" Link="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
</ItemGroup>
</Project>

View File

@ -21,7 +21,6 @@
<Content Include="$(MSBuildThisFileDirectory)xunit.runner.json" Link="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Reference Include="Microsoft.Extensions.Logging.Testing" />
</ItemGroup>
</Project>

View File

@ -21,7 +21,6 @@
<Reference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" />
<Reference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" />
<Reference Include="Microsoft.AspNetCore.Testing" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
<!-- Avoid CS1705 errors due to mix of assemblies brought in transitively. -->
<Reference Include="Microsoft.AspNetCore.SignalR.Common" />

View File

@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.Extensions.Logging.Testing
{
public class BeginScopeContext
{
public object Scope { get; set; }
public string LoggerName { get; set; }
}
}

View File

@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
namespace Microsoft.Extensions.Logging.Testing
{
public interface ITestSink
{
event Action<WriteContext> MessageLogged;
event Action<BeginScopeContext> ScopeStarted;
Func<WriteContext, bool> WriteEnabled { get; set; }
Func<BeginScopeContext, bool> BeginEnabled { get; set; }
IProducerConsumerCollection<BeginScopeContext> Scopes { get; set; }
IProducerConsumerCollection<WriteContext> Writes { get; set; }
void Write(WriteContext context);
void Begin(BeginScopeContext context);
}
}

View File

@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.Extensions.Logging.Testing
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false)]
public class LogLevelAttribute : Attribute
{
public LogLevelAttribute(LogLevel logLevel)
{
LogLevel = logLevel;
}
public LogLevel LogLevel { get; }
}
}

View File

@ -0,0 +1,81 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing
{
public static class LogValuesAssert
{
/// <summary>
/// Asserts that the given key and value are present in the actual values.
/// </summary>
/// <param name="key">The key of the item to be found.</param>
/// <param name="value">The value of the item to be found.</param>
/// <param name="actualValues">The actual values.</param>
public static void Contains(
string key,
object value,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
Contains(new[] { new KeyValuePair<string, object>(key, value) }, actualValues);
}
/// <summary>
/// Asserts that all the expected values are present in the actual values by ignoring
/// the order of values.
/// </summary>
/// <param name="expectedValues">Expected subset of values</param>
/// <param name="actualValues">Actual set of values</param>
public static void Contains(
IEnumerable<KeyValuePair<string, object>> expectedValues,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
if (expectedValues == null)
{
throw new ArgumentNullException(nameof(expectedValues));
}
if (actualValues == null)
{
throw new ArgumentNullException(nameof(actualValues));
}
var comparer = new LogValueComparer();
foreach (var expectedPair in expectedValues)
{
if (!actualValues.Contains(expectedPair, comparer))
{
throw new EqualException(
expected: GetString(expectedValues),
actual: GetString(actualValues));
}
}
}
private static string GetString(IEnumerable<KeyValuePair<string, object>> logValues)
{
return string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]"));
}
private class LogValueComparer : IEqualityComparer<KeyValuePair<string, object>>
{
public bool Equals(KeyValuePair<string, object> x, KeyValuePair<string, object> y)
{
return string.Equals(x.Key, y.Key) && object.Equals(x.Value, y.Value);
}
public int GetHashCode(KeyValuePair<string, object> obj)
{
// We are never going to put this KeyValuePair in a hash table,
// so this is ok.
throw new NotImplementedException();
}
}
}
}

View File

@ -0,0 +1,77 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.Extensions.Logging.Testing
{
public class TestLogger : ILogger
{
private object _scope;
private readonly ITestSink _sink;
private readonly string _name;
private readonly Func<LogLevel, bool> _filter;
public TestLogger(string name, ITestSink sink, bool enabled)
: this(name, sink, _ => enabled)
{
}
public TestLogger(string name, ITestSink sink, Func<LogLevel, bool> filter)
{
_sink = sink;
_name = name;
_filter = filter;
}
public string Name { get; set; }
public IDisposable BeginScope<TState>(TState state)
{
_scope = state;
_sink.Begin(new BeginScopeContext()
{
LoggerName = _name,
Scope = state,
});
return TestDisposable.Instance;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
_sink.Write(new WriteContext()
{
LogLevel = logLevel,
EventId = eventId,
State = state,
Exception = exception,
Formatter = (s, e) => formatter((TState)s, e),
LoggerName = _name,
Scope = _scope
});
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None && _filter(logLevel);
}
private class TestDisposable : IDisposable
{
public static readonly TestDisposable Instance = new TestDisposable();
public void Dispose()
{
// intentionally does nothing
}
}
}
}

View File

@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.Extensions.Logging.Testing
{
public class TestLoggerFactory : ILoggerFactory
{
private readonly ITestSink _sink;
private readonly bool _enabled;
public TestLoggerFactory(ITestSink sink, bool enabled)
{
_sink = sink;
_enabled = enabled;
}
public ILogger CreateLogger(string name)
{
return new TestLogger(name, _sink, _enabled);
}
public void AddProvider(ILoggerProvider provider)
{
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.Extensions.Logging.Testing
{
public class TestLoggerProvider : ILoggerProvider
{
private readonly ITestSink _sink;
public TestLoggerProvider(ITestSink sink)
{
_sink = sink;
}
public ILogger CreateLogger(string categoryName)
{
return new TestLogger(categoryName, _sink, enabled: true);
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.Extensions.Logging.Testing
{
public class TestLogger<T> : ILogger
{
private readonly ILogger _logger;
public TestLogger(TestLoggerFactory factory)
{
_logger = factory.CreateLogger<T>();
}
public IDisposable BeginScope<TState>(TState state)
{
return _logger.BeginScope(state);
}
public bool IsEnabled(LogLevel logLevel)
{
return _logger.IsEnabled(logLevel);
}
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
{
_logger.Log(logLevel, eventId, state, exception, formatter);
}
}
}

View File

@ -0,0 +1,66 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
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 event Action<WriteContext> MessageLogged;
public event Action<BeginScopeContext> ScopeStarted;
public void Write(WriteContext context)
{
if (WriteEnabled == null || WriteEnabled(context))
{
_writes.Enqueue(context);
}
MessageLogged?.Invoke(context);
}
public void Begin(BeginScopeContext context)
{
if (BeginEnabled == null || BeginEnabled(context))
{
_scopes.Enqueue(context);
}
ScopeStarted?.Invoke(context);
}
public static bool EnableWithTypeName<T>(WriteContext context)
{
return context.LoggerName.Equals(typeof(T).FullName);
}
public static bool EnableWithTypeName<T>(BeginScopeContext context)
{
return context.LoggerName.Equals(typeof(T).FullName);
}
}
}

View File

@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.Extensions.Logging.Testing
{
public class WriteContext
{
public LogLevel LogLevel { get; set; }
public EventId EventId { get; set; }
public object State { get; set; }
public Exception Exception { get; set; }
public Func<object, Exception, string> Formatter { get; set; }
public object Scope { get; set; }
public string LoggerName { get; set; }
public string Message
{
get
{
return Formatter(State, Exception);
}
}
}
}

View File

@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Extensions.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 ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart)
{
builder.Services.AddSingleton<ILoggerProvider>(new XunitLoggerProvider(output, minLevel, logStart));
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;
}
public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart)
{
loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel, logStart));
return loggerFactory;
}
}
}

View File

@ -0,0 +1,127 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Text;
using Xunit.Abstractions;
namespace Microsoft.Extensions.Logging.Testing
{
public class XunitLoggerProvider : ILoggerProvider
{
private readonly ITestOutputHelper _output;
private readonly LogLevel _minLevel;
private readonly DateTimeOffset? _logStart;
public XunitLoggerProvider(ITestOutputHelper output)
: this(output, LogLevel.Trace)
{
}
public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel)
: this(output, minLevel, null)
{
}
public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart)
{
_output = output;
_minLevel = minLevel;
_logStart = logStart;
}
public ILogger CreateLogger(string categoryName)
{
return new XunitLogger(_output, categoryName, _minLevel, _logStart);
}
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;
private DateTimeOffset? _logStart;
public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart)
{
_minLogLevel = minLogLevel;
_category = category;
_output = output;
_logStart = logStart;
}
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 = _logStart.HasValue ? $"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds.ToString("N3")}s" : DateTimeOffset.UtcNow.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()
{
}
}
}
}

View File

@ -19,7 +19,9 @@
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Logging.Testing" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.Win32.Registry" />
<Reference Include="Serilog.Extensions.Logging" />
<Reference Include="Serilog.Sinks.File" />

View File

@ -0,0 +1,222 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Sdk;
namespace Microsoft.Extensions.Logging.Testing.Tests
{
public class LogValuesAssertTest
{
public static TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>> ExpectedValues_SubsetOf_ActualValuesData
{
get
{
return new TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>>()
{
{
new KeyValuePair<string,object>[] { },
new KeyValuePair<string,object>[] { }
},
{
// subset
new KeyValuePair<string,object>[] { },
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
}
},
{
// subset
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
},
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteConstraint", "Something")
}
},
{
// equal number of values
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
},
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id"),
}
}
};
}
}
[Theory]
[MemberData(nameof(ExpectedValues_SubsetOf_ActualValuesData))]
public void Asserts_Success_ExpectedValues_SubsetOf_ActualValues(
IEnumerable<KeyValuePair<string, object>> expectedValues,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
// Act && Assert
LogValuesAssert.Contains(expectedValues, actualValues);
}
public static TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>> ExpectedValues_MoreThan_ActualValuesData
{
get
{
return new TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>>()
{
{
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
},
new KeyValuePair<string,object>[] { }
},
{
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteConstraint", "Something")
},
new[]
{
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
}
}
};
}
}
[Theory]
[MemberData(nameof(ExpectedValues_MoreThan_ActualValuesData))]
public void Asserts_Failure_ExpectedValues_MoreThan_ActualValues(
IEnumerable<KeyValuePair<string, object>> expectedValues,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
// Act && Assert
var equalException = Assert.Throws<EqualException>(
() => LogValuesAssert.Contains(expectedValues, actualValues));
Assert.Equal(GetString(expectedValues), equalException.Expected);
Assert.Equal(GetString(actualValues), equalException.Actual);
}
[Fact]
public void Asserts_Success_IgnoringOrderOfItems()
{
// Arrange
var expectedLogValues = new[]
{
new KeyValuePair<string, object>("RouteConstraint", "Something"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
new KeyValuePair<string, object>("RouteKey", "id")
};
var actualLogValues = new[]
{
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteConstraint", "Something"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
};
// Act && Assert
LogValuesAssert.Contains(expectedLogValues, actualLogValues);
}
[Fact]
public void Asserts_Success_OnSpecifiedKeyAndValue()
{
// Arrange
var actualLogValues = new[]
{
new KeyValuePair<string, object>("RouteConstraint", "Something"),
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
};
// Act && Assert
LogValuesAssert.Contains("RouteKey", "id", actualLogValues);
}
public static TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>> CaseSensitivityComparisionData
{
get
{
return new TheoryData<
IEnumerable<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>>()
{
{
new[]
{
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
},
new[]
{
new KeyValuePair<string, object>("ROUTEKEY", "id"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
}
},
{
new[]
{
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteValue", "Failure"),
},
new[]
{
new KeyValuePair<string, object>("RouteKey", "id"),
new KeyValuePair<string, object>("RouteValue", "FAILURE"),
}
}
};
}
}
[Theory]
[MemberData(nameof(CaseSensitivityComparisionData))]
public void DefaultComparer_Performs_CaseSensitiveComparision(
IEnumerable<KeyValuePair<string, object>> expectedValues,
IEnumerable<KeyValuePair<string, object>> actualValues)
{
// Act && Assert
var equalException = Assert.Throws<EqualException>(
() => LogValuesAssert.Contains(expectedValues, actualValues));
Assert.Equal(GetString(expectedValues), equalException.Expected);
Assert.Equal(GetString(actualValues), equalException.Actual);
}
private string GetString(IEnumerable<KeyValuePair<string, object>> logValues)
{
return logValues == null ?
"Null" :
string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]"));
}
}
}

View File

@ -0,0 +1,95 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.Extensions.Logging.Testing.Tests
{
public class XunitLoggerProviderTest
{
[Fact]
public void LoggerProviderWritesToTestOutputHelper()
{
var testTestOutputHelper = new TestTestOutputHelper();
var loggerFactory = CreateTestLogger(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 = CreateTestLogger(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 = CreateTestLogger(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 = CreateTestLogger(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");
private static ILoggerFactory CreateTestLogger(Action<ILoggingBuilder> configure)
{
return new ServiceCollection()
.AddLogging(configure)
.BuildServiceProvider()
.GetRequiredService<ILoggerFactory>();
}
}
}