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

Commit fedb9b0

Browse files
committed
Add tests
1 parent 248ae2b commit fedb9b0

File tree

6 files changed

+266
-27
lines changed

6 files changed

+266
-27
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using Microsoft.Extensions.Logging.Abstractions;
56
using Microsoft.Extensions.Logging.Abstractions.Internal;
67
using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal;
78
using Serilog;

src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobLoggerProvider.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,14 @@ public Logger ConfigureLogger(IWebAppLogConfigurationReader reader)
7474
var messageFormatter = new MessageTemplateTextFormatter(_outputTemplate, null);
7575
var container = new CloudBlobContainer(new Uri(reader.Current.BlobContainerUrl));
7676
var fileName = _instanceId + "-" + _fileName;
77-
var azureBlobSink = new AzureBlobSink(container, _appName, fileName, messageFormatter, _batchSize, _period);
77+
var azureBlobSink = new AzureBlobSink(
78+
name => new BlobAppendReferenceWrapper(container.GetAppendBlobReference(name)),
79+
_appName,
80+
fileName,
81+
messageFormatter,
82+
_batchSize,
83+
_period);
84+
7885
var backgroundSink = new BackgroundSink(azureBlobSink, _backgroundQueueSize);
7986
var loggerConfiguration = new LoggerConfiguration();
8087

src/Microsoft.Extensions.Logging.AzureWebAppDiagnostics/Internal/AzureBlobSink.cs

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Linq;
88
using System.Threading.Tasks;
99
using Microsoft.WindowsAzure.Storage;
10-
using Microsoft.WindowsAzure.Storage.Blob;
1110
using Serilog.Core;
1211
using Serilog.Events;
1312
using Serilog.Formatting;
@@ -20,23 +19,21 @@ namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal
2019
/// </summary>
2120
public class AzureBlobSink : PeriodicBatchingSink
2221
{
23-
private const string BlobPathSeparator = "/";
24-
2522
private readonly string _appName;
2623
private readonly string _fileName;
2724
private readonly ITextFormatter _formatter;
28-
private readonly CloudBlobContainer _container;
25+
private readonly Func<string, ICloudAppendBlob> _blobReferenceFactory;
2926

3027
/// <summary>
3128
/// Creates a new instance of <see cref="AzureBlobSink"/>
3229
/// </summary>
33-
/// <param name="container">The container to store logs to.</param>
30+
/// <param name="blobReferenceFactory">The container to store logs to.</param>
3431
/// <param name="appName">The application name to use in blob path generation.</param>
3532
/// <param name="fileName">The last segment of blob name.</param>
3633
/// <param name="formatter">The <see cref="ITextFormatter"/> for log messages.</param>
3734
/// <param name="batchSizeLimit">The maximum number of events to include in a single batch.</param>
3835
/// <param name="period">The time to wait between checking for event batches.</param>
39-
public AzureBlobSink(CloudBlobContainer container,
36+
public AzureBlobSink(Func<string, ICloudAppendBlob> blobReferenceFactory,
4037
string appName,
4138
string fileName,
4239
ITextFormatter formatter,
@@ -67,7 +64,7 @@ public AzureBlobSink(CloudBlobContainer container,
6764
_appName = appName;
6865
_fileName = fileName;
6966
_formatter = formatter;
70-
_container = container;
67+
_blobReferenceFactory = blobReferenceFactory;
7168
}
7269

7370
/// <inheritdoc />
@@ -76,37 +73,28 @@ protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
7673
var eventGroups = events.GroupBy(GetBlobKey);
7774
foreach (var eventGroup in eventGroups)
7875
{
79-
var blobName = string.Concat(
80-
_appName, BlobPathSeparator,
81-
eventGroup.Key.Item1, BlobPathSeparator,
82-
eventGroup.Key.Item2, BlobPathSeparator,
83-
eventGroup.Key.Item3, BlobPathSeparator,
84-
eventGroup.Key.Item4, BlobPathSeparator,
85-
_fileName
86-
);
76+
var key = eventGroup.Key;
77+
var blobName = $"{_appName}/{key.Item1}/{key.Item2:00}/{key.Item3:00}/{key.Item4:00}/{_fileName}";
8778

88-
var blob = _container.GetAppendBlobReference(blobName);
79+
var blob = _blobReferenceFactory(blobName);
8980

90-
CloudBlobStream stream;
81+
Stream stream;
9182
try
9283
{
93-
stream = await blob.OpenWriteAsync(createNew: false);
84+
stream = await blob.OpenWriteAsync();
9485
}
9586
// Blob does not exist
9687
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
9788
{
98-
await blob.CreateOrReplaceAsync(AccessCondition.GenerateIfNotExistsCondition(), null, null);
99-
stream = await blob.OpenWriteAsync(createNew: false);
89+
await blob.CreateAsync();
90+
stream = await blob.OpenWriteAsync();
10091
}
10192

102-
using (stream)
93+
using (var writer = new StreamWriter(stream))
10394
{
104-
using (var writer = new StreamWriter(stream))
95+
foreach (var logEvent in eventGroup)
10596
{
106-
foreach (var logEvent in eventGroup)
107-
{
108-
_formatter.Format(logEvent, writer);
109-
}
97+
_formatter.Format(logEvent, writer);
11098
}
11199
}
112100
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.IO;
5+
using System.Threading.Tasks;
6+
using Microsoft.WindowsAzure.Storage;
7+
using Microsoft.WindowsAzure.Storage.Blob;
8+
9+
namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal
10+
{
11+
/// <inheritdoc />
12+
public class BlobAppendReferenceWrapper : ICloudAppendBlob
13+
{
14+
private readonly CloudAppendBlob _cloudAppendBlob;
15+
16+
/// <summary>
17+
/// Creates new instance of <see cref="BlobAppendReferenceWrapper"/>.
18+
/// </summary>
19+
/// <param name="cloudAppendBlob">The <see cref="CloudAppendBlob"/> instance to wrap.</param>
20+
public BlobAppendReferenceWrapper(CloudAppendBlob cloudAppendBlob)
21+
{
22+
_cloudAppendBlob = cloudAppendBlob;
23+
}
24+
/// <inheritdoc />
25+
public async Task<Stream> OpenWriteAsync()
26+
{
27+
return await _cloudAppendBlob.OpenWriteAsync(createNew: false);
28+
}
29+
30+
/// <inheritdoc />
31+
public async Task CreateAsync()
32+
{
33+
await _cloudAppendBlob.CreateOrReplaceAsync(AccessCondition.GenerateIfNotExistsCondition(), options: null, operationContext: null);
34+
}
35+
}
36+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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.IO;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal
8+
{
9+
/// <summary>
10+
/// Represents an append blob, a type of blob where blocks of data are always committed to the end of the blob.
11+
/// </summary>
12+
public interface ICloudAppendBlob
13+
{
14+
/// <summary>
15+
/// Initiates an asynchronous operation to open a stream for writing to the blob.
16+
/// </summary>
17+
/// <returns>A <see cref="T:System.Threading.Tasks.Task`1" /> object of type <see cref="Stream" /> that represents the asynchronous operation.</returns>
18+
Task<Stream> OpenWriteAsync();
19+
20+
/// <summary>
21+
/// Initiates an asynchronous operation to create an empty append blob.
22+
/// </summary>
23+
/// <returns>A <see cref="T:System.Threading.Tasks.Task" /> object that represents the asynchronous operation.</returns>
24+
Task CreateAsync();
25+
}
26+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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.Globalization;
7+
using System.IO;
8+
using System.Linq;
9+
using System.Text;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using Microsoft.Extensions.Logging.AzureWebAppDiagnostics.Internal;
13+
using Microsoft.WindowsAzure.Storage;
14+
using Moq;
15+
using Serilog;
16+
using Serilog.Core;
17+
using Serilog.Events;
18+
using Serilog.Formatting.Display;
19+
using Serilog.Parsing;
20+
using Xunit;
21+
22+
namespace Microsoft.Extensions.Logging.AzureWebApps.Test
23+
{
24+
public class AzureBlobSinkTests
25+
{
26+
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10);
27+
28+
[Fact]
29+
public void WritesMessagesInBatches()
30+
{
31+
var blob = new Mock<ICloudAppendBlob>();
32+
var buffers = new List<byte[]>();
33+
blob.Setup(b => b.OpenWriteAsync()).Returns(() => Task.FromResult((Stream)new TestMemoryStream(buffers)));
34+
35+
var sink = new TestAzureBlobSink(name => blob.Object, 5);
36+
var logger = CreateLogger(sink);
37+
38+
for (int i = 0; i < 5; i++)
39+
{
40+
logger.Information("Text " + i);
41+
}
42+
43+
Assert.True(sink.CountdownEvent.Wait(DefaultTimeout));
44+
45+
#if NET451
46+
Assert.Equal(1, buffers.Count);
47+
Assert.Equal(Encoding.UTF8.GetString(buffers[0]), @"Information Text 0
48+
Information Text 1
49+
Information Text 2
50+
Information Text 3
51+
Information Text 4
52+
");
53+
#else
54+
// PeriodicBatchingSink always writes first message as seperate batch on coreclr
55+
// https://github.com/serilog/serilog-sinks-periodicbatching/issues/7
56+
Assert.Equal(2, buffers.Count);
57+
Assert.Equal(Encoding.UTF8.GetString(buffers[0]), "Information Text 0\r\n");
58+
Assert.Equal(Encoding.UTF8.GetString(buffers[1]), @"Information Text 1
59+
Information Text 2
60+
Information Text 3
61+
Information Text 4
62+
");
63+
#endif
64+
}
65+
66+
[Fact]
67+
public void GroupsByHour()
68+
{
69+
var blob = new Mock<ICloudAppendBlob>();
70+
var buffers = new List<byte[]>();
71+
var names = new List<string>();
72+
73+
blob.Setup(b => b.OpenWriteAsync()).Returns(() => Task.FromResult((Stream)new TestMemoryStream(buffers)));
74+
75+
var sink = new TestAzureBlobSink(name =>
76+
{
77+
names.Add(name);
78+
return blob.Object;
79+
}, 3);
80+
var logger = CreateLogger(sink);
81+
82+
var startDate = new DateTime(2016, 8, 29, 22, 0, 0);
83+
for (int i = 0; i < 3; i++)
84+
{
85+
var addHours = startDate.AddHours(i);
86+
logger.Write(new LogEvent(
87+
new DateTimeOffset(addHours),
88+
LogEventLevel.Information,
89+
null,
90+
new MessageTemplate("Text", Enumerable.Empty<MessageTemplateToken>()),
91+
Enumerable.Empty<LogEventProperty>()));
92+
}
93+
94+
Assert.True(sink.CountdownEvent.Wait(DefaultTimeout));
95+
96+
Assert.Equal(3, buffers.Count);
97+
98+
Assert.Equal("appname/2016/08/29/22/filename", names[0]);
99+
Assert.Equal("appname/2016/08/29/23/filename", names[1]);
100+
Assert.Equal("appname/2016/08/30/00/filename", names[2]);
101+
}
102+
103+
[Fact]
104+
public void CreatesBlobIfNotExists()
105+
{
106+
var blob = new Mock<ICloudAppendBlob>();
107+
var buffers = new List<byte[]>();
108+
bool created = false;
109+
110+
blob.Setup(b => b.OpenWriteAsync()).Returns(() =>
111+
{
112+
if (!created)
113+
{
114+
throw new StorageException(new RequestResult() { HttpStatusCode = 404 }, string.Empty, null);
115+
}
116+
return Task.FromResult((Stream) new TestMemoryStream(buffers));
117+
});
118+
119+
blob.Setup(b => b.CreateAsync()).Returns(() =>
120+
{
121+
created = true;
122+
return Task.FromResult(0);
123+
});
124+
125+
var sink = new TestAzureBlobSink((name) => blob.Object, 1);
126+
var logger = CreateLogger(sink);
127+
logger.Information("Text");
128+
129+
Assert.True(sink.CountdownEvent.Wait(DefaultTimeout));
130+
131+
Assert.Equal(1, buffers.Count);
132+
Assert.Equal(true, created);
133+
}
134+
135+
private static Logger CreateLogger(AzureBlobSink sink)
136+
{
137+
var loggerConfiguration = new LoggerConfiguration();
138+
loggerConfiguration.WriteTo.Sink(sink);
139+
var logger = loggerConfiguration.CreateLogger();
140+
return logger;
141+
}
142+
143+
private class TestAzureBlobSink: AzureBlobSink
144+
{
145+
public CountdownEvent CountdownEvent { get; }
146+
147+
public TestAzureBlobSink(Func<string, ICloudAppendBlob> blob, int count):base(
148+
blob,
149+
"appname",
150+
"filename",
151+
new MessageTemplateTextFormatter("{Level} {Message}{NewLine}", CultureInfo.InvariantCulture),
152+
10,
153+
TimeSpan.FromSeconds(0.1))
154+
{
155+
CountdownEvent = new CountdownEvent(count);
156+
}
157+
158+
protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
159+
{
160+
await base.EmitBatchAsync(events);
161+
CountdownEvent.Signal(events.Count());
162+
}
163+
}
164+
165+
private class TestMemoryStream : MemoryStream
166+
{
167+
public List<byte[]> Buffers { get; }
168+
169+
public TestMemoryStream(List<byte[]> buffers)
170+
{
171+
Buffers = buffers;
172+
}
173+
174+
protected override void Dispose(bool disposing)
175+
{
176+
Buffers.Add(ToArray());
177+
base.Dispose(disposing);
178+
}
179+
}
180+
}
181+
}

0 commit comments

Comments
 (0)