Skip to content
This repository has been archived by the owner on Sep 26, 2023. It is now read-only.

Commit

Permalink
Merge pull request #13 from Authenticom/develop
Browse files Browse the repository at this point in the history
Added periodic batching support to the primary sink
  • Loading branch information
sirkirby authored Sep 27, 2017
2 parents 179ef5a + 5589fb0 commit 19011e5
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public static LoggerConfiguration EventGrid(
string customSubjectPropertyName = "EventSubject",
string customTypePropertyName = "EventType",
CustomEventRequestAuth customEventRequestAuth = CustomEventRequestAuth.Key,
int batchSizeLimit = EventGridSink.DefaultBatchPostingLimit,
TimeSpan? period = null,
LogEventLevel restrictedToMinimumLevel = LogEventLevel.Information,
IFormatProvider formatProvider = null)
{
Expand All @@ -36,8 +38,19 @@ public static LoggerConfiguration EventGrid(
if (!topicUri.IsAbsoluteUri)
throw new ArgumentException("topicEndpoint must be an absolute uri");

var timePeriod = period ?? EventGridSink.DefaultPeriod;

return loggerConfiguration.Sink(
new EventGridSink(formatProvider, key, topicUri, customEventSubject, customEventType, customSubjectPropertyName, customTypePropertyName, customEventRequestAuth), restrictedToMinimumLevel);
new EventGridSink(
formatProvider,
key, topicUri,
customEventSubject,
customEventType,
customEventRequestAuth,
customSubjectPropertyName,
customTypePropertyName,
batchSizeLimit,
timePeriod), restrictedToMinimumLevel);
}
}
}
5 changes: 3 additions & 2 deletions src/Serilog.Sinks.EventGrid/Serilog.Sinks.EventGrid.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
<PackageIconUrl>http://serilog.net/images/serilog-sink-nuget.png</PackageIconUrl>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Authenticom/serilog-sinks-eventgrid</RepositoryUrl>
<PackageTags>serilog events eventgrid</PackageTags>
<PackageTags>serilog sink events eventgrid azure</PackageTags>
<Copyright>Copyright © Chris Kirby 2017</Copyright>
<Version>1.0.3</Version>
<Version>1.1.0</Version>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
Expand All @@ -26,6 +26,7 @@
<ItemGroup>
<PackageReference Include="newtonsoft.json" Version="10.0.3" />
<PackageReference Include="serilog" Version="2.5.0" />
<PackageReference Include="serilog.sinks.periodicbatching" Version="2.1.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.4.0" />
</ItemGroup>

Expand Down
146 changes: 146 additions & 0 deletions src/Serilog.Sinks.EventGrid/Sinks/EventGrid/EventGridClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Serilog.Events;

namespace Serilog.Sinks.EventGrid
{
public class EventGridClient
{
private readonly string _key;
private readonly Uri _topicUri;
private readonly string _customEventSubject;
private readonly string _customEventType;
private readonly CustomEventRequestAuth _customEventRequestAuth;
private readonly string _customSubjectPropertyName;
private readonly string _customTypePropertyName;

public EventGridClient(string key,
Uri topicUri,
string customEventSubject,
string customEventType,
CustomEventRequestAuth customEventRequestAuth,
string customSubjectPropertyName,
string customTypePropertyName)
{
_key = key;
_topicUri = topicUri;
_customEventSubject = customEventSubject;
_customEventType = customEventType;
_customEventRequestAuth = customEventRequestAuth;
_customSubjectPropertyName = customSubjectPropertyName;
_customTypePropertyName = customTypePropertyName;
}

public async Task SendEvent(LogEvent logEvent)
{
// make a dictionary from the log event properties
var props = logEvent.Properties
.Select(pv => new { Name = pv.Key, Value = EventGridPropertyFormatter.Simplify(pv.Value) })
.ToDictionary(a => a.Name, b => b.Value);

// build the request from sink config and log event properties
var customEvent = new CustomEventRequest
{
Subject = _customEventSubject ?? GetEventSubjectFromProperties(props),
EventType = _customEventType ?? GetEventTypeFromProperties(props)
};

// if we don't have what we need from the config or the event, pull event info from the call stack
if (string.IsNullOrEmpty(customEvent.Subject) || string.IsNullOrEmpty(customEvent.EventType))
GetEventInfoFromAttribute(customEvent);

// clean up the payload
props.Add("LogMessage", logEvent.MessageTemplate.Text);
customEvent.Data = props.Where(p => p.Key != _customSubjectPropertyName && p.Key != _customTypePropertyName);

// finally, we have what we need post the event
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, _topicUri);
request.Headers.Add(_customEventRequestAuth == CustomEventRequestAuth.Key ? "aeg-sas-key" : "aeg-sas-token", _key);
var body = new[] { customEvent };

var json = JsonConvert.SerializeObject(body, Newtonsoft.Json.Formatting.Indented, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
});

request.Content = new StringContent(json);
await client.SendAsync(request);
}

private string GetEventSubjectFromProperties(Dictionary<string, object> props)
{
return props.ContainsKey(_customSubjectPropertyName) ? props.First(p => p.Key == _customSubjectPropertyName).Value.ToString() : null;
}

private string GetEventTypeFromProperties(Dictionary<string, object> props)
{
return props.ContainsKey(_customTypePropertyName) ? props.First(p => p.Key == _customTypePropertyName).Value.ToString() : null;
}

private void GetEventInfoFromAttribute(CustomEventRequest customEvent)
{
// walk up the stack and check for our EventGridSinkAttribute
var st = new StackTrace();
var stackFrames = st.GetFrames();
if (stackFrames == null) return;

var methods = stackFrames.Where(f => f != null).Select(f => f.GetMethod()).ToArray();
// walk through serilog to reach the calling method
var callingMethod = methods.FirstOrDefault(m => !m?.DeclaringType?.FullName?.StartsWith("Serilog") ?? false) ?? methods.First();

var subjectAttributeValue = GetCustomValueFromAttribute<EventGridSubjectAttribute>(methods);
var typeAttributeValue = GetCustomValueFromAttribute<EventGridTypeAttribute>(methods);

// assign the event info, failing back to generic defaults
customEvent.Subject = customEvent.Subject ?? subjectAttributeValue ?? GetSubject();
customEvent.EventType = customEvent.EventType ?? typeAttributeValue ?? _customEventType ?? GetEventType() ?? "AppDomain/Class";

string GetSubject()
{
var methodName = GetMethodWithParams() ?? "Method/default";
return $"{methodName}";
}

string GetMethodWithParams()
{
var parameterNames = callingMethod != null && callingMethod.GetParameters().Any()
? callingMethod.GetParameters().Select(x => x.Name).Aggregate((combined, next) => combined += string.IsNullOrEmpty(combined) ? next : $"/{next}")
: "default";
var methodWithParameters = $"{callingMethod?.Name}/{parameterNames}";
return methodWithParameters;
}

string GetEventType()
{
var assemblyName = callingMethod?.ReflectedType?.Assembly.GetName().Name ?? "General";
var className = callingMethod?.ReflectedType?.Name ?? "Class";
return $"{assemblyName}/{className}";
}
}

private static string GetCustomValueFromAttribute<TAttribute>(MethodBase[] methods) where TAttribute : Attribute, IEventGridAttribute
{
TAttribute tAttribute;
// look for the first method in the stack with the type attribute
var methodAttribute = methods.FirstOrDefault(m => m.GetCustomAttribute<TAttribute>() != null)?.GetCustomAttribute<TAttribute>();
if (methodAttribute != null)
tAttribute = methodAttribute;
else
{
// then look for the first class with the attribute, there can be only one
var classAttribute = methods.FirstOrDefault(m => m.ReflectedType != null && m.ReflectedType.GetCustomAttribute<TAttribute>() != null)?.ReflectedType?.GetCustomAttribute<TAttribute>();
tAttribute = classAttribute;
}
return tAttribute?.CustomValue;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Serilog.Sinks.EventGrid
{
public static class EventGridPropertyFormatter
{
static readonly HashSet<Type> EventGridScalars = new HashSet<Type>
private static readonly HashSet<Type> EventGridScalars = new HashSet<Type>
{
typeof(bool),
typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint),
Expand Down Expand Up @@ -53,7 +53,7 @@ public static object Simplify(LogEventPropertyValue value)
return props;
}

static object SimplifyScalar(object value)
private static object SimplifyScalar(object value)
{
if (value == null) return null;

Expand Down
146 changes: 18 additions & 128 deletions src/Serilog.Sinks.EventGrid/Sinks/EventGrid/EventGridSink.cs
Original file line number Diff line number Diff line change
@@ -1,150 +1,40 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Serilog.Core;
using System.Threading.Tasks;
using Serilog.Events;
using Serilog.Sinks.PeriodicBatching;

namespace Serilog.Sinks.EventGrid
{
public class EventGridSink : ILogEventSink
public class EventGridSink : PeriodicBatchingSink
{
readonly string _key;
readonly Uri _topicUri;
readonly string _customEventSubject;
readonly string _customEventType;
readonly CustomEventRequestAuth _customEventRequestAuth;
readonly string _customSubjectPropertyName;
readonly string _customTypePropertyName;
private readonly EventGridClient _client;

public const int DefaultBatchPostingLimit = 10;
public static readonly TimeSpan DefaultPeriod = TimeSpan.FromSeconds(5);

public EventGridSink(IFormatProvider formatProvider,
string key,
Uri topicUri,
string customEventSubject = null,
string customEventType = null,
string customSubjectPropertyName = "EventSubject",
string customTypePropertyName = "EventType",
CustomEventRequestAuth customEventRequestAuth = CustomEventRequestAuth.Key)
string customEventSubject,
string customEventType,
CustomEventRequestAuth customEventRequestAuth,
string customSubjectPropertyName,
string customTypePropertyName,
int batchSizeLimit,
TimeSpan period) : base(batchSizeLimit, period)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentNullException("key");

_key = key;
_topicUri = topicUri;
_customEventSubject = customEventSubject;
_customEventType = customEventType;
_customSubjectPropertyName = customSubjectPropertyName;
_customTypePropertyName = customTypePropertyName;
_customEventRequestAuth = customEventRequestAuth;
}

public void Emit(LogEvent logEvent)
{
// make a dictionary from the log event properties
var props = logEvent.Properties
.Select(pv => new {Name = pv.Key, Value = EventGridPropertyFormatter.Simplify(pv.Value)})
.ToDictionary(a => a.Name, b => b.Value);

// build the request from sink config and log event properties
var customEvent = new CustomEventRequest
{
Subject = _customEventSubject ?? GetEventSubjectFromProperties(props),
EventType = _customEventType ?? GetEventTypeFromProperties(props)
};

// if we don't have what we need from the config or the event, pull event info from the call stack
if (string.IsNullOrEmpty(customEvent.Subject) || string.IsNullOrEmpty(customEvent.EventType))
GetEventInfoFromAttribute(customEvent);

// clean up the payload
props.Add("LogMessage", logEvent.MessageTemplate.Text);
customEvent.Data = props.Where(p => p.Key != _customSubjectPropertyName && p.Key != _customTypePropertyName);

// finally, we have what we need post the event
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, _topicUri);
request.Headers.Add(_customEventRequestAuth == CustomEventRequestAuth.Key ? "aeg-sas-key" : "aeg-sas-token", _key);
var body = new[] { customEvent };

var json = JsonConvert.SerializeObject(body, Newtonsoft.Json.Formatting.Indented, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
});

request.Content = new StringContent(json);
var response = client.SendAsync(request).Result;
}

string GetEventSubjectFromProperties(Dictionary<string, object> props)
{
return props.ContainsKey(_customSubjectPropertyName) ? props.First(p => p.Key == _customSubjectPropertyName).Value.ToString() : null;
}

string GetEventTypeFromProperties(Dictionary<string, object> props)
{
return props.ContainsKey(_customTypePropertyName) ? props.First(p => p.Key == _customTypePropertyName).Value.ToString() : null;
}

void GetEventInfoFromAttribute(CustomEventRequest customEvent)
{
// walk up the stack and check for our EventGridSinkAttribute
var st = new StackTrace();
var stackFrames = st.GetFrames();
if (stackFrames == null) return;

var methods = stackFrames.Where(f => f != null).Select(f => f.GetMethod()).ToArray();
// walk through serilog to reach the calling method
var callingMethod = methods.FirstOrDefault(m => !m?.DeclaringType?.FullName?.StartsWith("Serilog") ?? false) ?? methods.First();

var subjectAttributeValue = GetCustomValueFromAttribute<EventGridSubjectAttribute>(methods);
var typeAttributeValue = GetCustomValueFromAttribute<EventGridTypeAttribute>(methods);

// assign the event info, failing back to generic defaults
customEvent.Subject = customEvent.Subject ?? subjectAttributeValue ?? GetSubject();
customEvent.EventType = customEvent.EventType ?? typeAttributeValue ?? _customEventType ?? GetEventType() ?? "AppDomain/Class";

string GetSubject()
{
var methodName = GetMethodWithParams() ?? "Method/default";
return $"{methodName}";
}

string GetMethodWithParams()
{
var parameterNames = callingMethod != null && callingMethod.GetParameters().Any()
? callingMethod.GetParameters().Select(x => x.Name).Aggregate((combined, next) => combined += string.IsNullOrEmpty(combined) ? next : $"/{next}")
: "default";
var methodWithParameters = $"{callingMethod.Name}/{parameterNames}";
return methodWithParameters;
}

string GetEventType()
{
var assemblyName = callingMethod?.ReflectedType?.Assembly.GetName().Name ?? "General";
var className = callingMethod?.ReflectedType?.Name ?? "Class";
return $"{assemblyName}/{className}";
}
_client = new EventGridClient(key, topicUri, customEventSubject, customEventType, customEventRequestAuth, customSubjectPropertyName, customTypePropertyName);
}

private string GetCustomValueFromAttribute<TAttribute>(MethodBase[] methods) where TAttribute : Attribute, IEventGridAttribute
protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
{
TAttribute tAttribute;
// look for the first method in the stack with the type attribute
var methodAttribute = methods.FirstOrDefault(m => m.GetCustomAttribute<TAttribute>() != null)?.GetCustomAttribute<TAttribute>();
if (methodAttribute != null)
tAttribute = methodAttribute;
else
{
// then look for the first class with the attribute, there can be only one
var classAttribute = methods.FirstOrDefault(m => m.ReflectedType != null && m.ReflectedType.GetCustomAttribute<TAttribute>() != null)?.ReflectedType?.GetCustomAttribute<TAttribute>();
tAttribute = classAttribute;
}
return tAttribute?.CustomValue;
var tasks = events.Select(_client.SendEvent);
await Task.WhenAll(tasks);
}
}
}

0 comments on commit 19011e5

Please sign in to comment.