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

Commit 2de4588

Browse files
committed
Add Blob AzureWebApp provider
1 parent f4fa0e5 commit 2de4588

15 files changed

+446
-264
lines changed

src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsFactoryExtensions.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,22 @@ public static class AzureWebAppDiagnosticsFactoryExtensions
1515
/// Adds an Azure Web Apps diagnostics logger.
1616
/// </summary>
1717
/// <param name="factory">The extension method argument</param>
18-
/// <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>
19-
public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory, int fileSizeLimitMb = FileLoggerProvider.DefaultFileSizeLimitMb)
18+
public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory)
19+
{
20+
return AddAzureWebAppDiagnostics(factory, null);
21+
}
22+
23+
/// <summary>
24+
/// Adds an Azure Web Apps diagnostics logger.
25+
/// </summary>
26+
/// <param name="factory">The extension method argument</param>
27+
/// <param name="settings">The setting object to configure loggers.</param>
28+
public static ILoggerFactory AddAzureWebAppDiagnostics(this ILoggerFactory factory, AzureWebAppDiagnosticsSettings settings)
2029
{
2130
if (WebAppContext.Default.IsRunningInAzureWebApp)
2231
{
2332
// Only add the provider if we're in Azure WebApp. That cannot change once the apps started
24-
factory.AddProvider(new AzureWebAppDiagnosticsLoggerProvider(WebAppContext.Default, fileSizeLimitMb));
33+
factory.AddProvider(new AzureWebAppDiagnosticsLoggerProvider(WebAppContext.Default, settings));
2534
}
2635
return factory;
2736
}

src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/AzureWebAppDiagnosticsLoggerProvider.cs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.Extensions.Logging.Abstractions.Internal;
55
using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal;
6+
using Serilog;
67

78
namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics
89
{
@@ -13,45 +14,50 @@ public class AzureWebAppDiagnosticsLoggerProvider : ILoggerProvider
1314
{
1415
private readonly IWebAppLogConfigurationReader _configurationReader;
1516

16-
private readonly ILoggerProvider _innerLoggerProvider;
17-
private readonly bool _runningInWebApp;
17+
private readonly LoggerFactory _loggerFactory;
1818

1919
/// <summary>
2020
/// Creates a new instance of the <see cref="AzureWebAppDiagnosticsLoggerProvider"/> class.
2121
/// </summary>
22-
public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, int fileSizeLimitMb)
22+
public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, AzureWebAppDiagnosticsSettings settings)
2323
{
2424
_configurationReader = new WebAppLogConfigurationReader(context);
2525

2626
var config = _configurationReader.Current;
27-
_runningInWebApp = config.IsRunningInWebApp;
27+
var runningInWebApp = config.IsRunningInWebApp;
2828

29-
if (!_runningInWebApp)
29+
if (runningInWebApp)
3030
{
31-
_innerLoggerProvider = NullLoggerProvider.Instance;
32-
}
33-
else
34-
{
35-
_innerLoggerProvider = new FileLoggerProvider(_configurationReader, fileSizeLimitMb);
31+
_loggerFactory = new LoggerFactory();
32+
var fileLoggerProvider = new FileLoggerProvider(
33+
settings.FileSizeLimit,
34+
settings.RetainedFileCountLimit,
35+
settings.OutputTemplate);
3636

37+
_loggerFactory.AddSerilog(fileLoggerProvider.ConfigureLogger(_configurationReader));
3738
if (!string.IsNullOrEmpty(config.BlobContainerUrl))
3839
{
39-
// TODO: Add the blob logger by creating a composite inner logger which calls
40-
// both loggers
40+
var blobLoggerProvider = new AzureBlobLoggerProvider(
41+
settings.OutputTemplate,
42+
context.SiteName,
43+
settings.BlobName,
44+
settings.BlobBatchSize,
45+
settings.BlobCommitPeriod);
46+
_loggerFactory.AddSerilog(blobLoggerProvider.ConfigureLogger(_configurationReader));
4147
}
4248
}
4349
}
4450

4551
/// <inheritdoc />
4652
public ILogger CreateLogger(string categoryName)
4753
{
48-
return _innerLoggerProvider.CreateLogger(categoryName);
54+
return _loggerFactory?.CreateLogger(categoryName) ?? NullLogger.Instance;
4955
}
5056

5157
/// <inheritdoc />
5258
public void Dispose()
5359
{
54-
_innerLoggerProvider.Dispose();
60+
_loggerFactory.Dispose();
5561
_configurationReader.Dispose();
5662
}
5763
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics
7+
{
8+
/// <summary>
9+
/// Settings for <see cref="AzureWebAppDiagnosticsLoggerProvider"/>.
10+
/// </summary>
11+
public class AzureWebAppDiagnosticsSettings
12+
{
13+
/// <summary>
14+
/// A strictly positive value representing the maximum log size in bytes. Once the log is full, no more message will be appended
15+
/// </summary>
16+
public int FileSizeLimit { get; set; } = 10 * 1024 * 1024;
17+
18+
/// <summary>
19+
/// A strictly positive value representing the maximum retained file count
20+
/// </summary>
21+
public int RetainedFileCountLimit { get; set; } = 2;
22+
23+
/// <summary>
24+
/// A message template describing the output messages
25+
/// </summary>
26+
public string OutputTemplate { get; set; } = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}";
27+
28+
/// <summary>
29+
/// The maximum number of events to include in a single blob append batch.
30+
/// </summary>
31+
public int BlobBatchSize { get; set; } = 32;
32+
33+
/// <summary>
34+
/// The time to wait between checking for blob log batches
35+
/// </summary>
36+
public TimeSpan BlobCommitPeriod { get; set; } = TimeSpan.FromSeconds(5);
37+
38+
/// <summary>
39+
/// The last section of log blob name.
40+
/// </summary>
41+
public string BlobName { get; set; } = "applicationLog.txt";
42+
}
43+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Serilog;
6+
using Serilog.Core;
7+
using Serilog.Formatting.Display;
8+
using Microsoft.WindowsAzure.Storage.Blob;
9+
10+
namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal
11+
{
12+
/// <summary>
13+
/// The <see cref="SerilogLoggerProvider"/> implemenation that creates instances of <see cref="Serilog.Core.Logger"/> connected to <see cref="AzureBlobSink"/>.
14+
/// </summary>
15+
public class AzureBlobLoggerProvider : SerilogLoggerProvider
16+
{
17+
private readonly string _outputTemplate;
18+
private readonly string _appName;
19+
private readonly string _fileName;
20+
private readonly int _batchSize;
21+
private readonly TimeSpan _period;
22+
23+
/// <summary>
24+
/// Creates a new instance of the <see cref="AzureBlobLoggerProvider"/> class.
25+
/// </summary>
26+
/// <param name="outputTemplate"></param>
27+
/// <param name="appName"></param>
28+
/// <param name="fileName"></param>
29+
/// <param name="batchSize"></param>
30+
/// <param name="period"></param>
31+
public AzureBlobLoggerProvider(string outputTemplate, string appName, string fileName, int batchSize, TimeSpan period)
32+
{
33+
_outputTemplate = outputTemplate;
34+
_appName = appName;
35+
_fileName = fileName;
36+
_batchSize = batchSize;
37+
_period = period;
38+
}
39+
40+
/// <inheritdoc />
41+
public override Logger ConfigureLogger(IWebAppLogConfigurationReader reader)
42+
{
43+
var messageFormatter = new MessageTemplateTextFormatter(_outputTemplate, null);
44+
var container = new CloudBlobContainer(new Uri(reader.Current.BlobContainerUrl));
45+
var azureBlobSink = new AzureBlobSink(container, _appName, _fileName, messageFormatter, _batchSize, _period);
46+
var backgroundSink = new BackgroundSink(azureBlobSink, BackgroundSink.DefaultLogMessagesQueueSize);
47+
LoggerConfiguration loggerConfiguration = new LoggerConfiguration();
48+
49+
loggerConfiguration.WriteTo.Sink(backgroundSink);
50+
loggerConfiguration.MinimumLevel.ControlledBy(new WebConfigurationReaderLevelSwitch(reader,
51+
configuration =>
52+
{
53+
return configuration.BlobLoggingEnabled ? configuration.BlobLoggingLevel: LogLevel.None;
54+
}));
55+
56+
return loggerConfiguration.CreateLogger();
57+
}
58+
59+
}
60+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Microsoft.WindowsAzure.Storage;
10+
using Microsoft.WindowsAzure.Storage.Blob;
11+
using Serilog.Core;
12+
using Serilog.Events;
13+
using Serilog.Formatting;
14+
using Serilog.Sinks.PeriodicBatching;
15+
16+
namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal
17+
{
18+
/// <summary>
19+
/// The <see cref="ILogEventSink"/> implemenation that stores messages by appending them to Azure Blob in batches.
20+
/// </summary>
21+
public class AzureBlobSink : PeriodicBatchingSink
22+
{
23+
private const string BlobPathSeparator = "/";
24+
25+
private readonly string _appName;
26+
private readonly string _fileName;
27+
private readonly ITextFormatter _formatter;
28+
private readonly CloudBlobContainer _container;
29+
30+
/// <summary>
31+
/// Creates a new instance of <see cref="AzureBlobSink"/>
32+
/// </summary>
33+
/// <param name="container">The container to store logs to.</param>
34+
/// <param name="appName">The application name to use in blob path generation.</param>
35+
/// <param name="fileName">The last segment of blob name.</param>
36+
/// <param name="formatter">The <see cref="ITextFormatter"/> for log messages.</param>
37+
/// <param name="batchSizeLimit">The maximum number of events to include in a single batch.</param>
38+
/// <param name="period">The time to wait between checking for event batches.</param>
39+
public AzureBlobSink(CloudBlobContainer container,
40+
string appName,
41+
string fileName,
42+
ITextFormatter formatter,
43+
int batchSizeLimit,
44+
TimeSpan period) : base(batchSizeLimit, period)
45+
{
46+
_appName = appName;
47+
_fileName = fileName;
48+
_formatter = formatter;
49+
if (batchSizeLimit < 1)
50+
{
51+
throw new ArgumentException(nameof(batchSizeLimit));
52+
}
53+
_container = container;
54+
}
55+
56+
/// <inheritdoc />
57+
protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
58+
{
59+
var eventGroups = events.GroupBy(GetBlobKey);
60+
foreach (var eventGroup in eventGroups)
61+
{
62+
var blobName = string.Concat(
63+
_appName, BlobPathSeparator,
64+
eventGroup.Key.Item1, BlobPathSeparator,
65+
eventGroup.Key.Item2, BlobPathSeparator,
66+
eventGroup.Key.Item3, BlobPathSeparator,
67+
eventGroup.Key.Item4, BlobPathSeparator,
68+
_fileName
69+
);
70+
71+
var blob = _container.GetAppendBlobReference(blobName);
72+
73+
CloudBlobStream stream;
74+
try
75+
{
76+
stream = await blob.OpenWriteAsync(createNew: false);
77+
}
78+
// Blob does not exist
79+
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
80+
{
81+
await blob.CreateOrReplaceAsync(AccessCondition.GenerateIfNotExistsCondition(), null, null);
82+
stream = await blob.OpenWriteAsync(createNew: false);
83+
}
84+
85+
using (stream)
86+
{
87+
using (var writer = new StreamWriter(stream))
88+
{
89+
foreach (var logEvent in eventGroup)
90+
{
91+
_formatter.Format(logEvent, writer);
92+
}
93+
}
94+
}
95+
}
96+
}
97+
98+
private Tuple<int,int,int,int> GetBlobKey(LogEvent e)
99+
{
100+
return Tuple.Create(e.Timestamp.Year,
101+
e.Timestamp.Month,
102+
e.Timestamp.Day,
103+
e.Timestamp.Hour);
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)