diff --git a/kmd-logic-audit-client.sln b/kmd-logic-audit-client.sln old mode 100755 new mode 100644 index d6f4c48..b8c533a --- a/kmd-logic-audit-client.sln +++ b/kmd-logic-audit-client.sln @@ -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 @@ -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 @@ -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} diff --git a/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AuditEventPayload.cs b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AuditEventPayload.cs new file mode 100644 index 0000000..e6312b4 --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AuditEventPayload.cs @@ -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 +{ + /// + /// This class handles methods related to Audit event payload + /// + public static class AuditEventPayload + { + public const string MessageTemplate = "Event details for event id {0} can be found at {1}"; + + /// + /// This method checks if audit event payload size is more than the limit or not + /// + /// Formatter + /// Log event + /// Event size limit + /// Returns if message size is greater than the limit + 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; + } + + /// + /// Transform the logevent to provide different message with blob url property added + /// + /// Log event + /// Blob url + /// New log event after message transformation + public static LogEvent AuditEventMessageTransformation(LogEvent logEvent, Uri blobUrl) + { + var properties = new List(); + 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; + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AzureBlobOrEventHubCustomSink.cs b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AzureBlobOrEventHubCustomSink.cs new file mode 100644 index 0000000..d3f2771 --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AzureBlobOrEventHubCustomSink.cs @@ -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); + } + + /// + /// Handles uploading to Azure blob + /// + /// Log event + /// blob url + 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); + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AzureBlobServiceHelper.cs b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AzureBlobServiceHelper.cs new file mode 100644 index 0000000..41f0483 --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AzureBlobServiceHelper.cs @@ -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; + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AzureBlobServiceProvider.cs b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AzureBlobServiceProvider.cs new file mode 100644 index 0000000..c5f5e4f --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/AzureBlobServiceProvider.cs @@ -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 = "/"; + + /// + /// Uploads blob and return the blob url + /// + /// Blob service client + /// Container name + /// Event id from log context + /// Content to be uploaded + /// Blob url + 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; + } + } + } + + /// + /// Check if container or blob exists or not and return a blob client object accordingly + /// + /// Blob service client + /// Container name + /// Event id from log context + /// Blob client + 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); + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink.csproj b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink.csproj new file mode 100644 index 0000000..2bc7ca8 --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink.csproj @@ -0,0 +1,15 @@ + + + + Library + netstandard2.0 + + + + + + + + + + diff --git a/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/LoggerConfigurationAzureBlobOrEventHubExtensions.cs b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/LoggerConfigurationAzureBlobOrEventHubExtensions.cs new file mode 100644 index 0000000..0853023 --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink/LoggerConfigurationAzureBlobOrEventHubExtensions.cs @@ -0,0 +1,102 @@ +using System; +using Azure.Storage.Blobs; +using Microsoft.Azure.EventHubs; +using Serilog; +using Serilog.Configuration; +using Serilog.Formatting; + +namespace Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink +{ + public static class LoggerConfigurationAzureBlobOrEventHubExtensions + { + public static LoggerConfiguration AzureBlobOrEventHub( + this LoggerSinkConfiguration loggerConfiguration, + string storageConnectionString, + string eventHubConnectionString, + string eventHubName, + int eventSizeLimitInBytes, + string storageContainerName = null, + ITextFormatter formatter = null) + { + if (loggerConfiguration == null) + { + throw new ArgumentNullException(nameof(loggerConfiguration)); + } + + if (string.IsNullOrEmpty(storageConnectionString)) + { + throw new ArgumentNullException(nameof(storageConnectionString)); + } + + if (string.IsNullOrWhiteSpace(eventHubConnectionString)) + { + throw new ArgumentNullException(nameof(eventHubConnectionString)); + } + + if (string.IsNullOrWhiteSpace(eventHubName)) + { + throw new ArgumentNullException(nameof(eventHubName)); + } + + if (formatter == null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + var eventHubConnectionstringBuilder = new EventHubsConnectionStringBuilder(eventHubConnectionString) + { + EntityPath = eventHubName + }; + + var eventHubclient = EventHubClient.CreateFromConnectionString(eventHubConnectionstringBuilder.ToString()); + + var blobServiceClient = new BlobServiceClient(storageConnectionString); + + return loggerConfiguration.Sink(new AzureBlobOrEventHubCustomSink(blobServiceClient, eventHubclient, formatter, eventSizeLimitInBytes, storageContainerName)); + } + + public static LoggerConfiguration AzureBlobOrEventHub( + this LoggerSinkConfiguration loggerConfiguration, + string eventHubConnectionString, + string eventHubName, + int eventSizeLimitInBytes, + BlobServiceClient blobServiceClient, + string storageContainerName = null, + ITextFormatter formatter = null) + { + if (loggerConfiguration == null) + { + throw new ArgumentNullException(nameof(loggerConfiguration)); + } + + if (string.IsNullOrWhiteSpace(eventHubConnectionString)) + { + throw new ArgumentNullException(nameof(eventHubConnectionString)); + } + + if (string.IsNullOrWhiteSpace(eventHubName)) + { + throw new ArgumentNullException(nameof(eventHubName)); + } + + if (blobServiceClient == null) + { + throw new ArgumentNullException(nameof(blobServiceClient)); + } + + if (formatter == null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + var eventHubConnectionstringBuilder = new EventHubsConnectionStringBuilder(eventHubConnectionString) + { + EntityPath = eventHubName + }; + + var eventHubclient = EventHubClient.CreateFromConnectionString(eventHubConnectionstringBuilder.ToString()); + + return loggerConfiguration.Sink(new AzureBlobOrEventHubCustomSink(blobServiceClient, eventHubclient, formatter, eventSizeLimitInBytes, storageContainerName)); + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/CreatedDateTimeEnricher.cs b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/CreatedDateTimeEnricher.cs new file mode 100644 index 0000000..22b8b9a --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/CreatedDateTimeEnricher.cs @@ -0,0 +1,14 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace Kmd.Logic.Audit.Client.SerilogLargeAuditEvents +{ + internal class CreatedDateTimeEnricher : ILogEventEnricher + { + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("_CreatedDateTime", DateTimeOffset.UtcNow)); + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/EventIdEnricher.cs b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/EventIdEnricher.cs new file mode 100644 index 0000000..50b3de4 --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/EventIdEnricher.cs @@ -0,0 +1,14 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace Kmd.Logic.Audit.Client.SerilogLargeAuditEvents +{ + internal class EventIdEnricher : ILogEventEnricher + { + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("_EventId", Guid.NewGuid())); + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents.csproj b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents.csproj new file mode 100644 index 0000000..f36a7b1 --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + Library + + + + + + + + + + + + + diff --git a/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogCustomSinkLoggerAudit.cs b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogCustomSinkLoggerAudit.cs new file mode 100644 index 0000000..ff2853f --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogCustomSinkLoggerAudit.cs @@ -0,0 +1,26 @@ +using System; +using Serilog; + +namespace Kmd.Logic.Audit.Client.SerilogLargeAuditEvents +{ + internal class SerilogCustomSinkLoggerAudit : IAudit + { + private readonly ILogger logger; + + public SerilogCustomSinkLoggerAudit(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void Write(string messageTemplate, params object[] propertyValues) + { + this.logger.Information(messageTemplate, propertyValues); + } + + public IAudit ForContext(string propertyName, object value, bool captureObjectStructure = false) + { + return new SerilogCustomSinkLoggerAudit( + this.logger.ForContext(propertyName, value, destructureObjects: captureObjectStructure)); + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLargeAuditEventClient.cs b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLargeAuditEventClient.cs new file mode 100644 index 0000000..97d6cf1 --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLargeAuditEventClient.cs @@ -0,0 +1,63 @@ +using System; +using Azure.Storage.Blobs; +using Serilog.Core; + +namespace Kmd.Logic.Audit.Client.SerilogLargeAuditEvents +{ + public class SerilogLargeAuditEventClient : IAudit, IDisposable + { + private readonly Logger customSinkLogger; + private readonly bool disposeLogger; + private readonly SerilogCustomSinkLoggerAudit auditToCustomSink; + + public SerilogLargeAuditEventClient(SerilogLargeAuditEventClientConfiguration config) + { + this.customSinkLogger = config.CreateDefaultAzureBlobOrEventHubConfiguration().CreateLogger(); + this.disposeLogger = true; + this.auditToCustomSink = new SerilogCustomSinkLoggerAudit(this.customSinkLogger); + } + + public SerilogLargeAuditEventClient(SerilogLargeAuditEventClientConfiguration config, BlobServiceClient blobServiceClient) + { + this.customSinkLogger = config.CreateAzureBlobOrEventHubConfiguration(blobServiceClient).CreateLogger(); + this.disposeLogger = true; + this.auditToCustomSink = new SerilogCustomSinkLoggerAudit(this.customSinkLogger); + } + + private SerilogLargeAuditEventClient(Logger logger, bool disposeLogger) + { + this.customSinkLogger = logger; + this.disposeLogger = disposeLogger; + this.auditToCustomSink = new SerilogCustomSinkLoggerAudit(this.customSinkLogger); + } + + public static SerilogLargeAuditEventClient CreateCustomized(Logger logger, bool disposeLogger = true) + { + return new SerilogLargeAuditEventClient(logger, disposeLogger); + } + + public void Write(string messageTemplate, params object[] propertyValues) + { + this.auditToCustomSink.Write(messageTemplate, propertyValues); + } + + public IAudit ForContext(string propertyName, object value, bool captureObjectStructure = false) + { + return this.auditToCustomSink.ForContext(propertyName, value, captureObjectStructure); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && this.disposeLogger) + { + this.customSinkLogger.Dispose(); + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLargeAuditEventClientConfiguration.cs b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLargeAuditEventClientConfiguration.cs new file mode 100644 index 0000000..fe5e527 --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLargeAuditEventClientConfiguration.cs @@ -0,0 +1,47 @@ +namespace Kmd.Logic.Audit.Client.SerilogLargeAuditEvents +{ + public class SerilogLargeAuditEventClientConfiguration + { + /// + /// Gets or sets a value that will be tagged on every event to make + /// it clear what the source of the event was. + /// + public string EventSource { get; set; } + + /// + /// Gets or sets the Azure EventHubs connection string. + /// + public string EventHubConnectionString { get; set; } + + /// + /// Gets or sets the Azure Blob ConnectionString. + /// + public string StorageConnectionString { get; set; } + + /// + /// Gets or sets the Azure Blob account name. + /// + public string StorageAccountName { get; set; } + + /// + /// Gets or sets the Azure Blob container name. + /// + public string StorageContainerName { get; set; } = "logs"; + + /// + /// Gets or sets the the topic where audit events are ingested on Azure EventHubs. + /// Normally just leave this as the default value of "audit". + /// + public string AuditEventTopic { get; set; } = "audit"; + + /// + /// Gets or sets if it is going to use enrich from logcontext + /// + public bool? EnrichFromLogContext { get; set; } + + /// + /// Gets or sets the event size limit which decides whether message will be pushed to blob and event hub or only event hub + /// + public int EventSizeLimitinBytes { get; set; } = 256 * 1024; + } +} diff --git a/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLargeAuditEventClientConfigurationExtensions.cs b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLargeAuditEventClientConfigurationExtensions.cs new file mode 100644 index 0000000..ebe7caa --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLargeAuditEventClientConfigurationExtensions.cs @@ -0,0 +1,64 @@ +using System; +using Azure.Storage.Blobs; +using Kmd.Logic.Audit.Client.AzureBlobOrEventHubSink; +using Serilog; + +namespace Kmd.Logic.Audit.Client.SerilogLargeAuditEvents +{ + public static class SerilogLargeAuditEventClientConfigurationExtensions + { + public static LoggerConfiguration CreateDefaultAzureBlobOrEventHubConfiguration(this SerilogLargeAuditEventClientConfiguration config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + var configBuilder = new LoggerConfiguration() + .Enrich.With(new EventIdEnricher()) + .Enrich.With(new CreatedDateTimeEnricher()) + .Enrich.WithProperty("_EventSource", config.EventSource) + .WriteTo.AzureBlobOrEventHub( + storageConnectionString: config.StorageConnectionString, + storageContainerName: config.StorageContainerName, + eventHubConnectionString: config.EventHubConnectionString, + eventHubName: config.AuditEventTopic, + eventSizeLimitInBytes: config.EventSizeLimitinBytes, + formatter: new Serilog.Formatting.Compact.CompactJsonFormatter()); + + if (config.EnrichFromLogContext == true) + { + configBuilder = configBuilder.Enrich.FromLogContext(); + } + + return configBuilder; + } + + public static LoggerConfiguration CreateAzureBlobOrEventHubConfiguration(this SerilogLargeAuditEventClientConfiguration config, BlobServiceClient blobServiceClient) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + var configBuilder = new LoggerConfiguration() + .Enrich.With(new EventIdEnricher()) + .Enrich.With(new CreatedDateTimeEnricher()) + .Enrich.WithProperty("_EventSource", config.EventSource) + .WriteTo.AzureBlobOrEventHub( + eventHubConnectionString: config.EventHubConnectionString, + eventHubName: config.AuditEventTopic, + eventSizeLimitInBytes: config.EventSizeLimitinBytes, + blobServiceClient: blobServiceClient, + storageContainerName: config.StorageContainerName, + formatter: new Serilog.Formatting.Compact.CompactJsonFormatter()); + + if (config.EnrichFromLogContext == true) + { + configBuilder = configBuilder.Enrich.FromLogContext(); + } + + return configBuilder; + } + } +} diff --git a/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLoggerAudit.cs b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLoggerAudit.cs new file mode 100644 index 0000000..6acdb4b --- /dev/null +++ b/src/Kmd.Logic.Audit.Client.SerilogLargeAuditEvents/SerilogLoggerAudit.cs @@ -0,0 +1,26 @@ +using System; +using Serilog; + +namespace Kmd.Logic.Audit.Client.SerilogLargeAuditEvents +{ + internal class SerilogLoggerAudit : IAudit + { + private readonly ILogger logger; + + public SerilogLoggerAudit(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void Write(string messageTemplate, params object[] propertyValues) + { + this.logger.Information(messageTemplate, propertyValues); + } + + public IAudit ForContext(string propertyName, object value, bool captureObjectStructure = false) + { + return new SerilogLoggerAudit( + this.logger.ForContext(propertyName, value, destructureObjects: captureObjectStructure)); + } + } +}