-
Notifications
You must be signed in to change notification settings - Fork 65
/
LogEventConverter.cs
403 lines (339 loc) · 13.3 KB
/
LogEventConverter.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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Serilog.Events;
using static Elastic.CommonSchema.Serilog.SpecialProperties;
namespace Elastic.CommonSchema.Serilog
{
/// <summary>
/// Elastic Common Schema converter for LogEvent
/// </summary>
public static class LogEventConverter
{
private static Agent DefaultAgent { get; } = EcsDocument.CreateAgent(typeof(LogEventConverter));
/// <summary>
/// Converts a <see cref="LogEvent"/> to an <typeparamref name="TEcsDocument"/>
/// </summary>
public static TEcsDocument ConvertToEcs<TEcsDocument>(LogEvent logEvent, IEcsTextFormatterConfiguration<TEcsDocument> configuration)
where TEcsDocument : EcsDocument, new()
{
var ecsEvent = EcsDocument.CreateNewWithDefaults<TEcsDocument>(logEvent.Timestamp, logEvent.Exception, configuration);
if (logEvent.TryGetScalarString(SpecialKeys.MachineName, out var machineName))
{
ecsEvent.Host ??= new Host();
ecsEvent.Host.Name = machineName;
}
// if we don't want to lookup up process through System.Diagnostics still include whatever information we get from
// serilog
if (!configuration.IncludeProcess)
ecsEvent.Process = GetProcessFromProperties(logEvent);
// prefer tracing information set by Elastic APM
if (TryGetTrace(logEvent, out var traceId)) ecsEvent.TraceId = traceId;
if (TryGetTransaction(logEvent, out var transactionId)) ecsEvent.TransactionId = transactionId;
if (TryGetSpan(logEvent, out var spanId)) ecsEvent.SpanId = spanId;
// prefer service information set by Elastic APM
var service = GetService(logEvent);
if (service != null) ecsEvent.Service = service;
// prefer our own user information, especially in web contexts this is richer
var user = GetUser(logEvent, configuration);
if (user != null) ecsEvent.User = user;
ecsEvent.Message = logEvent.RenderMessage(configuration.MessageFormatProvider);
ecsEvent.Log = GetLog(logEvent);
ecsEvent.Agent = GetAgent(logEvent) ?? DefaultAgent;
ecsEvent.Event = GetEvent(logEvent);
ecsEvent.Server = GetServer(logEvent, configuration);
ecsEvent.Http = GetHttp(logEvent, configuration);
ecsEvent.Url = GetUrl(logEvent, configuration);
ecsEvent.UserAgent = GetUserAgent(logEvent, configuration);
ecsEvent.Client = GetClient(logEvent, configuration);
var metaData = GetMetadata(logEvent, configuration.LogEventPropertiesToFilter);
foreach (var kv in metaData)
ecsEvent.AssignField(kv.Key, kv.Value);
if (configuration.MapCustom != null)
ecsEvent = configuration.MapCustom(ecsEvent, logEvent);
return ecsEvent;
}
private static Service? GetService(LogEvent logEvent)
{
if (!logEvent.TryGetScalarString("ElasticApmServiceName", out var serviceName))
return null;
return new Service
{
Name = serviceName,
Version = logEvent.TryGetScalarString("ElasticApmServiceVersion", out var version) ? version : null,
NodeName = logEvent.TryGetScalarString("ElasticApmServiceNodeName", out var name) ? name : null
};
}
private static bool TryGetTrace(LogEvent logEvent, [NotNullWhen(true)]out string? traceId)
{
traceId = logEvent.TryGetScalarString("ElasticApmTraceId", out var id) ? id : null;
return !string.IsNullOrWhiteSpace(traceId);
}
private static bool TryGetTransaction(LogEvent logEvent, [NotNullWhen(true)]out string? transactionId)
{
transactionId = logEvent.TryGetScalarString("ElasticApmTransactionId", out var id) ? id : null;
return !string.IsNullOrWhiteSpace(transactionId);
}
private static bool TryGetSpan(LogEvent logEvent, [NotNullWhen(true)]out string? spanId)
{
spanId = logEvent.TryGetScalarString("ElasticApmSpanId", out var id) ? id : null;
return !string.IsNullOrWhiteSpace(spanId);
}
private static MetadataDictionary GetMetadata(LogEvent logEvent, ISet<string>? logEventPropertiesToFilter)
{
var dict = new MetadataDictionary { { "MessageTemplate", logEvent.MessageTemplate.Text } };
//TODO what does this do and where does it come from?
if (logEvent.Properties.TryGetValue("ActionPayload", out var actionPayload))
{
var logEventPropertyValues = (actionPayload as SequenceValue)?.Elements;
if (logEventPropertyValues != null)
{
foreach (var item in logEventPropertyValues.Select(x => x.ToString()
.Replace("\"", string.Empty)
.Replace("[", string.Empty)
.Replace("]", string.Empty)
.Split(','))
.Select(value => new { Key = value[0].Trim(), Value = value[1].Trim() }))
dict.Add(item.Key, item.Value);
}
}
foreach (var logEventPropertyValue in logEvent.Properties)
{
if (PropertyAlreadyMapped(logEventPropertyValue.Key))
continue;
//key present in list of keys to filter
if (logEventPropertiesToFilter?.Contains(logEventPropertyValue.Key) ?? false)
continue;
dict.Add(logEventPropertyValue.Key, PropertyValueToObject(logEventPropertyValue.Value));
}
return dict;
}
private static bool PropertyAlreadyMapped(string property)
{
switch (property)
{
// already mapped as structured ECS event
case SpecialKeys.SourceContext:
case SpecialKeys.EnvironmentUserName:
case SpecialKeys.Host:
case SpecialKeys.ActionCategory:
case SpecialKeys.ActionName:
case SpecialKeys.ActionId:
case SpecialKeys.ActionKind:
case SpecialKeys.ActionSeverity:
case SpecialKeys.EventId:
case SpecialKeys.ApplicationId:
case SpecialKeys.ApplicationName:
case SpecialKeys.ApplicationType:
case SpecialKeys.ApplicationVersion:
case SpecialKeys.ProcessName:
case SpecialKeys.ProcessId:
case SpecialKeys.ThreadId:
case SpecialKeys.MachineName:
case SpecialKeys.Elapsed:
case SpecialKeys.ElapsedMilliseconds:
case SpecialKeys.Method:
case SpecialKeys.RequestMethod:
case SpecialKeys.Path:
case SpecialKeys.RequestPath:
case SpecialKeys.StatusCode:
case SpecialKeys.Scheme:
case SpecialKeys.QueryString:
case SpecialKeys.RequestId:
case SpecialKeys.HttpContext:
case SpecialKeys.ContentType:
case SpecialKeys.HostingRequestFinishedLog:
return true;
default:
return false;
}
}
private static object PropertyValueToObject(LogEventPropertyValue propertyValue)
{
switch (propertyValue)
{
case SequenceValue values:
return values.Elements.Select(PropertyValueToObject).ToArray();
case ScalarValue sv:
return sv.Value;
case DictionaryValue dv:
return dv.Elements.ToDictionary(keySelector: kvp => kvp.Key.Value.ToString(),
elementSelector: (kvp) => PropertyValueToObject(kvp.Value));
case StructureValue ov:
{
var dict = ov.Properties.ToDictionary(p => p.Name, p => PropertyValueToObject(p.Value));
if (ov.TypeTag != null) dict.Add("$type", ov.TypeTag);
return dict;
}
default:
return propertyValue;
}
}
private static Server? GetServer(LogEvent e, IEcsTextFormatterConfiguration configuration)
{
e.TryGetScalarString(SpecialKeys.EnvironmentUserName, out var environmentUserName);
e.TryGetScalarString(SpecialKeys.Host, out var host);
var server = configuration.MapHttpAdapter?.Server;
if (server == null && environmentUserName == null && host == null)
return null;
server ??= new Server();
server.User = environmentUserName != null
? new User { Name = environmentUserName }
: null;
server.Address = host;
return server;
}
private static Process? GetProcessFromProperties(LogEvent e)
{
e.TryGetScalarString(SpecialKeys.ProcessName, out var processName);
e.TryGetScalarString(SpecialKeys.ProcessId, out var processId);
e.TryGetScalarString(SpecialKeys.ThreadId, out var threadId);
if (processName == null && processId == null && threadId== null)
return null;
var pid = int.TryParse(processId ?? "", out var p) ? p : (int?)null;
return new Process
{
Title = string.IsNullOrEmpty(processName) ? null : processName,
Name = processName,
Pid = pid,
ThreadId = int.TryParse(threadId ?? processId, out var id) ? id : null,
};
}
private static Log GetLog(LogEvent e)
{
var source = e.TryGetScalarString(SpecialKeys.SourceContext, out var context)
? context
: SpecialKeys.DefaultLogger;
var log = new Log { Level = e.Level.ToString("F"), Logger = source };
return log;
}
private static Event GetEvent(LogEvent e)
{
var hasElapsedMs = e.TryGetScalarPropertyValue(SpecialKeys.Elapsed, out var elapsedMsObj)
|| e.TryGetScalarPropertyValue(SpecialKeys.ElapsedMilliseconds, out elapsedMsObj);
var elapsedMs = hasElapsedMs ? (double?)Convert.ToDouble(elapsedMsObj!.Value) : null;
var evnt = new Event
{
Created = e.Timestamp,
Category = e.TryGetScalarString(SpecialKeys.ActionCategory, out var actionCategory)
? new[] { actionCategory }
: null,
Action = e.TryGetScalarString(SpecialKeys.ActionName, out var action)
? action
: null,
Id = e.TryGetScalarString(SpecialKeys.ActionId, out var actionId)
? actionId
: null,
Kind = e.TryGetScalarString(SpecialKeys.ActionKind, out var actionKind) ? actionKind : null,
Severity = e.TryGetScalarString(SpecialKeys.ActionSeverity, out var actionSev)
? long.Parse(actionSev)
: (int)e.Level,
Timezone = TimeZoneInfo.Local.StandardName,
Duration = elapsedMs != null ? (long)(elapsedMs * 1000000) : null
};
if (e.Properties.TryGetValue(SpecialKeys.EventId, out var eventData) && eventData is StructureValue dv)
{
var idProp = dv.Properties.FirstOrDefault(p => p.Name == "Id");
var eventId = idProp?.Value is ScalarValue i ? i.Value as int? : null;
if (eventId != null)
evnt.Code = eventId.ToString();
var nameProp = dv.Properties.FirstOrDefault(p => p.Name == "Name");
var eventAction = nameProp?.Value is ScalarValue n ? n.Value as string : null;
if (eventAction != null)
evnt.Action = eventAction;
}
return evnt;
}
private static Agent? GetAgent(LogEvent e)
{
Agent? agent = null;
void Assign(string key, Action<Agent, string> assign)
{
if (!e.TryGetScalarString(key, out var v)) return;
agent ??= new Agent();
assign(agent, v);
}
Assign(SpecialKeys.ApplicationId, (a, v) => a.Id = v);
Assign(SpecialKeys.ApplicationName, (a, v) => a.Name = v);
Assign(SpecialKeys.ApplicationType, (a, v) => a.Type = v);
Assign(SpecialKeys.ApplicationVersion, (a, v) => a.Version = v);
return agent;
}
private static Http? GetHttp(LogEvent e, IEcsTextFormatterConfiguration configuration)
{
if (e.TryGetScalarPropertyValue(SpecialKeys.HttpContext, out var httpContext)
&& httpContext.Value is HttpContextEnrichments enriched)
return enriched.Http;
var http = configuration.MapHttpAdapter?.Http;
if (e.TryGetScalarString(SpecialKeys.Method, out var method) || e.TryGetScalarString(SpecialKeys.RequestMethod, out method))
{
http ??= new Http();
http.RequestMethod = method;
}
if (e.TryGetScalarString(SpecialKeys.RequestId, out var requestId))
{
http ??= new Http();
http.RequestId = requestId;
}
if (e.TryGetScalarPropertyValue(SpecialKeys.StatusCode, out var statusCode))
{
http ??= new Http();
http.ResponseStatusCode = statusCode.Value is int s ? s : null;
}
if (e.TryGetScalarString(SpecialKeys.ContentType, out var contentType))
{
http ??= new Http();
http.ResponseMimeType = contentType;
}
return http;
}
private static Url? GetUrl(LogEvent e, IEcsTextFormatterConfiguration configuration)
{
if (e.TryGetScalarPropertyValue(SpecialKeys.HttpContext, out var httpContext)
&& httpContext.Value is HttpContextEnrichments enriched)
return enriched.Url;
var url = configuration.MapHttpAdapter?.Url;
if (e.TryGetScalarString(SpecialKeys.Path, out var path) || e.TryGetScalarString(SpecialKeys.RequestPath, out path))
{
url ??= new Url();
url.Path = path;
}
if (e.TryGetScalarString(SpecialKeys.Scheme, out var scheme))
{
url ??= new Url();
url.Scheme = scheme;
}
if (e.TryGetScalarString(SpecialKeys.QueryString, out var queryString))
{
url ??= new Url();
url.Query = string.IsNullOrEmpty(queryString) ? null : queryString.TrimStart('?');
}
return url;
}
private static UserAgent? GetUserAgent(LogEvent e, IEcsTextFormatterConfiguration configuration)
{
if (e.TryGetScalarPropertyValue(SpecialKeys.HttpContext, out var httpContext)
&& httpContext.Value is HttpContextEnrichments enriched)
return enriched.UserAgent;
return configuration.MapHttpAdapter?.UserAgent;
}
private static User? GetUser(LogEvent e, IEcsTextFormatterConfiguration configuration)
{
if (e.TryGetScalarPropertyValue(SpecialKeys.HttpContext, out var httpContext)
&& httpContext.Value is HttpContextEnrichments enriched)
return enriched.User;
return configuration.MapHttpAdapter?.User;
}
private static Client? GetClient(LogEvent e, IEcsTextFormatterConfiguration configuration)
{
if (e.TryGetScalarPropertyValue(SpecialKeys.HttpContext, out var httpContext)
&& httpContext.Value is HttpContextEnrichments enriched)
return enriched.Client;
return configuration.MapHttpAdapter?.Client;
}
}
}