Skip to content

Commit d529cbb

Browse files
committed
add IDisposable to EventCollectorSink and dispose request/response in EmitBatchAsync
1 parent 217fd53 commit d529cbb

File tree

2 files changed

+147
-5
lines changed

2 files changed

+147
-5
lines changed

src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorSink.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ namespace Serilog.Sinks.Splunk
2929
/// <summary>
3030
/// A sink to log to the Event Collector available in Splunk 6.3
3131
/// </summary>
32-
public class EventCollectorSink : IBatchedLogEventSink
32+
public class EventCollectorSink : IBatchedLogEventSink, IDisposable
3333
{
3434
internal const int DefaultQueueLimit = 100000;
3535

3636
private readonly string _splunkHost;
3737
private readonly string _uriPath;
3838
private readonly ITextFormatter _jsonFormatter;
3939
private readonly EventCollectorClient _httpClient;
40+
private bool _disposed;
4041

4142

4243
/// <summary>
@@ -183,15 +184,15 @@ public EventCollectorSink(
183184
/// <inheritdoc />
184185
public virtual async Task EmitBatchAsync(IReadOnlyCollection<LogEvent> batch)
185186
{
186-
var allEvents = new StringWriter();
187-
187+
using var allEvents = new StringWriter();
188+
188189
foreach (var logEvent in batch)
189190
{
190191
_jsonFormatter.Format(logEvent, allEvents);
191192
}
192193

193-
var request = new EventCollectorRequest(_splunkHost, allEvents.ToString(), _uriPath);
194-
var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
194+
using var request = new EventCollectorRequest(_splunkHost, allEvents.ToString(), _uriPath);
195+
using var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
195196

196197
if (!response.IsSuccessStatusCode)
197198
{
@@ -210,5 +211,28 @@ public virtual async Task EmitBatchAsync(IReadOnlyCollection<LogEvent> batch)
210211
}
211212
}
212213
}
214+
215+
/// <summary>
216+
/// Releases resources used by the sink.
217+
/// </summary>
218+
/// <param name="disposing">True if called from Dispose, false if called from a finalizer.</param>
219+
protected virtual void Dispose(bool disposing)
220+
{
221+
if (!_disposed)
222+
{
223+
if (disposing)
224+
{
225+
_httpClient?.Dispose();
226+
}
227+
_disposed = true;
228+
}
229+
}
230+
231+
/// <inheritdoc/>
232+
public void Dispose()
233+
{
234+
Dispose(true);
235+
GC.SuppressFinalize(this);
236+
}
213237
}
214238
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using Serilog.Events;
2+
using Serilog.Parsing;
3+
using Serilog.Sinks.Splunk;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Net;
7+
using System.Net.Http;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Xunit;
11+
12+
namespace Serilog.Sinks.Splunk.Tests
13+
{
14+
public class EventCollectorSinkTests
15+
{
16+
/// <summary>
17+
/// A fake HttpMessageHandler that records disposal and captures outgoing requests.
18+
/// </summary>
19+
private class FakeHandler : HttpMessageHandler
20+
{
21+
public bool Disposed { get; private set; }
22+
public HttpRequestMessage LastRequest { get; private set; }
23+
public HttpStatusCode ResponseStatusCode { get; set; } = HttpStatusCode.OK;
24+
25+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
26+
{
27+
LastRequest = request;
28+
return Task.FromResult(new HttpResponseMessage(ResponseStatusCode));
29+
}
30+
31+
protected override void Dispose(bool disposing)
32+
{
33+
Disposed = true;
34+
base.Dispose(disposing);
35+
}
36+
}
37+
38+
[Fact]
39+
public void DisposingSinkDisposesOwnedHttpResources()
40+
{
41+
var handler = new FakeHandler();
42+
var sink = new EventCollectorSink(
43+
"http://splunk.example.com:8088",
44+
"test-token",
45+
null,
46+
new SplunkJsonFormatter(false, true, null),
47+
handler);
48+
49+
sink.Dispose();
50+
51+
Assert.True(handler.Disposed, "HttpMessageHandler should be disposed when sink is disposed");
52+
}
53+
54+
[Fact]
55+
public void DisposingSinkTwiceDoesNotThrow()
56+
{
57+
var handler = new FakeHandler();
58+
var sink = new EventCollectorSink(
59+
"http://splunk.example.com:8088",
60+
"test-token",
61+
null,
62+
new SplunkJsonFormatter(false, true, null),
63+
handler);
64+
65+
sink.Dispose();
66+
sink.Dispose(); // Should not throw
67+
}
68+
69+
[Fact]
70+
public async Task EmitBatchSendsRequestWithCorrectAuthHeaders()
71+
{
72+
var handler = new FakeHandler();
73+
var sink = new EventCollectorSink(
74+
"http://splunk.example.com:8088",
75+
"my-secret-token",
76+
"services/collector/event",
77+
new SplunkJsonFormatter(false, true, null),
78+
handler);
79+
80+
var logEvent = new LogEvent(
81+
DateTimeOffset.UtcNow,
82+
LogEventLevel.Information,
83+
null,
84+
new MessageTemplate("Test", new List<MessageTemplateToken>()),
85+
new LogEventProperty[] { });
86+
87+
await sink.EmitBatchAsync(new[] { logEvent });
88+
89+
Assert.NotNull(handler.LastRequest);
90+
Assert.Equal("Splunk", handler.LastRequest.Headers.Authorization?.Scheme);
91+
Assert.Equal("my-secret-token", handler.LastRequest.Headers.Authorization?.Parameter);
92+
Assert.True(handler.LastRequest.Headers.Contains("X-Splunk-Request-Channel"));
93+
94+
sink.Dispose();
95+
}
96+
97+
[Fact]
98+
public void DisposingLoggerDisposesHttpResources()
99+
{
100+
var handler = new FakeHandler();
101+
102+
var logger = new LoggerConfiguration()
103+
.WriteTo.EventCollector(
104+
"http://splunk.example.com:8088",
105+
"test-token",
106+
new SplunkJsonFormatter(false, true, null),
107+
messageHandler: handler)
108+
.CreateLogger();
109+
110+
logger.Information("Test event");
111+
112+
// Logger disposal should cascade to sink disposal
113+
logger.Dispose();
114+
115+
Assert.True(handler.Disposed, "HttpMessageHandler should be disposed when logger is disposed");
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)