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

Commit 88ee78a

Browse files
authored
Add Blob AzureWebApp provider (#477)
* Add Blob AzureWebApp provider
1 parent e9e62d2 commit 88ee78a

18 files changed

+766
-281
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, new AzureWebAppDiagnosticsSettings());
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: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using Microsoft.Extensions.Logging.Abstractions;
6+
using Microsoft.Extensions.Logging.Abstractions.Internal;
57
using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal;
8+
using Serilog;
69

710
namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics
811
{
@@ -13,45 +16,58 @@ public class AzureWebAppDiagnosticsLoggerProvider : ILoggerProvider
1316
{
1417
private readonly IWebAppLogConfigurationReader _configurationReader;
1518

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

1921
/// <summary>
2022
/// Creates a new instance of the <see cref="AzureWebAppDiagnosticsLoggerProvider"/> class.
2123
/// </summary>
22-
public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, int fileSizeLimitMb)
24+
public AzureWebAppDiagnosticsLoggerProvider(WebAppContext context, AzureWebAppDiagnosticsSettings settings)
2325
{
26+
if (settings == null)
27+
{
28+
throw new ArgumentNullException(nameof(settings));
29+
}
30+
2431
_configurationReader = new WebAppLogConfigurationReader(context);
2532

2633
var config = _configurationReader.Current;
27-
_runningInWebApp = config.IsRunningInWebApp;
34+
var runningInWebApp = config.IsRunningInWebApp;
2835

29-
if (!_runningInWebApp)
30-
{
31-
_innerLoggerProvider = NullLoggerProvider.Instance;
32-
}
33-
else
36+
if (runningInWebApp)
3437
{
35-
_innerLoggerProvider = new FileLoggerProvider(_configurationReader, fileSizeLimitMb);
38+
_loggerFactory = new LoggerFactory();
39+
var fileLoggerProvider = new FileLoggerProvider(
40+
settings.FileSizeLimit,
41+
settings.RetainedFileCountLimit,
42+
settings.BackgroundQueueSize,
43+
settings.OutputTemplate);
3644

45+
_loggerFactory.AddSerilog(fileLoggerProvider.ConfigureLogger(_configurationReader));
3746
if (!string.IsNullOrEmpty(config.BlobContainerUrl))
3847
{
39-
// TODO: Add the blob logger by creating a composite inner logger which calls
40-
// both loggers
48+
var blobLoggerProvider = new AzureBlobLoggerProvider(
49+
settings.OutputTemplate,
50+
context.SiteName,
51+
context.SiteInstanceId,
52+
settings.BlobName,
53+
settings.BlobBatchSize,
54+
settings.BackgroundQueueSize,
55+
settings.BlobCommitPeriod);
56+
_loggerFactory.AddSerilog(blobLoggerProvider.ConfigureLogger(_configurationReader));
4157
}
4258
}
4359
}
4460

4561
/// <inheritdoc />
4662
public ILogger CreateLogger(string categoryName)
4763
{
48-
return _innerLoggerProvider.CreateLogger(categoryName);
64+
return _loggerFactory?.CreateLogger(categoryName) ?? NullLogger.Instance;
4965
}
5066

5167
/// <inheritdoc />
5268
public void Dispose()
5369
{
54-
_innerLoggerProvider.Dispose();
70+
_loggerFactory?.Dispose();
5571
_configurationReader.Dispose();
5672
}
5773
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
/// Gets or sets 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+
/// Gets or sets a strictly positive value representing the maximum retained file count.
20+
/// </summary>
21+
public int RetainedFileCountLimit { get; set; } = 2;
22+
23+
/// <summary>
24+
/// Gets or sets 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+
/// Gets or sets a 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+
/// Gets or sets a time to wait between checking for blob log batches.
35+
/// </summary>
36+
public TimeSpan BlobCommitPeriod { get; set; } = TimeSpan.FromSeconds(5);
37+
38+
/// <summary>
39+
/// Gets or sets the last section of log blob name.
40+
/// </summary>
41+
public string BlobName { get; set; } = "applicationLog.txt";
42+
43+
/// Gets of sets the maximum size of the background log message queue.
44+
public int BackgroundQueueSize { get; set; }
45+
}
46+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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 implemenation of logger provider that creates instances of <see cref="Serilog.Core.Logger"/>.
14+
/// </summary>
15+
public class AzureBlobLoggerProvider
16+
{
17+
private readonly string _outputTemplate;
18+
private readonly string _appName;
19+
private readonly string _instanceId;
20+
private readonly string _fileName;
21+
private readonly int _batchSize;
22+
private readonly int _backgroundQueueSize;
23+
private readonly TimeSpan _period;
24+
25+
/// <summary>
26+
/// Creates a new instance of the <see cref="AzureBlobLoggerProvider"/> class.
27+
/// </summary>
28+
/// <param name="outputTemplate">A message template describing the output messages</param>
29+
/// <param name="appName">The application name to use in blob name</param>
30+
/// <param name="instanceId">The application instance id to use in blob name</param>
31+
/// <param name="fileName">The last section in log blob name</param>
32+
/// <param name="batchSize">A maximum number of events to include in a single blob append batch</param>
33+
/// <param name="backgroundQueueSize">The maximum size of the background queue</param>
34+
/// <param name="period">A time to wait between checking for blob log batches</param>
35+
public AzureBlobLoggerProvider(string outputTemplate, string appName, string instanceId, string fileName, int batchSize, int backgroundQueueSize, TimeSpan period)
36+
{
37+
if (outputTemplate == null)
38+
{
39+
throw new ArgumentNullException(nameof(outputTemplate));
40+
}
41+
if (appName == null)
42+
{
43+
throw new ArgumentNullException(nameof(appName));
44+
}
45+
if (instanceId == null)
46+
{
47+
throw new ArgumentNullException(nameof(instanceId));
48+
}
49+
if (fileName == null)
50+
{
51+
throw new ArgumentNullException(nameof(fileName));
52+
}
53+
if (batchSize <= 0)
54+
{
55+
throw new ArgumentOutOfRangeException(nameof(batchSize), $"{nameof(batchSize)} should be a positive number.");
56+
}
57+
if (period <= TimeSpan.Zero)
58+
{
59+
throw new ArgumentOutOfRangeException(nameof(period), $"{nameof(period)} should be longer than zero.");
60+
}
61+
62+
_outputTemplate = outputTemplate;
63+
_appName = appName;
64+
_instanceId = instanceId;
65+
_fileName = fileName;
66+
_batchSize = batchSize;
67+
_backgroundQueueSize = backgroundQueueSize;
68+
_period = period;
69+
}
70+
71+
/// <inheritdoc />
72+
public Logger ConfigureLogger(IWebAppLogConfigurationReader reader)
73+
{
74+
var messageFormatter = new MessageTemplateTextFormatter(_outputTemplate, null);
75+
var container = new CloudBlobContainer(new Uri(reader.Current.BlobContainerUrl));
76+
var fileName = _instanceId + "-" + _fileName;
77+
var azureBlobSink = new AzureBlobSink(
78+
name => new BlobAppendReferenceWrapper(container.GetAppendBlobReference(name)),
79+
_appName,
80+
fileName,
81+
messageFormatter,
82+
_batchSize,
83+
_period);
84+
85+
var backgroundSink = new BackgroundSink(azureBlobSink, _backgroundQueueSize);
86+
var loggerConfiguration = new LoggerConfiguration();
87+
88+
loggerConfiguration.WriteTo.Sink(backgroundSink);
89+
loggerConfiguration.MinimumLevel.ControlledBy(new WebConfigurationReaderLevelSwitch(reader,
90+
configuration =>
91+
{
92+
return configuration.BlobLoggingEnabled ? configuration.BlobLoggingLevel : LogLevel.None;
93+
}));
94+
95+
return loggerConfiguration.CreateLogger();
96+
}
97+
98+
}
99+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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 Serilog.Core;
11+
using Serilog.Events;
12+
using Serilog.Formatting;
13+
using Serilog.Sinks.PeriodicBatching;
14+
15+
namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal
16+
{
17+
/// <summary>
18+
/// The <see cref="ILogEventSink"/> implemenation that stores messages by appending them to Azure Blob in batches.
19+
/// </summary>
20+
public class AzureBlobSink : PeriodicBatchingSink
21+
{
22+
private readonly string _appName;
23+
private readonly string _fileName;
24+
private readonly ITextFormatter _formatter;
25+
private readonly Func<string, ICloudAppendBlob> _blobReferenceFactory;
26+
27+
/// <summary>
28+
/// Creates a new instance of <see cref="AzureBlobSink"/>
29+
/// </summary>
30+
/// <param name="blobReferenceFactory">The container to store logs to.</param>
31+
/// <param name="appName">The application name to use in blob path generation.</param>
32+
/// <param name="fileName">The last segment of blob name.</param>
33+
/// <param name="formatter">The <see cref="ITextFormatter"/> for log messages.</param>
34+
/// <param name="batchSizeLimit">The maximum number of events to include in a single batch.</param>
35+
/// <param name="period">The time to wait between checking for event batches.</param>
36+
public AzureBlobSink(Func<string, ICloudAppendBlob> blobReferenceFactory,
37+
string appName,
38+
string fileName,
39+
ITextFormatter formatter,
40+
int batchSizeLimit,
41+
TimeSpan period) : base(batchSizeLimit, period)
42+
{
43+
if (appName == null)
44+
{
45+
throw new ArgumentNullException(nameof(appName));
46+
}
47+
if (fileName == null)
48+
{
49+
throw new ArgumentNullException(nameof(fileName));
50+
}
51+
if (formatter == null)
52+
{
53+
throw new ArgumentNullException(nameof(formatter));
54+
}
55+
if (batchSizeLimit <= 0)
56+
{
57+
throw new ArgumentOutOfRangeException(nameof(batchSizeLimit), $"{nameof(batchSizeLimit)} should be a positive number.");
58+
}
59+
if (period <= TimeSpan.Zero)
60+
{
61+
throw new ArgumentOutOfRangeException(nameof(period), $"{nameof(period)} should be longer than zero.");
62+
}
63+
64+
_appName = appName;
65+
_fileName = fileName;
66+
_formatter = formatter;
67+
_blobReferenceFactory = blobReferenceFactory;
68+
}
69+
70+
/// <inheritdoc />
71+
protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
72+
{
73+
var eventGroups = events.GroupBy(GetBlobKey);
74+
foreach (var eventGroup in eventGroups)
75+
{
76+
var key = eventGroup.Key;
77+
var blobName = $"{_appName}/{key.Item1}/{key.Item2:00}/{key.Item3:00}/{key.Item4:00}/{_fileName}";
78+
79+
var blob = _blobReferenceFactory(blobName);
80+
81+
Stream stream;
82+
try
83+
{
84+
stream = await blob.OpenWriteAsync();
85+
}
86+
// Blob does not exist
87+
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
88+
{
89+
await blob.CreateAsync();
90+
stream = await blob.OpenWriteAsync();
91+
}
92+
93+
using (var writer = new StreamWriter(stream))
94+
{
95+
foreach (var logEvent in eventGroup)
96+
{
97+
_formatter.Format(logEvent, writer);
98+
}
99+
}
100+
}
101+
}
102+
103+
private Tuple<int,int,int,int> GetBlobKey(LogEvent e)
104+
{
105+
return Tuple.Create(e.Timestamp.Year,
106+
e.Timestamp.Month,
107+
e.Timestamp.Day,
108+
e.Timestamp.Hour);
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)