-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathClient.cs
More file actions
140 lines (123 loc) · 5.54 KB
/
Copy pathClient.cs
File metadata and controls
140 lines (123 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace BetterStack.Logs
{
/// <summary>
/// The Client class is responsible for reliable delivery of logs to the Better Stack servers.
/// </summary>
public sealed class Client
{
private readonly HttpClient httpClient;
private readonly JsonSerializerSettings settings = new JsonSerializerSettings {
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ContractResolver = new DefaultContractResolver {
NamingStrategy = new CamelCaseNamingStrategy()
}
};
private readonly int retries;
public Client(
string sourceToken,
string endpoint = "https://in.logs.betterstack.com",
TimeSpan? timeout = null,
int retries = 10
)
{
settings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
settings.Converters.Add(new ToStringJsonConverter(typeof(System.Reflection.MemberInfo)));
settings.Converters.Add(new ToStringJsonConverter(typeof(System.Reflection.Assembly)));
settings.Converters.Add(new ToStringJsonConverter(typeof(System.Reflection.Module)));
settings.Error = (sender, args) =>
{
args.ErrorContext.Handled = true; // Ignore Properties that throws Exceptions
};
httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {sourceToken}");
httpClient.BaseAddress = new Uri(endpoint);
httpClient.Timeout = timeout ?? TimeSpan.FromSeconds(10);
this.retries = retries;
}
/// <summary>
/// Sends a collection of logs to the server with several retries
/// if an error occures.
/// </summary>
public async Task Send(IEnumerable<Log> logs)
{
var payload = serialize(logs);
for (int i = 0; i < retries; ++i) {
await Task.Delay(TimeSpan.FromSeconds(i));
var success = await sendOnce(payload);
if (success) break;
}
}
private async Task<bool> sendOnce(byte[] payload)
{
try {
// Every attempt needs its own HttpContent. On .NET Framework, HttpClient disposes
// the request content as soon as the request completes -- on success, on 5xx, on
// timeout and on network error alike -- so reusing one instance makes every retry
// after the first throw ObjectDisposedException instead of reaching the server.
using (var content = buildContent(payload))
using (var response = await httpClient.PostAsync("/", content)) {
return response.IsSuccessStatusCode;
}
} catch (TaskCanceledException ex) {
// request timed out
global::NLog.Common.InternalLogger.Warn(ex, "BetterStack.Logs: request timed out.");
} catch (HttpRequestException ex) {
// TODO: repeat only for certain HTTP errors (429, 5xx)
// some networking error
global::NLog.Common.InternalLogger.Warn(ex, "BetterStack.Logs: request failed.");
} catch (Exception ex) {
// An unexpected exception must never escape: it would fault the Drain's delivery
// task and silently stop all logging for the lifetime of the process.
global::NLog.Common.InternalLogger.Error(ex, "BetterStack.Logs: unexpected error while sending logs.");
}
return false;
}
private byte[] serialize(IEnumerable<Log> logs) {
var payload = JsonConvert.SerializeObject(logs, settings);
return Encoding.UTF8.GetBytes(payload);
}
private HttpContent buildContent(byte[] payload) {
var content = new ByteArrayContent(payload);
content.Headers.Add("Content-Type", "application/json");
return content;
}
/// <summary>
/// JSON converter that just calls ToString on the target value (when non-null).
/// This is configured as the converter for types that will otherwise spew a lot of irrelevant JSON
/// into logs.
/// </summary>
internal sealed class ToStringJsonConverter : JsonConverter
{
private readonly System.Type _type;
/// <inheritdoc />
public override bool CanRead => false;
public ToStringJsonConverter(System.Type type) =>
_type = type;
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is null)
{
writer.WriteNull();
}
else
{
writer.WriteValue(value.ToString());
}
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer) =>
throw new NotSupportedException("Only serialization is supported");
/// <inheritdoc />
public override bool CanConvert(System.Type objectType) =>
_type.IsAssignableFrom(objectType);
}
}
}