Merge pull request #20421 from dotnet/johluo/migrate-more-extensions-logging
Johluo/migrate more extensions logging
This commit is contained in:
commit
8fac70750e
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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")]
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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:");
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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}]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue