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

Commit b11a8f7

Browse files
committed
Add tests
1 parent 248ae2b commit b11a8f7

File tree

6 files changed

+264
-27
lines changed

6 files changed

+264
-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: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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+
[Fact]
27+
public void WritesMessagesInBatches()
28+
{
29+
var blob = new Mock<ICloudAppendBlob>();
30+
var buffers = new List<byte[]>();
31+
blob.Setup(b => b.OpenWriteAsync()).Returns(() => Task.FromResult((Stream)new TestMemoryStream(buffers)));
32+
33+
var sink = new TestAzureBlobSink(name => blob.Object, 5);
34+
var logger = CreateLogger(sink);
35+
36+
for (int i = 0; i < 5; i++)
37+
{
38+
logger.Information("Text " + i);
39+
}
40+
41+
Assert.True(sink.CountdownEvent.Wait(TimeSpan.FromSeconds(1)));
42+
43+
#if NET451
44+
Assert.Equal(1, buffers.Count);
45+
Assert.Equal(Encoding.UTF8.GetString(buffers[0]), @"Information Text 0
46+
Information Text 1
47+
Information Text 2
48+
Information Text 3
49+
Information Text 4
50+
");
51+
#else
52+
// PeriodicBatchingSink always writes first message as seperate batch on coreclr
53+
// https://github.com/serilog/serilog-sinks-periodicbatching/issues/7
54+
Assert.Equal(2, buffers.Count);
55+
Assert.Equal(Encoding.UTF8.GetString(buffers[0]), "Information Text 0\r\n");
56+
Assert.Equal(Encoding.UTF8.GetString(buffers[1]), @"Information Text 1
57+
Information Text 2
58+
Information Text 3
59+
Information Text 4
60+
");
61+
#endif
62+
}
63+
64+
[Fact]
65+
public void GroupsByHour()
66+
{
67+
var blob = new Mock<ICloudAppendBlob>();
68+
var buffers = new List<byte[]>();
69+
var names = new List<string>();
70+
71+
blob.Setup(b => b.OpenWriteAsync()).Returns(() => Task.FromResult((Stream)new TestMemoryStream(buffers)));
72+
73+
var sink = new TestAzureBlobSink(name =>
74+
{
75+
names.Add(name);
76+
return blob.Object;
77+
}, 3);
78+
var logger = CreateLogger(sink);
79+
80+
var startDate = new DateTime(2016, 8, 29, 22, 0, 0);
81+
for (int i = 0; i < 3; i++)
82+
{
83+
var addHours = startDate.AddHours(i);
84+
logger.Write(new LogEvent(
85+
new DateTimeOffset(addHours),
86+
LogEventLevel.Information,
87+
null,
88+
new MessageTemplate("Text", Enumerable.Empty<MessageTemplateToken>()),
89+
Enumerable.Empty<LogEventProperty>()));
90+
}
91+
92+
Assert.True(sink.CountdownEvent.Wait(TimeSpan.FromSeconds(1)));
93+
94+
Assert.Equal(3, buffers.Count);
95+
96+
Assert.Equal("appname/2016/08/29/22/filename", names[0]);
97+
Assert.Equal("appname/2016/08/29/23/filename", names[1]);
98+
Assert.Equal("appname/2016/08/30/00/filename", names[2]);
99+
}
100+
101+
[Fact]
102+
public void CreatesBlobIfNotExists()
103+
{
104+
var blob = new Mock<ICloudAppendBlob>();
105+
var buffers = new List<byte[]>();
106+
bool created = false;
107+
108+
blob.Setup(b => b.OpenWriteAsync()).Returns(() =>
109+
{
110+
if (!created)
111+
{
112+
throw new StorageException(new RequestResult() { HttpStatusCode = 404 }, string.Empty, null);
113+
}
114+
return Task.FromResult((Stream) new TestMemoryStream(buffers));
115+
});
116+
117+
blob.Setup(b => b.CreateAsync()).Returns(() =>
118+
{
119+
created = true;
120+
return Task.FromResult(0);
121+
});
122+
123+
var sink = new TestAzureBlobSink((name) => blob.Object, 1);
124+
var logger = CreateLogger(sink);
125+
logger.Information("Text");
126+
127+
Assert.True(sink.CountdownEvent.Wait(TimeSpan.FromSeconds(1)));
128+
129+
Assert.Equal(1, buffers.Count);
130+
Assert.Equal(true, created);
131+
}
132+
133+
private static Logger CreateLogger(AzureBlobSink sink)
134+
{
135+
var loggerConfiguration = new LoggerConfiguration();
136+
loggerConfiguration.WriteTo.Sink(sink);
137+
var logger = loggerConfiguration.CreateLogger();
138+
return logger;
139+
}
140+
141+
private class TestAzureBlobSink: AzureBlobSink
142+
{
143+
public CountdownEvent CountdownEvent { get; }
144+
145+
public TestAzureBlobSink(Func<string, ICloudAppendBlob> blob, int count):base(
146+
blob,
147+
"appname",
148+
"filename",
149+
new MessageTemplateTextFormatter("{Level} {Message}{NewLine}", CultureInfo.InvariantCulture),
150+
10,
151+
TimeSpan.FromSeconds(0.1))
152+
{
153+
CountdownEvent = new CountdownEvent(count);
154+
}
155+
156+
protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
157+
{
158+
await base.EmitBatchAsync(events);
159+
CountdownEvent.Signal(events.Count());
160+
}
161+
}
162+
163+
private class TestMemoryStream : MemoryStream
164+
{
165+
public List<byte[]> Buffers { get; }
166+
167+
public TestMemoryStream(List<byte[]> buffers)
168+
{
169+
Buffers = buffers;
170+
}
171+
172+
protected override void Dispose(bool disposing)
173+
{
174+
Buffers.Add(ToArray());
175+
base.Dispose(disposing);
176+
}
177+
}
178+
}
179+
}

0 commit comments

Comments
 (0)