Skip to content

Commit

Permalink
Custom sink for handling large audit events (#15)
Browse files Browse the repository at this point in the history
* added structure for the additional serilog sink for blob and eventhub

* added addition blob storage serilog sink. now audit events would go to both eventhub and blob destinations

* Separate extension logger methods created for blob and eventhub for the purpose of conditional logging

* Adding custom sink

* adding blob upload

* upload blob with name as event id

* Renaming project

* restructured the folders

* adding event hub upload

* Adding message check

* upgrade image to chcek if build is passing

* reverting the image upgrade as build failed

* configurable event size limit and renaming variables

* removing unwanted files

* changing variable name

* audit client

* pr comments

* pr comments

* pr comments

* pr comments

* library updated

* fixing appveyor build issues

* code fix for build failure

* pr review comments

* removing blob name from class variable

* pr comments

* renaming blob name

* removing unused package reference

* remove unwanted interfaces

* refactoring

* pr comments

* renaming variable

* added extension method accepting blob client

* pr comments

* pr comments

Co-authored-by: Avin Mathew (UYW) <UYW@kmd.dk>
Co-authored-by: Satyajit <YYW@kmd.dk>
  • Loading branch information
3 people authored Nov 3, 2020
1 parent 3ce7c91 commit 7dff721
Show file tree
Hide file tree
Showing 15 changed files with 656 additions and 0 deletions.
36 changes: 36 additions & 0 deletions kmd-logic-audit-client.sln
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kmd.Logic.Audit.Client.Seri
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kmd.Logic.Audit.Client.SampleEventHubs", "sample\Kmd.Logic.Audit.Client.SampleEventHubs\Kmd.Logic.Audit.Client.SampleEventHubs.csproj", "{8921E5CF-FC63-4641-B265-EEA4939BACFA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kmd.Logic.Audit.Client.SerilogLargeAuditEvents", "src\Kmd.Logic.Audit.Client.SerilogLargeAuditEvents\Kmd.Logic.Audit.Client.SerilogLargeAuditEvents.csproj", "{4DD5B679-97AB-42DA-86CF-C999ED9BE797}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink", "src\Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink\Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink.csproj", "{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -134,6 +138,33 @@ Global
{8921E5CF-FC63-4641-B265-EEA4939BACFA}.Release|x64.Build.0 = Release|Any CPU
{8921E5CF-FC63-4641-B265-EEA4939BACFA}.Release|x86.ActiveCfg = Release|Any CPU
{8921E5CF-FC63-4641-B265-EEA4939BACFA}.Release|x86.Build.0 = Release|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Debug|x64.ActiveCfg = Debug|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Debug|x64.Build.0 = Debug|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Debug|x86.ActiveCfg = Debug|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Debug|x86.Build.0 = Debug|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Release|Any CPU.Build.0 = Release|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Release|x64.ActiveCfg = Release|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Release|x64.Build.0 = Release|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Release|x86.ActiveCfg = Release|Any CPU
{4DD5B679-97AB-42DA-86CF-C999ED9BE797}.Release|x86.Build.0 = Release|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Debug|x64.ActiveCfg = Debug|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Debug|x64.Build.0 = Debug|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Debug|x86.ActiveCfg = Debug|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Debug|x86.Build.0 = Debug|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Release|Any CPU.Build.0 = Release|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Release|x64.ActiveCfg = Release|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Release|x64.Build.0 = Release|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Release|x86.ActiveCfg = Release|Any CPU
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -146,6 +177,11 @@ Global
{B3870AF6-BD84-4943-A0FD-EA79DFE18480} = {C26E8896-8D49-4B97-8A19-556F4EC3A357}
{0159FC58-57BE-435A-B0B8-678D48AFCF8B} = {C928BF1D-6596-4F8F-AB5E-67308DA6D26B}
{8921E5CF-FC63-4641-B265-EEA4939BACFA} = {C26E8896-8D49-4B97-8A19-556F4EC3A357}
{4DD5B679-97AB-42DA-86CF-C999ED9BE797} = {C928BF1D-6596-4F8F-AB5E-67308DA6D26B}
{5AFCFD4C-F6FA-472C-9AEA-E4F5B4C9DBB4} = {C928BF1D-6596-4F8F-AB5E-67308DA6D26B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7B6A42A3-6999-43F8-9F44-C64FC107FA47}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9A26425E-4672-4E22-8415-29BE7A953DA1}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using Serilog.Events;
using Serilog.Formatting;

namespace Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink
{
/// <summary>
/// This class handles methods related to Audit event payload
/// </summary>
public static class AuditEventPayload
{
public const string MessageTemplate = "Event details for event id {0} can be found at {1}";

/// <summary>
/// This method checks if audit event payload size is more than the limit or not
/// </summary>
/// <param name="textFormatter">Formatter</param>
/// <param name="logEvent">Log event</param>
/// <param name="eventSizeLimit">Event size limit</param>
/// <returns>Returns if message size is greater than the limit</returns>
public static bool DoesAuditEventPayloadExceedLimit(ITextFormatter textFormatter, LogEvent logEvent, int eventSizeLimit)
{
string content;
using (var render = new StringWriter())
{
textFormatter.Format(logEvent, render);
content = render.ToString();
}

if (Encoding.UTF8.GetByteCount(content) > eventSizeLimit)
{
return true;
}

return false;
}

/// <summary>
/// Transform the logevent to provide different message with blob url property added
/// </summary>
/// <param name="logEvent">Log event</param>
/// <param name="blobUrl">Blob url</param>
/// <returns>New log event after message transformation</returns>
public static LogEvent AuditEventMessageTransformation(LogEvent logEvent, Uri blobUrl)
{
var properties = new List<LogEventProperty>();
foreach (var property in logEvent.Properties)
{
var logEventProperty = new LogEventProperty(property.Key, property.Value);
properties.Add(logEventProperty);
}

var newLogEvent = new LogEvent(
logEvent.Timestamp,
logEvent.Level,
logEvent.Exception,
new MessageTemplate(string.Format(CultureInfo.InvariantCulture, MessageTemplate, logEvent.Properties["_EventId"], blobUrl), logEvent.MessageTemplate.Tokens),
properties);

return newLogEvent;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using Azure.Storage.Blobs;
using Microsoft.Azure.EventHubs;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Sinks.AzureEventHub;

namespace Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink
{
public class AzureBlobOrEventHubCustomSink : ILogEventSink
{
private readonly BlobServiceClient blobServiceClient;
private readonly ITextFormatter textFormatter;
private readonly int eventSizeLimit;
private readonly string storageContainerName;
private readonly AzureEventHubSink azureEventHubSink;

public AzureBlobOrEventHubCustomSink(
BlobServiceClient blobServiceClient,
EventHubClient eventHubClient,
ITextFormatter textFormatter,
int eventSizeLimit = 256 * 1024,
string storageContainerName = "logs")
{
this.textFormatter = textFormatter;
this.blobServiceClient = blobServiceClient;
this.storageContainerName = storageContainerName;
this.eventSizeLimit = eventSizeLimit;
this.azureEventHubSink = new AzureEventHubSink(eventHubClient, textFormatter);
}

public void Emit(LogEvent logEvent)
{
if (AuditEventPayload.DoesAuditEventPayloadExceedLimit(this.textFormatter, logEvent, this.eventSizeLimit))
{
var blobUrl = this.UploadToBlob(logEvent);
logEvent = AuditEventPayload.AuditEventMessageTransformation(logEvent, blobUrl);
}

this.azureEventHubSink.Emit(logEvent);
}

/// <summary>
/// Handles uploading to Azure blob
/// </summary>
/// <param name="logEvent">Log event</param>
/// <returns>blob url</returns>
private Uri UploadToBlob(LogEvent logEvent)
{
var content = AzureBlobServiceHelper.PrepareBlobContentForUpload(this.textFormatter, logEvent);
var eventId = string.Empty;

// Get Event Id value and use it as blob name
logEvent.Properties.TryGetValue("_EventId", out var eventIdValue);
if (eventIdValue != null)
{
eventId = eventIdValue.ToString();
}

return AzureBlobServiceProvider.UploadBlob(this.blobServiceClient, this.storageContainerName, eventId, content);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.IO;
using Serilog.Events;
using Serilog.Formatting;

namespace Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink
{
public static class AzureBlobServiceHelper
{
public static string PrepareBlobContentForUpload(ITextFormatter textFormatter, LogEvent logEvent)
{
if (textFormatter == null)
{
throw new ArgumentNullException(nameof(textFormatter));
}

if (logEvent == null)
{
throw new ArgumentNullException(nameof(logEvent));
}

string content;

using (var tempStringWriter = new StringWriter())
{
try
{
textFormatter.Format(logEvent, tempStringWriter);
tempStringWriter.Flush();
}
catch (Exception ex)
{
Serilog.Debugging.SelfLog.WriteLine($"Exception {ex} thrown during logEvent formatting. The log event will be dropped.");
}

content = tempStringWriter.ToString();
}

return content;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.IO;
using System.Text;
using Azure.Storage.Blobs;

namespace Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink
{
public static class AzureBlobServiceProvider
{
public const string PathDivider = "/";

/// <summary>
/// Uploads blob and return the blob url
/// </summary>
/// <param name="blobServiceClient">Blob service client</param>
/// <param name="blobContainerName">Container name</param>
/// <param name="eventId">Event id from log context</param>
/// <param name="content">Content to be uploaded</param>
/// <returns>Blob url</returns>
public static Uri UploadBlob(BlobServiceClient blobServiceClient, string blobContainerName, string eventId, string content)
{
// Get a reference to a blob
var blobClient = GetBlobClient(blobServiceClient, blobContainerName, eventId);
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)))
{
try
{
blobClient.Upload(stream);
return blobClient.Uri;
}
catch (Exception ex)
{
Serilog.Debugging.SelfLog.WriteLine($"Exception {ex} thrown while trying to upload blob.");
return null;
}
}
}

/// <summary>
/// Check if container or blob exists or not and return a blob client object accordingly
/// </summary>
/// <param name="blobServiceClient">Blob service client</param>
/// <param name="blobContainerName">Container name</param>
/// <param name="eventId">Event id from log context</param>
/// <returns>Blob client</returns>
private static BlobClient GetBlobClient(BlobServiceClient blobServiceClient, string blobContainerName, string eventId)
{
var dt = DateTimeOffset.UtcNow;
var containerClient = blobServiceClient.GetBlobContainerClient(blobContainerName);
containerClient.CreateIfNotExists();

// If event id is empty then create a new guid. This scenario is remotely likely to happen
eventId = eventId ?? Guid.NewGuid().ToString();
eventId = $"{dt:yyyy}{PathDivider}{dt:MM}{PathDivider}{dt:dd}{PathDivider}{dt:yyyyMMdd_HHMM}_{eventId}.log";
return containerClient.GetBlobClient(eventId);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.6.0" />
<PackageReference Include="Microsoft.Azure.EventHubs" Version="4.3.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.AzureEventHub" Version="5.0.0" />
</ItemGroup>

</Project>
Loading

0 comments on commit 7dff721

Please sign in to comment.