Skip to content

Commit 5e60a5a

Browse files
authored
L-733 Create BetterStack.Logs.Serilog independent of NLog (#1)
1 parent f17bc8a commit 5e60a5a

18 files changed

+343
-496
lines changed

.gitignore

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
bin/*
2-
obj/*
1+
bin/
2+
obj/
33
global.json
44

5-
example-project/bin/*
6-
example-project/obj/*
5+
example-project/bin/
6+
example-project/obj/
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<PackageId>Logtail</PackageId>
4-
<Version>0.2.6</Version>
3+
<PackageId>BetterStack.Logs.Serilog</PackageId>
4+
<Version>1.0.0</Version>
55
<Authors>Simon Rozsival, Tomas Hromada</Authors>
66
<Company>Better Stack</Company>
77
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
88
<PackageReadmeFile>README.md</PackageReadmeFile>
99
<PackageIcon>icon.png</PackageIcon>
10-
<PackageProjectUrl>https://logtail.com</PackageProjectUrl>
11-
<RepositoryUrl>https://github.com/logtail/logtail-dotnet</RepositoryUrl>
10+
<PackageProjectUrl>https://logs.betterstack.com</PackageProjectUrl>
11+
<RepositoryUrl>https://github.com/BetterStackHQ/logs-client-serilog</RepositoryUrl>
1212
<RespositoryType>git</RespositoryType>
13-
<Tags>logging logtail livetail nlog</Tags>
13+
<Tags>betterstack logging livetail serilog</Tags>
1414
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1515
</PropertyGroup>
1616

1717
<PropertyGroup>
1818
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
19-
<DefaultItemExcludes>$(DefaultItemExcludes);example-project/**</DefaultItemExcludes>
19+
<DefaultItemExcludes>$(DefaultItemExcludes);example-project/**;dashboard.png</DefaultItemExcludes>
2020
</PropertyGroup>
2121

2222
<PropertyGroup Condition="'$(Configuration)'=='Release'">
@@ -37,7 +37,8 @@
3737

3838
<ItemGroup>
3939
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
40-
<PackageReference Include="NLog" Version="4.7.11" />
40+
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
41+
<PackageReference Include="Serilog.Sinks.Http" Version="8.0.0" />
4142
</ItemGroup>
4243

4344
<ItemGroup>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using Microsoft.Extensions.Configuration;
2+
using Serilog.Sinks.Http;
3+
using System.IO;
4+
using System.Net.Http.Headers;
5+
using System.Net.Http;
6+
using System.Threading.Tasks;
7+
using System;
8+
9+
namespace BetterStack.Logs.Serilog
10+
{
11+
/// <summary>
12+
/// HTTP client sending JSON to Better Stack over the network.
13+
/// </summary>
14+
public class BetterStackHttpClient : IHttpClient
15+
{
16+
private readonly HttpClient httpClient;
17+
18+
/// <summary>
19+
/// Initializes a new instance of the BetterStackHttpClient class with specified source token.
20+
/// </summary>
21+
/// <param name="sourceToken">
22+
/// Your source token (taken from https://logs.betterstack.com/dashboard -> Sources -> Edit)
23+
/// </param>
24+
public BetterStackHttpClient(string sourceToken)
25+
{
26+
this.httpClient = new HttpClient();
27+
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sourceToken);
28+
}
29+
30+
~BetterStackHttpClient()
31+
{
32+
Dispose(false);
33+
}
34+
35+
/// <inheritdoc />
36+
public virtual void Configure(IConfiguration configuration)
37+
{
38+
}
39+
40+
/// <inheritdoc />
41+
public virtual async Task<HttpResponseMessage> PostAsync(string requestUri, Stream contentStream)
42+
{
43+
using var content = new StreamContent(contentStream);
44+
content.Headers.Add("Content-Type", "application/json");
45+
46+
var response = await httpClient
47+
.PostAsync(requestUri, content)
48+
.ConfigureAwait(false);
49+
50+
return response;
51+
}
52+
53+
/// <inheritdoc />
54+
public void Dispose()
55+
{
56+
Dispose(true);
57+
GC.SuppressFinalize(this);
58+
}
59+
60+
protected virtual void Dispose(bool disposing)
61+
{
62+
if (disposing)
63+
{
64+
httpClient.Dispose();
65+
}
66+
}
67+
}
68+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using Serilog.Debugging;
2+
using Serilog.Events;
3+
using Serilog.Formatting.Json;
4+
using Serilog.Formatting;
5+
using System.IO;
6+
using System;
7+
8+
namespace BetterStack.Logs.Serilog
9+
{
10+
/// <summary>
11+
/// JSON formatter serializing log events for ingestion by Better Stack.
12+
/// </summary>
13+
public class BetterStackTextFormatter : ITextFormatter
14+
{
15+
private readonly JsonValueFormatter jsonValueFormatter;
16+
17+
public BetterStackTextFormatter()
18+
{
19+
this.jsonValueFormatter = new JsonValueFormatter();
20+
}
21+
22+
public void Format(LogEvent logEvent, TextWriter output)
23+
{
24+
try
25+
{
26+
var buffer = new StringWriter();
27+
FormatContent(logEvent, buffer);
28+
29+
// If formatting was successful, write to output
30+
output.WriteLine(buffer.ToString());
31+
}
32+
catch (Exception e)
33+
{
34+
SelfLog.WriteLine(
35+
"Event at {0} with message template {1} could not be formatted into JSON and will be dropped: {2}",
36+
logEvent.Timestamp.ToString("o"),
37+
logEvent.MessageTemplate.Text,
38+
e
39+
);
40+
}
41+
}
42+
43+
private void FormatContent(LogEvent logEvent, TextWriter output)
44+
{
45+
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
46+
if (output == null) throw new ArgumentNullException(nameof(output));
47+
48+
output.Write("{\"dt\":\"");
49+
output.Write(logEvent.Timestamp.UtcDateTime.ToString("o"));
50+
51+
output.Write("\",\"level\":\"");
52+
var level = logEvent.Level.ToString().ToUpper();
53+
output.Write(level == "INFORMATION" ? "INFO" : level);
54+
55+
output.Write("\",\"message\":");
56+
var message = logEvent.MessageTemplate.Render(logEvent.Properties);
57+
JsonValueFormatter.WriteQuotedJsonString(message, output);
58+
59+
output.Write(",\"messageTemplate\":");
60+
JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output);
61+
62+
if (logEvent.Exception != null)
63+
{
64+
output.Write(",\"exception\":");
65+
JsonValueFormatter.WriteQuotedJsonString(logEvent.Exception.ToString(), output);
66+
}
67+
68+
if (logEvent.Properties.Count != 0)
69+
{
70+
output.Write(",\"properties\":{");
71+
72+
var delimiter = string.Empty;
73+
foreach (var property in logEvent.Properties)
74+
{
75+
output.Write(delimiter);
76+
delimiter = ",";
77+
78+
JsonValueFormatter.WriteQuotedJsonString(property.Key, output);
79+
output.Write(':');
80+
jsonValueFormatter.Format(property.Value, output);
81+
}
82+
83+
output.Write('}');
84+
}
85+
86+
output.Write('}');
87+
}
88+
}
89+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using BetterStack.Logs.Serilog;
2+
using Serilog.Configuration;
3+
using Serilog.Core;
4+
using Serilog.Events;
5+
using Serilog.Formatting;
6+
using Serilog.Sinks.Http.BatchFormatters;
7+
using Serilog.Sinks.Http.Private.NonDurable;
8+
using System;
9+
10+
namespace Serilog
11+
{
12+
/// <summary>
13+
/// Class containing extension method to <see cref="LoggerConfiguration"/> for Better Stack sink config
14+
/// </summary>
15+
public static class LoggerSinkConfigurationExtensions
16+
{
17+
/// <summary>
18+
/// Adds a sink that sends log events to Better Stack using preconfigured Serilog.Sinks.Http
19+
/// The log events are stored in memory in the case that the log server cannot be reached.
20+
/// </summary>
21+
/// <param name="sinkConfiguration">The logger configuration.</param>
22+
/// <param name="sourceToken">
23+
/// Your source token (taken from https://logs.betterstack.com/dashboard -> Sources -> Edit)
24+
/// </param>
25+
/// <param name="betterStackEndpoint">
26+
/// The URI of the Better Stack endpoint your logs are sent to. Default value is https://in.logs.betterstack.com.
27+
/// </param>
28+
/// <param name="queueLimitBytes">
29+
/// The maximum size of events stored in memory, waiting to be sent. Default value is null (no limit).
30+
/// </param>
31+
/// <param name="batchSize">
32+
/// The maximum number of log events sent as a single batch. Default value is 1000.
33+
/// </param>
34+
/// <param name="batchInterval">
35+
/// The maximum time before sending logs to Better Stack. Default value is 1 second.
36+
/// </param>
37+
/// <param name="restrictedToMinimumLevel">
38+
/// The minimum level for events passed through the sink. Ignored if <paramref name="levelSwitch"/> specified.
39+
/// Default value is <see cref="LevelAlias.Minimum"/>.
40+
/// </param>
41+
/// <param name="levelSwitch">
42+
/// A switch allowing the pass-through level to be changed at runtime.
43+
/// </param>
44+
public static LoggerConfiguration BetterStack(
45+
this LoggerSinkConfiguration sinkConfiguration,
46+
string sourceToken,
47+
string betterStackEndpoint = "https://in.logs.betterstack.com",
48+
long? queueLimitBytes = null,
49+
int? batchSize = null,
50+
TimeSpan? batchInterval = null,
51+
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
52+
LoggingLevelSwitch? levelSwitch = null)
53+
{
54+
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
55+
56+
batchSize ??= 1000;
57+
batchInterval ??= TimeSpan.FromSeconds(1);
58+
59+
var sink = new HttpSink(
60+
requestUri: betterStackEndpoint,
61+
queueLimitBytes: queueLimitBytes,
62+
logEventLimitBytes: null,
63+
logEventsInBatchLimit: batchSize,
64+
batchSizeLimitBytes: null,
65+
period: batchInterval.Value,
66+
textFormatter: new BetterStackTextFormatter(),
67+
batchFormatter: new ArrayBatchFormatter(),
68+
httpClient: new BetterStackHttpClient(sourceToken));
69+
70+
return sinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch);
71+
}
72+
}
73+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
using Newtonsoft.Json;
77
using Newtonsoft.Json.Serialization;
88

9-
namespace Logtail
9+
namespace BetterStack.Logs
1010
{
1111
/// <summary>
12-
/// The Client class is responsible for reliable delivery of logs
13-
/// to the Logtail servers.
12+
/// The Client class is responsible for reliable delivery of logs to the Better Stack servers.
1413
/// </summary>
1514
public sealed class Client
1615
{
@@ -25,7 +24,7 @@ public sealed class Client
2524

2625
public Client(
2726
string sourceToken,
28-
string endpoint = "https://in.logtail.com",
27+
string endpoint = "https://in.logs.betterstack.com",
2928
TimeSpan? timeout = null,
3029
int retries = 10
3130
)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using System.Threading;
55
using System.Threading.Tasks;
66

7-
namespace Logtail
7+
namespace BetterStack.Logs
88
{
99
/// <summary>
1010
/// The Drain class is responsible for maintaining a queue of log events that need
@@ -24,7 +24,7 @@ public sealed class Drain
2424
private CancellationTokenSource cancellationTokenSource;
2525

2626
/// <summary>
27-
/// Initializes a Logtail drain and starts periodic logs delivery.
27+
/// Initializes a Better Stack Logs drain and starts periodic logs delivery.
2828
/// </summary>
2929
public Drain(
3030
Client client,

Logtail/DrainIsClosedException.cs renamed to BetterStack.Logs/DrainIsClosedException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System;
22

3-
namespace Logtail
3+
namespace BetterStack.Logs
44
{
55
public sealed class DrainIsClosedException : Exception
66
{

Logtail/Log.cs renamed to BetterStack.Logs/Log.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using System.Collections.Generic;
33
using Newtonsoft.Json;
44

5-
namespace Logtail
5+
namespace BetterStack.Logs
66
{
77
public sealed class Log
88
{

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
License
22

3-
Copyright (c) 2021, Logtail
3+
Copyright (c) 2021, Better Stack, Inc.
44

55
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
66

0 commit comments

Comments
 (0)