Skip to content

Commit

Permalink
Merge pull request #18 from evilpilaf/bugs
Browse files Browse the repository at this point in the history
Bugs
  • Loading branch information
evilpilaf authored Jan 10, 2020
2 parents 4497729 + 3d8482b commit 46494a7
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 76 deletions.
59 changes: 59 additions & 0 deletions src/Honeycomb.Serilog.Sink/Formatters/RawJsonFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Json;

namespace Honeycomb.Serilog.Sink.Formatters
{
internal class RawJsonFormatter : ITextFormatter
{
private static readonly JsonValueFormatter ValueFormatter = new JsonValueFormatter();

public void Format(LogEvent logEvent, TextWriter output)
{
FormatContent(logEvent, output);
}

public static void FormatContent(LogEvent logEvent, TextWriter output)
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
if (output == null) throw new ArgumentNullException(nameof(output));

output.Write($"{{\"time\":\"{logEvent.Timestamp:O}\",");
output.Write($"\"data\":{{");
output.Write($"\"level\":\"{logEvent.Level}\"");
output.Write(",\"messageTemplate\":");
JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output);
if (logEvent.Exception != null)
{
output.Write(",\"exception\":");
JsonValueFormatter.WriteQuotedJsonString(logEvent.Exception.ToString(), output);
}

if (logEvent.Properties.Any())
{
WriteProperties(logEvent.Properties, output);
}

output.Write('}');
output.Write('}');
}

private static void WriteProperties(IReadOnlyDictionary<string, LogEventPropertyValue> properties, TextWriter output)
{
var precedingDelimiter = ",";
foreach (var property in properties)
{
output.Write(precedingDelimiter);

JsonValueFormatter.WriteQuotedJsonString(property.Key, output);
output.Write(':');
ValueFormatter.Format(property.Value, output);
}
}
}
}
12 changes: 11 additions & 1 deletion src/Honeycomb.Serilog.Sink/Honeycomb.Serilog.Sink.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Title>Honeycomb Serilog sink</Title>
<Authors>evilpilaf</Authors>
Expand All @@ -11,12 +11,22 @@
<Copyright>evilpilaf © $([System.DateTime]::Now.Year)</Copyright>
</PropertyGroup>

<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
<DefineConstants>NETCORE;NETSTANDARD;NETSTANDARD2_0</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition=" '$(TargetFramework)' == 'net461'">
<DefineConstants>NET461;NETFULL</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MinVer" Version="2.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Sinks.PeriodicBatching" Version="2.2.0" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net461' OR '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net47' OR '$(TargetFramework)' == 'net471' OR '$(TargetFramework)' == 'net472' OR '$(TargetFramework)' == 'net48' ">
Expand Down
69 changes: 36 additions & 33 deletions src/Honeycomb.Serilog.Sink/HoneycombSerilogSink.cs
Original file line number Diff line number Diff line change
@@ -1,69 +1,73 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

using Honeycomb.Serilog.Sink.Formatters;

using Serilog.Core;
using Serilog.Events;
using Serilog.Sinks.PeriodicBatching;

namespace Honeycomb.Serilog.Sink
{
internal class HoneycombSerilogSink : ILogEventSink
internal class HoneycombSerilogSink : PeriodicBatchingSink
{
private static readonly Uri _honeycombApiUrl = new Uri("https://api.honeycomb.io/1/events/");
private static readonly Uri _honeycombApiUrl = new Uri("https://api.honeycomb.io/");

private readonly string _teamId;
private readonly string _apiKey;

private readonly Lazy<HttpClient> _clientBuilder = new Lazy<HttpClient>(BuildHttpClient);
protected virtual HttpClient Client => _clientBuilder.Value;

public HoneycombSerilogSink(string teamId, string apiKey)
/// <param name="teamId">The name of the team to submit the events to</param>
/// <param name="apiKey">The API key given in the Honeycomb ui</param>
/// <param name="batchSizeLimit">The maximum number of events to include in a single batch.</param>
/// <param name="period">The time to wait between checking for event batches.</param>
public HoneycombSerilogSink(
string teamId,
string apiKey,
int batchSizeLimit,
TimeSpan period)
: base(batchSizeLimit, period)
{
_teamId = string.IsNullOrWhiteSpace(teamId) ? throw new ArgumentNullException(nameof(teamId)) : teamId;
_apiKey = string.IsNullOrWhiteSpace(apiKey) ? throw new ArgumentNullException(nameof(apiKey)) : apiKey;
}

public void Emit(LogEvent logEvent)
protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
{
using (var buffer = new StringWriter(new StringBuilder()))
using (TextWriter writer = new StringWriter())
{
var evnt = BuildLogEvent(logEvent);
var message = new HttpRequestMessage(HttpMethod.Post, $"/{_teamId}")
{
Content = new StringContent(evnt)
};
message.Headers.Add("X-Honeycomb-Team", _apiKey);
Client.SendAsync(message).ConfigureAwait(false).GetAwaiter().GetResult();
BuildLogEvent(events, writer);
await SendBatchedEvents(writer.ToString());
}
}

private static string BuildLogEvent(LogEvent logEvent)
private async Task SendBatchedEvents(string events)
{
var evnt = new StringBuilder("{");

var propertyList = new List<string>(logEvent.Properties.Count() + 4)
var message = new HttpRequestMessage(HttpMethod.Post, $"/1/batch/{_teamId}")
{
$"\"timestamp\": \"{logEvent.Timestamp:O}\"",
$"\"level\": \"{logEvent.Level}\"",
$"\"messageTemplate\": \"{logEvent.MessageTemplate}\""
Content = new StringContent(events, Encoding.UTF8, "application/json")
};
message.Headers.Add("X-Honeycomb-Team", _apiKey);
var result = await Client.SendAsync(message).ConfigureAwait(false);
var response = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
}

if (logEvent.Exception != null)
{
propertyList.Add($"\"exception\": \"{logEvent.Exception.ToString()}\"");
}

foreach (var prop in logEvent.Properties)
private static void BuildLogEvent(IEnumerable<LogEvent> logEvents, TextWriter payload)
{
payload.Write("[");
var eventSepparator = "";
foreach (var evnt in logEvents)
{
propertyList.Add($"\"{prop.Key}\": {prop.Value.ToString()}");
payload.Write(eventSepparator);
eventSepparator = ",";
RawJsonFormatter.FormatContent(evnt, payload);
}

evnt.Append(string.Join(",", propertyList));
evnt.Append("}");
return evnt.ToString();
payload.Write("]");
}

private static HttpClient BuildHttpClient()
Expand All @@ -72,7 +76,6 @@ private static HttpClient BuildHttpClient()
{
BaseAddress = _honeycombApiUrl
};
client.DefaultRequestHeaders.Add("Content-Type", "application/json");
return client;
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/Honeycomb.Serilog.Sink/HoneycombSinkExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
using System;

using Serilog;
using Serilog.Configuration;

namespace Honeycomb.Serilog.Sink
{
public static class HoneycombSinkExtensions
{
/// <param name="teamId">The name of the team to submit the events to</param>
/// <param name="apiKey">The API key given in the Honeycomb ui</param>
/// <param name="batchSizeLimit">The maximum number of events to include in a single batch.</param>
/// <param name="period">The time to wait between checking for event batches.</param>
public static LoggerConfiguration HoneycombSink(this LoggerSinkConfiguration loggerConfiguration,
string teamId,
string apiKey)
string apiKey,
int batchSizeLimit,
TimeSpan period)
{
return loggerConfiguration.Sink(new HoneycombSerilogSink(teamId, apiKey));
return loggerConfiguration.Sink(new HoneycombSerilogSink(teamId, apiKey, batchSizeLimit, period));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
{
RequestMessage = request;
RequestContent = await request.Content.ReadAsStringAsync();
return new HttpResponseMessage(_statusCodeToReturn);
return new HttpResponseMessage(_statusCodeToReturn)
{
Content = new StringContent("")
};
}

public void ReturnsStatusCode(HttpStatusCode statusCode)
Expand Down
70 changes: 70 additions & 0 deletions test/Honeycomb.Serilog.Sink.Tests/Helpers/Some.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;

using Serilog;
using Serilog.Events;

using Xunit.Sdk;

namespace Honeycomb.Serilog.Sink.Tests.Helpers
{
static class Some
{
public static LogEvent LogEvent(string messageTemplate, params object[] propertyValues)
{
return LogEvent(null, messageTemplate, propertyValues);
}

public static LogEvent LogEvent(Exception exception, string messageTemplate, params object[] propertyValues)
{
return LogEvent(LogEventLevel.Information, exception, messageTemplate, propertyValues);
}

public static LogEvent LogEvent(LogEventLevel level, Exception exception, string messageTemplate, params object[] propertyValues)
{
var log = new LoggerConfiguration().CreateLogger();
MessageTemplate template;
IEnumerable<LogEventProperty> properties;
#pragma warning disable Serilog004 // Constant MessageTemplate verifier
if (!log.BindMessageTemplate(messageTemplate, propertyValues, out template, out properties))
#pragma warning restore Serilog004 // Constant MessageTemplate verifier
{
throw new XunitException("Template could not be bound.");
}
return new LogEvent(DateTimeOffset.Now, level, exception, template, properties);
}

public static LogEvent LogEvent(LogEventLevel level, string messageTemplate, params object[] propertyValues)
{
var log = new LoggerConfiguration().CreateLogger();

#pragma warning disable Serilog004 // Constant MessageTemplate verifier
if (!log.BindMessageTemplate(messageTemplate, propertyValues, out var template, out var properties))
#pragma warning restore Serilog004 // Constant MessageTemplate verifier
{
throw new XunitException("Template could not be bound.");
}
return new LogEvent(DateTimeOffset.Now, level, null, template, properties);
}

public static LogEvent DebugEvent()
{
return LogEvent(LogEventLevel.Debug, null, "Debug event");
}

public static LogEvent InformationEvent()
{
return LogEvent(LogEventLevel.Information, null, "Information event");
}

public static LogEvent ErrorEvent()
{
return LogEvent(LogEventLevel.Error, null, "Error event");
}

public static string String()
{
return Guid.NewGuid().ToString("n");
}
}
}
16 changes: 13 additions & 3 deletions test/Honeycomb.Serilog.Sink.Tests/HoneycombSerilogSinkStub.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
using System.Net.Http;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

using Serilog.Events;

namespace Honeycomb.Serilog.Sink.Tests
{
internal class HoneycombSerilogSinkStub : HoneycombSerilogSink
{
private readonly HttpClient _client;

public HoneycombSerilogSinkStub(HttpClient client, string teamId, string apiKey)
: base(teamId, apiKey)
public HoneycombSerilogSinkStub(HttpClient client, string teamId, string apiKey, int batchSizeLimit, TimeSpan period)
: base(teamId, apiKey, batchSizeLimit, period)
{
_client = client;
}

protected override HttpClient Client => _client;

public Task EmitTestable(params LogEvent[] events)
{
return EmitBatchAsync(events);
}
}
}
Loading

0 comments on commit 46494a7

Please sign in to comment.