-
Notifications
You must be signed in to change notification settings - Fork 30
/
LokiBatchFormatter.cs
226 lines (196 loc) · 7.38 KB
/
LokiBatchFormatter.cs
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
using System.Text;
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Sinks.Grafana.Loki.Models;
using Serilog.Sinks.Grafana.Loki.Utils;
namespace Serilog.Sinks.Grafana.Loki;
/// <summary>
/// Formatter serializing batches of log events into a JSON object in the format, recognized by Grafana Loki.
/// <para/>
/// Example:
/// <code>
/// {
/// "streams": [
/// {
/// "stream": {
/// "label": "value"
/// },
/// "values": [
/// [ "unix epoch in nanoseconds", "log line" ],
/// [ "unix epoch in nanoseconds", "log line" ]
/// ]
/// }
/// ]
/// }
/// </code>
/// </summary>
internal class LokiBatchFormatter : ILokiBatchFormatter
{
private const int DefaultWriteBufferCapacity = 256;
private readonly IEnumerable<LokiLabel> _globalLabels;
private readonly IReservedPropertyRenamingStrategy _renamingStrategy;
private readonly IEnumerable<string> _propertiesAsLabels;
private readonly bool _leavePropertiesIntact;
private readonly bool _useInternalTimestamp;
/// <summary>
/// Initializes a new instance of the <see cref="LokiBatchFormatter"/> class.
/// </summary>
/// <param name="renamingStrategy">
/// Renaming strategy for properties' names equal to reserved keywords.
/// <see cref="IReservedPropertyRenamingStrategy"/>
/// </param>
/// <param name="globalLabels">
/// The list of global <see cref="LokiLabel"/>.
/// </param>
/// <param name="propertiesAsLabels">
/// The list of properties, which would be mapped to the labels.
/// </param>
/// <param name="useInternalTimestamp">
/// Compute internal timestamp
/// </param>
/// <param name="leavePropertiesIntact">
/// Leave the list of properties intact after extracting the labels specified in propertiesAsLabels.
/// </param>
public LokiBatchFormatter(
IReservedPropertyRenamingStrategy renamingStrategy,
IEnumerable<LokiLabel>? globalLabels = null,
IEnumerable<string>? propertiesAsLabels = null,
bool useInternalTimestamp = false,
bool leavePropertiesIntact = false)
{
_renamingStrategy = renamingStrategy;
_globalLabels = globalLabels ?? Enumerable.Empty<LokiLabel>();
_propertiesAsLabels = propertiesAsLabels ?? Enumerable.Empty<string>();
_useInternalTimestamp = useInternalTimestamp;
_leavePropertiesIntact = leavePropertiesIntact;
}
/// <summary>
/// Format the log events into a payload.
/// </summary>
/// <param name="lokiLogEvents">
/// The events to format wrapped in <see cref="LokiLogEvent"/>.
/// </param>
/// <param name="formatter">
/// The formatter turning the log events into a textual representation.
/// </param>
/// <param name="output">
/// The payload to send over the network.
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown if one of params is null.
/// </exception>
public void Format(
IReadOnlyCollection<LokiLogEvent> lokiLogEvents,
ITextFormatter formatter,
TextWriter output)
{
if (lokiLogEvents == null)
{
throw new ArgumentNullException(nameof(lokiLogEvents));
}
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
if (lokiLogEvents.Count == 0)
{
return;
}
var batch = new LokiBatch();
// Group logEvent by labels
var groups = lokiLogEvents
.Select(AddLevelAsPropertySafely)
.Select(GenerateLabels)
.GroupBy(
le => le.Labels,
le => le.LokiLogEvent,
DictionaryComparer<string, string>.Instance);
foreach (var group in groups)
{
var labels = group.Key;
var stream = batch.CreateStream();
foreach (var (key, value) in labels)
{
stream.AddLabel(key, value);
}
foreach (var logEvent in group.OrderBy(x => _useInternalTimestamp ? x.InternalTimestamp : x.LogEvent.Timestamp))
{
GenerateEntry(
logEvent,
formatter,
stream);
}
}
if (batch.IsNotEmpty)
{
output.Write(batch.Serialize());
}
// Current behavior breaks rendering
// Log.Info("Hero's {level}", 42)
// Message: "Hero's \"info\""
// level: "info"
// _level: 42
LokiLogEvent AddLevelAsPropertySafely(LokiLogEvent lokiLogEvent)
{
var logEvent = lokiLogEvent.LogEvent;
logEvent.RenamePropertyIfPresent("level", _renamingStrategy.Rename);
logEvent.AddOrUpdateProperty(
new LogEventProperty("level", new ScalarValue(logEvent.Level.ToGrafanaLogLevel())));
return lokiLogEvent;
}
}
private void GenerateEntry(
LokiLogEvent lokiLogEvent,
ITextFormatter formatter,
LokiStream stream)
{
var buffer = new StringWriter(new StringBuilder(DefaultWriteBufferCapacity));
var logEvent = lokiLogEvent.LogEvent;
var timestamp = logEvent.Timestamp;
if (_useInternalTimestamp)
{
logEvent.AddPropertyIfAbsent(
new LogEventProperty("Timestamp", new ScalarValue(timestamp)));
timestamp = lokiLogEvent.InternalTimestamp;
}
formatter.Format(logEvent, buffer);
stream.AddEntry(timestamp, buffer.ToString().TrimEnd('\r', '\n'));
}
private (Dictionary<string, string> Labels, LokiLogEvent LokiLogEvent) GenerateLabels(LokiLogEvent lokiLogEvent)
{
var labels = _globalLabels.ToDictionary(label => label.Key, label => label.Value);
var properties = lokiLogEvent.Properties;
var (propertiesAsLabels, remainingProperties) =
properties.Partition(kvp => _propertiesAsLabels.Contains(kvp.Key));
foreach (var property in propertiesAsLabels)
{
var key = property.Key;
// If a message template is a composite format string that contains indexed placeholders ({0}, {1} etc),
// Serilog turns these placeholders into event properties keyed by numeric strings.
// Loki doesn't accept such strings as label keys. Prefix these numeric strings with "param"
// to turn them into valid label keys and at the same time denote them as ordinal parameters.
if (char.IsDigit(key[0]))
{
key = $"param{key}";
}
// Some enrichers generates extra quotes and it breaks the payload
var value = property.Value.ToString().Replace("\"", string.Empty);
if (labels.ContainsKey(key))
{
SelfLog.WriteLine(
"Labels already contains key {0}, added from global labels. Property value ({1}) with the same key is ignored",
key,
value);
continue;
}
labels.Add(key, value);
}
return (labels,
lokiLogEvent.CopyWithProperties(_leavePropertiesIntact ? properties : remainingProperties));
}
}