Skip to content
This repository was archived by the owner on Dec 13, 2018. It is now read-only.

A logger for Azure WebApps #468

Merged
merged 1 commit into from
Jul 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion Logging.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25123.0
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Logging", "src\Microsoft.Extensions.Logging\Microsoft.Extensions.Logging.xproj", "{19D1B6C5-8A62-4387-8816-C54874D1DF5F}"
EndProject
Expand Down Expand Up @@ -34,6 +34,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Loggin
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Logging.EventSource.Test", "test\Microsoft.Extensions.Logging.EventSource.Test\Microsoft.Extensions.Logging.EventSource.Test.xproj", "{F3B898C3-D441-4207-A92B-420D6E73CA5D}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Logging.AzureWebAppDiagnostics", "src\Microsoft.Extensions.Logging.AzureWebAppDiagnostics\Microsoft.Extensions.Logging.AzureWebAppDiagnostics.xproj", "{854133D5-6252-4A0A-B682-BDBB83B62AE6}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test", "test\Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test\Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Test.xproj", "{B4A43221-DE95-47BB-A2D4-2DC761FC9419}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -190,6 +194,30 @@ Global
{F3B898C3-D441-4207-A92B-420D6E73CA5D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F3B898C3-D441-4207-A92B-420D6E73CA5D}.Release|x86.ActiveCfg = Release|Any CPU
{F3B898C3-D441-4207-A92B-420D6E73CA5D}.Release|x86.Build.0 = Release|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Debug|x86.ActiveCfg = Debug|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Debug|x86.Build.0 = Debug|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Release|Any CPU.Build.0 = Release|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Release|x86.ActiveCfg = Release|Any CPU
{854133D5-6252-4A0A-B682-BDBB83B62AE6}.Release|x86.Build.0 = Release|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Debug|x86.ActiveCfg = Debug|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Debug|x86.Build.0 = Debug|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Release|Any CPU.Build.0 = Release|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Release|x86.ActiveCfg = Release|Any CPU
{B4A43221-DE95-47BB-A2D4-2DC761FC9419}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -208,5 +236,7 @@ Global
{0D190EE0-E305-403D-AC01-DEE71D8DBDB5} = {699DB330-0095-4266-B7B0-3EAB3710CA49}
{84073E58-1802-4525-A9E5-1E6A70DAF0B2} = {699DB330-0095-4266-B7B0-3EAB3710CA49}
{F3B898C3-D441-4207-A92B-420D6E73CA5D} = {09920C51-6220-4D8D-94DC-E70C13446187}
{854133D5-6252-4A0A-B682-BDBB83B62AE6} = {699DB330-0095-4266-B7B0-3EAB3710CA49}
{B4A43221-DE95-47BB-A2D4-2DC761FC9419} = {09920C51-6220-4D8D-94DC-E70C13446187}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.Extensions.Logging.Abstractions.Internal
{
public class NullLogger : ILogger
{
public static NullLogger Instance { get; } = new NullLogger();

private NullLogger()
{
}

public IDisposable BeginScope<TState>(TState state)
{
return NullScope.Instance;
}

public bool IsEnabled(LogLevel logLevel)
{
return false;
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.Extensions.Logging.Abstractions.Internal
{
public class NullLoggerProvider : ILoggerProvider
{
public static NullLoggerProvider Instance { get; } = new NullLoggerProvider();

private NullLoggerProvider()
{
}

public ILogger CreateLogger(string categoryName)
{
return NullLogger.Instance;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can cache NullLoggerProvider instance inside NullLoggerProvider, it's enough to have one singleton.

}

public void Dispose()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.Extensions.Logging.Abstractions.Internal
{
/// <summary>
/// An empty scope without any logic
/// </summary>
public class NullScope : IDisposable
{
public static NullScope Instance { get; } = new NullScope();

private NullScope()
{
}

/// <inheritdoc />
public void Dispose()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.Extensions.Logging.AzureWebAppDiagnostics;
using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal;

namespace Microsoft.Extensions.Logging
{
/// <summary>
/// Extension methods for <see cref="AzureWebAppDiagnosticsLoggerProvider"/>.
/// </summary>
public static class AzureWebAppDiagnosticsFactoryExtensions
{
/// <summary>
/// Adds an Azure Web Apps diagnostics logger.
/// </summary>
/// <param name="factory">The extension method argument</param>
/// <param name="fileSizeLimitMb">A strictly positive value representing the maximum log size in megabytes. Once the log is full, no more message will be appended</param>
public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory, int fileSizeLimitMb = FileLoggerProvider.DefaultFileSizeLimitMb)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code configuration will be addressed in #470

{
if (WebAppContext.Default.IsRunningInAzureWebApp)
{
// Only add the provider if we're in Azure WebApp. That cannot change once the apps started
factory.AddProvider(new AzureWebAppDiagnosticsLoggerProvider(WebAppContext.Default, fileSizeLimitMb));
}
return factory;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.Extensions.Logging.Abstractions.Internal;
using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal;

namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics
{
/// <summary>
/// Logger provider for Azure WebApp.
/// </summary>
public class AzureWebAppDiagnosticsLoggerProvider : ILoggerProvider
{
private readonly IWebAppLogConfigurationReader _configurationReader;

private readonly ILoggerProvider _innerLoggerProvider;
private readonly bool _runningInWebApp;

/// <summary>
/// Creates a new instance of the <see cref="AzureWebAppDiagnosticsLoggerProvider"/> class.
/// </summary>
public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, int fileSizeLimitMb)
{
_configurationReader = new WebAppLogConfigurationReader(context);

var config = _configurationReader.Current;
_runningInWebApp = config.IsRunningInWebApp;

if (!_runningInWebApp)
{
_innerLoggerProvider = NullLoggerProvider.Instance;
}
else
{
_innerLoggerProvider = new FileLoggerProvider(_configurationReader, fileSizeLimitMb);

if (!string.IsNullOrEmpty(config.BlobContainerUrl))
{
// TODO: Add the blob logger by creating a composite inner logger which calls
// both loggers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to reference another ticket somewhere for the TODO.

}
}
}

/// <inheritdoc />
public ILogger CreateLogger(string categoryName)
{
return _innerLoggerProvider.CreateLogger(categoryName);
}

/// <inheritdoc />
public void Dispose()
{
_innerLoggerProvider.Dispose();
_configurationReader.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Concurrent;
using System.Threading;
using Serilog.Core;
using Serilog.Events;

// TODO: Might want to consider using https://github.com/jezzsantos/Serilog.Sinks.Async
// instead of this, once that supports netstandard
namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal
{
/// <summary>
/// A background sink for Serilog.
/// </summary>
public class BackgroundSink : ILogEventSink, IDisposable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just spiked this up a few days ago as well: https://github.com/jezzsantos/Serilog.Sinks.Async/blob/master/src/Serilog.Sinks.Async/AsyncWorkerSink.cs - given there's a high probability of either Serilog.Sinks.Async, or eventually Serilog itself, shipping a type equivalent to this, it may be better internal at this stage?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this convention that anything in the Internal namespace can be public but it's still internal (so we can change it). We do that to avoid InternalsVisibleTo and still be able to test internal code

{
/// <summary>
/// The default queue size.
/// </summary>
public const int DefaultLogMessagesQueueSize = 1024;

private readonly CancellationTokenSource _disposedTokenSource = new CancellationTokenSource();
private readonly CancellationToken _disposedToken;

private readonly BlockingCollection<LogEvent> _messages;
private readonly Thread _workerThread;

private ILogEventSink _innerSink;

/// <summary>
/// Creates a new instance of the <see cref="BackgroundSink"/> class.
/// </summary>
/// <param name="innerSink">The inner sink which does the actual logging</param>
/// <param name="maxQueueSize">The maximum size of the background queue</param>
public BackgroundSink(ILogEventSink innerSink, int? maxQueueSize)
{
if (innerSink == null)
{
throw new ArgumentNullException(nameof(innerSink));
}

_disposedToken = _disposedTokenSource.Token;

if (maxQueueSize == null || maxQueueSize <= 0)
{
_messages = new BlockingCollection<LogEvent>(new ConcurrentQueue<LogEvent>());
}
else
{
_messages = new BlockingCollection<LogEvent>(new ConcurrentQueue<LogEvent>(), maxQueueSize.Value);
}

_innerSink = innerSink;

_workerThread = new Thread(Worker);
_workerThread.Name = GetType().Name;
_workerThread.IsBackground = true;
_workerThread.Start();
}

/// <inheritdoc />
public void Emit(LogEvent logEvent)
{
if (!_disposedToken.IsCancellationRequested)
{
_messages.Add(logEvent);
}
}

/// <summary>
/// Disposes this object instance.
/// </summary>
public virtual void Dispose()
{
lock (_disposedTokenSource)
{
if (!_disposedTokenSource.IsCancellationRequested)
{
_disposedTokenSource.Cancel();
}

// Wait for the thread to complete before disposing the resources
_workerThread.Join(5 /*seconds */ * 1000);
_messages.Dispose();
}
}

private void Worker()
{
try
{
foreach (var logEvent in _messages.GetConsumingEnumerable(_disposedToken))
{
PassLogEventToInnerSink(logEvent);
}
}
catch (OperationCanceledException)
{
// Do nothing, we just cancelled the task
}
}

private void PassLogEventToInnerSink(LogEvent logEvent)
{
_innerSink.Emit(logEvent);
}
}
}
Loading