From 1ac6dc9f680d3d239fcb14b16936ce8c944f18dc Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 11:00:21 +0200 Subject: [PATCH 01/19] :truck: Move Constants to different files --- Constants.cs | 24 ------------------------ Constants/HttpContextConstants.cs | 16 ++++++++++++++++ Constants/QueuesConstants.cs | 8 ++++++++ Constants/ServersConstants.cs | 8 ++++++++ LogLevels.cs | 11 +++++++++++ 5 files changed, 43 insertions(+), 24 deletions(-) delete mode 100644 Constants.cs create mode 100644 Constants/HttpContextConstants.cs create mode 100644 Constants/QueuesConstants.cs create mode 100644 Constants/ServersConstants.cs create mode 100644 LogLevels.cs diff --git a/Constants.cs b/Constants.cs deleted file mode 100644 index 77ea87b..0000000 --- a/Constants.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace NLogFlake -{ - public static class Servers - { - public const string PRODUCTION = "https://app.logflake.io"; - public const string TEST = "https://app-test.logflake.io"; - } - - public static class Queues - { - public const string LOGS = "logs"; - public const string PERFORMANCES = "perf"; - } - - public enum LogLevels - { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3, - FATAL = 4, - EXCEPTION = 5, - } -} diff --git a/Constants/HttpContextConstants.cs b/Constants/HttpContextConstants.cs new file mode 100644 index 0000000..49e79d0 --- /dev/null +++ b/Constants/HttpContextConstants.cs @@ -0,0 +1,16 @@ +namespace NLogFlake.Constants; + +internal static class HttpContextConstants +{ + internal const string ParentCorrelationHeader = "parent-correlation"; + + internal const string AutoLogGlobalExceptions = "AUTOLOG_GLOBAL_EXCEPTIONS"; + + internal const string TraceContext = "TRACE_CONTEXT"; + + internal const string ClientId = "clientId"; + + internal const string ClientIdOther = "azp"; + + internal const string HasCatchedError = "HasCatchedError"; +} diff --git a/Constants/QueuesConstants.cs b/Constants/QueuesConstants.cs new file mode 100644 index 0000000..ab828bc --- /dev/null +++ b/Constants/QueuesConstants.cs @@ -0,0 +1,8 @@ +namespace NLogFlake.Constants; + +internal static class QueuesConstants +{ + internal const string LOGS = "logs"; + + internal const string PERFORMANCES = "perf"; +} diff --git a/Constants/ServersConstants.cs b/Constants/ServersConstants.cs new file mode 100644 index 0000000..ed36b3c --- /dev/null +++ b/Constants/ServersConstants.cs @@ -0,0 +1,8 @@ +namespace NLogFlake.Constants; + +internal static class ServersConstants +{ + internal const string PRODUCTION = "https://app.logflake.io"; + + internal const string TEST = "https://app-test.logflake.io"; +} diff --git a/LogLevels.cs b/LogLevels.cs new file mode 100644 index 0000000..f6106c2 --- /dev/null +++ b/LogLevels.cs @@ -0,0 +1,11 @@ +namespace NLogFlake; + +public enum LogLevels +{ + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + FATAL = 4, + EXCEPTION = 5, +} From 327e4fa56cb976853435d38aba82001e4a345c69 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 12:09:06 +0200 Subject: [PATCH 02/19] :label: Create Options for LogFlake configuration --- Models/LogFlakeSettings.cs | 16 ++++++++++++ Models/Options/LogFlakeOptions.Validator.cs | 6 +++++ Models/Options/LogFlakeOptions.cs | 14 +++++++++++ .../LogFlakeSettingsOptions.Validator.cs | 6 +++++ Models/Options/LogFlakeSettingsOptions.cs | 25 +++++++++++++++++++ 5 files changed, 67 insertions(+) create mode 100644 Models/LogFlakeSettings.cs create mode 100644 Models/Options/LogFlakeOptions.Validator.cs create mode 100644 Models/Options/LogFlakeOptions.cs create mode 100644 Models/Options/LogFlakeSettingsOptions.Validator.cs create mode 100644 Models/Options/LogFlakeSettingsOptions.cs diff --git a/Models/LogFlakeSettings.cs b/Models/LogFlakeSettings.cs new file mode 100644 index 0000000..6377f86 --- /dev/null +++ b/Models/LogFlakeSettings.cs @@ -0,0 +1,16 @@ +namespace NLogFlake.Models; + +public sealed class LogFlakeSettings +{ + public bool AutoLogRequest { get; set; } + + public bool AutoLogResponse { get; set; } + + public bool AutoLogUnauthorized { get; set; } + + public bool AutoLogGlobalExceptions { get; set; } + + public bool PerformanceMonitor { get; set; } + + public IEnumerable? ExcludedPaths { get; set; } +} diff --git a/Models/Options/LogFlakeOptions.Validator.cs b/Models/Options/LogFlakeOptions.Validator.cs new file mode 100644 index 0000000..ce89371 --- /dev/null +++ b/Models/Options/LogFlakeOptions.Validator.cs @@ -0,0 +1,6 @@ +using Microsoft.Extensions.Options; + +namespace NLogFlake.Models.Options; + +[OptionsValidator] +internal sealed partial class LogFlakeOptionsValidator : IValidateOptions; diff --git a/Models/Options/LogFlakeOptions.cs b/Models/Options/LogFlakeOptions.cs new file mode 100644 index 0000000..750cfb6 --- /dev/null +++ b/Models/Options/LogFlakeOptions.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace NLogFlake.Models.Options; + +internal sealed class LogFlakeOptions +{ + public const string SectionName = "LogFlake"; + + [Required] + public string? AppId { get; set; } + + [Url] + public Uri? Endpoint { get; set; } +} diff --git a/Models/Options/LogFlakeSettingsOptions.Validator.cs b/Models/Options/LogFlakeSettingsOptions.Validator.cs new file mode 100644 index 0000000..ef6d4f7 --- /dev/null +++ b/Models/Options/LogFlakeSettingsOptions.Validator.cs @@ -0,0 +1,6 @@ +using Microsoft.Extensions.Options; + +namespace NLogFlake.Models.Options; + +[OptionsValidator] +internal sealed partial class LogFlakeSettingsOptionsValidator : IValidateOptions; diff --git a/Models/Options/LogFlakeSettingsOptions.cs b/Models/Options/LogFlakeSettingsOptions.cs new file mode 100644 index 0000000..96a16f1 --- /dev/null +++ b/Models/Options/LogFlakeSettingsOptions.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace NLogFlake.Models.Options; + +public sealed class LogFlakeSettingsOptions +{ + public const string SectionName = "LogFlakeSettings"; + + [Required] + public bool AutoLogRequest { get; set; } + + [Required] + public bool AutoLogGlobalExceptions { get; set; } + + [Required] + public bool AutoLogUnauthorized { get; set; } + + [Required] + public bool AutoLogResponse { get; set; } + + [Required] + public bool PerformanceMonitor { get; set; } + + public IEnumerable? ExcludedPaths { get; set; } +} From 6d87f4bfb7b2ddbb4db32dc28b4e18e9123cf06b Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 12:21:18 +0200 Subject: [PATCH 03/19] :sparkles: Include LogFlakeService --- Services/ILogFlakeService.cs | 16 ++++++++++ Services/LogFlakeService.cs | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 Services/ILogFlakeService.cs create mode 100644 Services/LogFlakeService.cs diff --git a/Services/ILogFlakeService.cs b/Services/ILogFlakeService.cs new file mode 100644 index 0000000..4a2fd11 --- /dev/null +++ b/Services/ILogFlakeService.cs @@ -0,0 +1,16 @@ +using NLogFlake.Models; + +namespace NLogFlake.Services; + +public interface ILogFlakeService +{ + LogFlakeSettings Settings { get; } + + void WriteLog(LogLevels logLevels, string? message, string? correlation, Dictionary? parameters = null); + + void WriteException(Exception ex, string? correlation, string? message = null, Dictionary? parameters = null); + + IPerformanceCounter MeasurePerformance(string label); + + bool SendPerformance(string label, long duration); +} diff --git a/Services/LogFlakeService.cs b/Services/LogFlakeService.cs new file mode 100644 index 0000000..edbaead --- /dev/null +++ b/Services/LogFlakeService.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Options; +using NLogFlake.Models; +using NLogFlake.Models.Options; + +namespace NLogFlake.Services; + +public class LogFlakeService : ILogFlakeService +{ + private readonly ILogFlake _logFlake; + + private readonly string _version; + + public LogFlakeSettings Settings { get; } + + public LogFlakeService(ILogFlake logFlake, IOptions logFlakeSettingsOptions, IVersionService versionService) + { + _logFlake = logFlake; + + Settings = new LogFlakeSettings + { + AutoLogRequest = logFlakeSettingsOptions.Value.AutoLogRequest, + AutoLogResponse = logFlakeSettingsOptions.Value.AutoLogResponse, + AutoLogUnauthorized = logFlakeSettingsOptions.Value.AutoLogUnauthorized, + AutoLogGlobalExceptions = logFlakeSettingsOptions.Value.AutoLogGlobalExceptions, + PerformanceMonitor = logFlakeSettingsOptions.Value.PerformanceMonitor, + ExcludedPaths = logFlakeSettingsOptions.Value.ExcludedPaths, + }; + + _version = versionService.Version; + } + + public void WriteLog(LogLevels logLevels, string? message, string? correlation, Dictionary? parameters = null) + { + parameters?.Add("Assembly version", _version); + + _logFlake.SendLog(logLevels, correlation, message, parameters); + } + + public void WriteException(Exception ex, string? correlation, string? message = null, Dictionary? parameters = null) + { + _logFlake.SendException(ex, correlation); + + WriteLog(LogLevels.FATAL, string.IsNullOrWhiteSpace(message) ? (ex?.Message ?? string.Empty) : $"{message}\n{ex?.Message ?? string.Empty}", correlation, parameters); + } + + public IPerformanceCounter MeasurePerformance(string label) => _logFlake.MeasurePerformance(label); + + public bool SendPerformance(string label, long duration) + { + try + { + _logFlake.SendPerformance(label, duration); + } + catch (ObjectDisposedException) + { + return false; + } + + return true; + } + +} From 1741d6fd2ae0eefafd45df44dd7a741ac15d69c2 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 12:49:30 +0200 Subject: [PATCH 04/19] :recycle: Create Interface for LogFlake class --- ILogFlake.cs | 18 ++++ LogFlake.cs | 271 +++++++++++++++++++++++++++------------------------ 2 files changed, 160 insertions(+), 129 deletions(-) create mode 100644 ILogFlake.cs diff --git a/ILogFlake.cs b/ILogFlake.cs new file mode 100644 index 0000000..66856ac --- /dev/null +++ b/ILogFlake.cs @@ -0,0 +1,18 @@ +namespace NLogFlake; + +public interface ILogFlake +{ + void SendLog(string content, Dictionary? parameters = null); + + void SendLog(LogLevels level, string content, Dictionary? parameters = null); + + void SendLog(LogLevels level, string? correlation, string? content, Dictionary? parameters = null); + + void SendException(Exception e); + + void SendException(Exception e, string? correlation); + + void SendPerformance(string label, long duration); + + IPerformanceCounter MeasurePerformance(string label); +} diff --git a/LogFlake.cs b/LogFlake.cs index e2ef982..b3fa80e 100644 --- a/LogFlake.cs +++ b/LogFlake.cs @@ -1,173 +1,186 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Diagnostics; -using System.Net; -using System.Net.Http; using System.Text; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using NLogFlake.Constants; using NLogFlake.Models; +using NLogFlake.Models.Options; using Snappier; -namespace NLogFlake +namespace NLogFlake; + +internal class LogFlake : ILogFlake { - public class LogFlake - { - private string Server { get; set; } = Servers.PRODUCTION; - private string _hostname = Environment.MachineName; - private string AppId { get; set; } + private Uri Server { get; set; } + private string? _hostname = Environment.MachineName; + private string AppId { get; set; } - private readonly ConcurrentQueue _logsQueue = new ConcurrentQueue(); - private readonly ManualResetEvent _processLogs = new ManualResetEvent(false); - private Thread LogsProcessorThread { get; set; } - private bool IsShuttingDown { get; set; } + private readonly ConcurrentQueue _logsQueue = new(); + private readonly ManualResetEvent _processLogs = new(false); + private readonly HttpClient _httpClient; - public int FailedPostRetries { get; set; } = 3; - public int PostTimeoutSeconds { get; set; } = 3; - public bool EnableCompression { get; set; } = true; + private Thread LogsProcessorThread { get; set; } + private bool IsShuttingDown { get; set; } - public void SetHostname() => SetHostname(null); + internal int FailedPostRetries { get; set; } = 3; + internal int PostTimeoutSeconds { get; set; } = 3; + internal bool EnableCompression { get; set; } = true; - public string GetHostname() => _hostname; + internal void SetHostname() => SetHostname(null); - public void SetHostname(string hostname) => _hostname = string.IsNullOrEmpty(hostname) ? null : hostname; + internal string? GetHostname() => _hostname; - public LogFlake(string appId, string logFlakeServer) : this(appId) => Server = logFlakeServer; + internal void SetHostname(string? hostname) => _hostname = string.IsNullOrWhiteSpace(hostname) ? null : hostname; - public LogFlake(string appId) - { - if (appId.Length == 0) throw new LogFlakeException("appId missing"); - AppId = appId; - LogsProcessorThread = new Thread(LogsProcessor); - LogsProcessorThread.Start(); - } + public LogFlake(IOptions logFlakeOptions, IHttpClientFactory httpClientFactory) + { + AppId = logFlakeOptions.Value.AppId!; + + Server = logFlakeOptions.Value.Endpoint ?? new Uri(ServersConstants.PRODUCTION); - ~LogFlake() => Shutdown(); + LogsProcessorThread = new Thread(LogsProcessor); + LogsProcessorThread.Start(); - public void Shutdown() + _httpClient = new HttpClient { - IsShuttingDown = true; - LogsProcessorThread.Join(); - } + BaseAddress = Server, + Timeout = TimeSpan.FromSeconds(PostTimeoutSeconds), + }; + _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "logflake-client-netcore/1.4.2"); + } + + ~LogFlake() => Shutdown(); - private void LogsProcessor() + public void Shutdown() + { + IsShuttingDown = true; + LogsProcessorThread.Join(); + } + + private void LogsProcessor() + { + SendLog(LogLevels.DEBUG, $"LogFlake started on {_hostname}"); + + _processLogs.WaitOne(); + + while (!_logsQueue.IsEmpty) { - SendLog(LogLevels.DEBUG, $"LogFlake started on {_hostname}"); - _processLogs.WaitOne(); - while (!_logsQueue.IsEmpty) + _ = _logsQueue.TryDequeue(out PendingLog? log); + log.Retries++; + bool success = Post(log.QueueName!, log.JsonString!).Result; + if (!success && log.Retries < FailedPostRetries) + { + _logsQueue.Enqueue(log); + } + + _processLogs.Reset(); + + if (_logsQueue.IsEmpty && !IsShuttingDown) { - // Process log - _logsQueue.TryDequeue(out var log); - log.Retries++; - var success = Post(log.QueueName, log.JsonString).Result; - if (!success && log.Retries < FailedPostRetries) _logsQueue.Enqueue(log); - _processLogs.Reset(); - if (_logsQueue.IsEmpty && !IsShuttingDown) _processLogs.WaitOne(); + _processLogs.WaitOne(); } } + } - private async Task Post(string queueName, string jsonString) + private async Task Post(string queueName, string jsonString) + { + if (queueName != QueuesConstants.LOGS && queueName != QueuesConstants.PERFORMANCES) { - if (queueName != Queues.LOGS && queueName != Queues.PERFORMANCES) return false; - try + return false; + } + + try + { + string requestUri = $"/api/ingestion/{AppId}/{queueName}"; + HttpResponseMessage result; + if (EnableCompression) { - using var client = new HttpClient(); - client.DefaultRequestHeaders.Add("User-Agent", "logflake-client-netcore/1.4.2"); - client.DefaultRequestHeaders.Add("Accept", "application/json"); - client.BaseAddress = new Uri($"{Server}"); - client.Timeout = TimeSpan.FromSeconds(PostTimeoutSeconds); - var requestUri = $"/api/ingestion/{AppId}/{queueName}"; - HttpResponseMessage result; - if (EnableCompression) - { - var jsonStringBytes = Encoding.UTF8.GetBytes(jsonString); - var base64String = Convert.ToBase64String(jsonStringBytes); - var compressed = Snappy.CompressToArray(Encoding.UTF8.GetBytes(base64String)); - var content = new ByteArrayContent(compressed); - content.Headers.Remove("Content-Type"); - content.Headers.Add("Content-Type", "application/octet-stream"); - result = await client.PostAsync(requestUri, content); - } - else - { - var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); - result = await client.PostAsync(requestUri, content); - } - return result.IsSuccessStatusCode; + byte[] jsonStringBytes = Encoding.UTF8.GetBytes(jsonString); + string base64String = Convert.ToBase64String(jsonStringBytes); + byte[] compressed = Snappy.CompressToArray(Encoding.UTF8.GetBytes(base64String)); + ByteArrayContent content = new(compressed); + content.Headers.Remove("Content-Type"); + content.Headers.Add("Content-Type", "application/octet-stream"); + result = await _httpClient.PostAsync(requestUri, content); } - catch (Exception) + else { - return false; + StringContent content = new(jsonString, Encoding.UTF8, "application/json"); + result = await _httpClient.PostAsync(requestUri, content); } + + return result.IsSuccessStatusCode; + } + catch (Exception) + { + return false; } + } - public void SendLog(string content, Dictionary parameters = null) => - SendLog(LogLevels.DEBUG, content, parameters); + public void SendLog(string content, Dictionary? parameters = null) => SendLog(LogLevels.DEBUG, content, parameters); - public void SendLog(LogLevels level, string content, Dictionary parameters = null) => - SendLog(level, null, content, parameters); + public void SendLog(LogLevels level, string content, Dictionary? parameters = null) => SendLog(level, null, content, parameters); - public void SendLog(LogLevels level, string correlation, string content, - Dictionary parameters = null) + public void SendLog(LogLevels level, string? correlation, string? content, Dictionary? parameters = null) + { + _logsQueue.Enqueue(new PendingLog { - _logsQueue.Enqueue(new PendingLog + QueueName = QueuesConstants.LOGS, + JsonString = new LogObject { - QueueName = Queues.LOGS, - JsonString = new LogObject - { - Level = level, - Hostname = GetHostname(), - Content = content, - Correlation = correlation, - Parameters = parameters - }.ToString() - }); - _processLogs.Set(); - } + Level = level, + Hostname = GetHostname(), + Content = content!, + Correlation = correlation, + Parameters = parameters, + }.ToString() + }); + + _processLogs.Set(); + } - public void SendException(Exception e) => - SendException(e, null); + public void SendException(Exception e) => SendException(e, null); - public void SendException(Exception e, string correlation) + public void SendException(Exception e, string? correlation) + { + StringBuilder additionalTrace = new(); + if (e.Data.Count > 0) { - var additionalTrace = new StringBuilder(); - if (e.Data.Count > 0) - { - additionalTrace.Append($"{Environment.NewLine}Data:"); - additionalTrace.Append( - $"{Environment.NewLine}{JsonSerializer.Serialize(e.Data, new JsonSerializerOptions { WriteIndented = true })}"); - } + additionalTrace.Append($"{Environment.NewLine}Data:"); + additionalTrace.Append($"{Environment.NewLine}{JsonSerializer.Serialize(e.Data, new JsonSerializerOptions { WriteIndented = true })}"); + } - _logsQueue.Enqueue(new PendingLog + _logsQueue.Enqueue(new PendingLog + { + QueueName = QueuesConstants.LOGS, + JsonString = new LogObject { - QueueName = Queues.LOGS, - JsonString = new LogObject - { - Level = LogLevels.EXCEPTION, - Hostname = GetHostname(), - Content = $"{e.Demystify()}{additionalTrace}", - Correlation = correlation - }.ToString() - }); - _processLogs.Set(); - } + Level = LogLevels.EXCEPTION, + Hostname = GetHostname(), + Content = $"{e.Demystify()}{additionalTrace}", + Correlation = correlation, + }.ToString() + }); + + _processLogs.Set(); + } - public void SendPerformance(string label, long duration) + public void SendPerformance(string label, long duration) + { + _logsQueue.Enqueue(new PendingLog { - _logsQueue.Enqueue(new PendingLog + QueueName = QueuesConstants.PERFORMANCES, + JsonString = new LogObject { - QueueName = Queues.PERFORMANCES, - JsonString = new LogObject - { - Label = label, - Duration = duration - }.ToString() - }); - _processLogs.Set(); - } + Label = label, + Duration = duration, + }.ToString() + }); - public PerformanceCounter MeasurePerformance(string label) => new PerformanceCounter(this, label); + _processLogs.Set(); } -} \ No newline at end of file + + public IPerformanceCounter MeasurePerformance(string label) => new PerformanceCounter(this, label); +} From 4672019c7547b203f137fa61b649a15b12ae3bf0 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 12:49:57 +0200 Subject: [PATCH 05/19] :recycle: Create Interface for PerformanceCount class --- IPerformanceCounter.cs | 12 +++++++++ PerformanceCounter.cs | 59 +++++++++++++++++++++++------------------- 2 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 IPerformanceCounter.cs diff --git a/IPerformanceCounter.cs b/IPerformanceCounter.cs new file mode 100644 index 0000000..11ea498 --- /dev/null +++ b/IPerformanceCounter.cs @@ -0,0 +1,12 @@ +namespace NLogFlake; + +public interface IPerformanceCounter +{ + void Start(); + + void Restart(); + + long Stop(); + + long Pause(); +} diff --git a/PerformanceCounter.cs b/PerformanceCounter.cs index 54429fa..47bcc38 100644 --- a/PerformanceCounter.cs +++ b/PerformanceCounter.cs @@ -1,39 +1,46 @@ using System.Diagnostics; -namespace NLogFlake +namespace NLogFlake; + +internal class PerformanceCounter : IPerformanceCounter { - public class PerformanceCounter + private readonly LogFlake _instance; + private readonly string _label; + private readonly Stopwatch _internalStopwatch; + + private bool AlreadySent { get; set; } + + internal PerformanceCounter(LogFlake instance, string label) { - private readonly LogFlake _instance; - private readonly string _label; - private readonly Stopwatch _internalSw; + _instance = instance; + _label = label; + _internalStopwatch = Stopwatch.StartNew(); + } - private bool AlreadySent { get; set; } + ~PerformanceCounter() + { + if (!AlreadySent) Stop(); + } - public PerformanceCounter(LogFlake instance, string label) - { - _instance = instance; - _label = label; - _internalSw = Stopwatch.StartNew(); - } + public void Start() => _internalStopwatch.Start(); - ~PerformanceCounter() - { - if (!AlreadySent) Stop(); - } + public void Restart() => _internalStopwatch.Restart(); + + public long Stop() => Stop(true); - public void Start() => _internalSw.Start(); - public void Restart() => _internalSw.Restart(); - public long Stop() => Stop(true); - public long Pause() => Stop(false); + public long Pause() => Stop(false); - private long Stop(bool shouldSend) + private long Stop(bool shouldSend) + { + _internalStopwatch.Stop(); + if (!shouldSend) { - _internalSw.Stop(); - if (!shouldSend) return _internalSw.ElapsedMilliseconds; - AlreadySent = true; - _instance.SendPerformance(_label, _internalSw.ElapsedMilliseconds); - return _internalSw.ElapsedMilliseconds; + return _internalStopwatch.ElapsedMilliseconds; } + + AlreadySent = true; + _instance.SendPerformance(_label, _internalStopwatch.ElapsedMilliseconds); + + return _internalStopwatch.ElapsedMilliseconds; } } From 5abcc2aa86e07273ec37039e0a8e1718732c5589 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 12:52:47 +0200 Subject: [PATCH 06/19] :recycle: Use file-scoped namespace --- LogFlakeException.cs | 21 +++++++-------- Models/LogObject.cs | 64 ++++++++++++++++++++------------------------ Models/PendingLog.cs | 15 ++++++----- 3 files changed, 47 insertions(+), 53 deletions(-) diff --git a/LogFlakeException.cs b/LogFlakeException.cs index f9f8be5..1d0c682 100644 --- a/LogFlakeException.cs +++ b/LogFlakeException.cs @@ -1,14 +1,13 @@ -using System; -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace NLogFlake +namespace NLogFlake; + +[Serializable] +public class LogFlakeException : ApplicationException { - [Serializable] - public class LogFlakeException : ApplicationException - { - public LogFlakeException() { } - public LogFlakeException(string message) : base(message) { } - public LogFlakeException(string message, Exception innerException) : base(message, innerException) { } - protected LogFlakeException(SerializationInfo info, StreamingContext context) : base(info, context) { } - } + public LogFlakeException() { } + + public LogFlakeException(string message) : base(message) { } + + public LogFlakeException(string message, Exception innerException) : base(message, innerException) { } } diff --git a/Models/LogObject.cs b/Models/LogObject.cs index 47341bb..4bd56cb 100644 --- a/Models/LogObject.cs +++ b/Models/LogObject.cs @@ -1,39 +1,33 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; -namespace NLogFlake.Models +namespace NLogFlake.Models; + +internal class LogObject { - internal class LogObject - { - [JsonPropertyName("datetime")] - public DateTime Date = DateTime.UtcNow; - - [JsonPropertyName("hostname")] - public string Hostname { get; set; } - - [JsonPropertyName("level")] - public LogLevels Level { get; set; } - - [JsonPropertyName("content")] - public string Content { get; set; } - - [JsonPropertyName("correlation")] - public string Correlation { get; set; } - - [JsonPropertyName("params")] - public Dictionary Parameters { get; set; } - - [JsonPropertyName("label")] - public string Label { get; set; } - - [JsonPropertyName("duration")] - public long Duration { get; set; } - - public override string ToString() - { - return JsonSerializer.Serialize(this); - } - } + [JsonPropertyName("datetime")] + public DateTime Date = DateTime.UtcNow; + + [JsonPropertyName("hostname")] + public string? Hostname { get; set; } + + [JsonPropertyName("level")] + public LogLevels Level { get; set; } + + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("correlation")] + public string? Correlation { get; set; } + + [JsonPropertyName("params")] + public Dictionary? Parameters { get; set; } + + [JsonPropertyName("label")] + public string? Label { get; set; } + + [JsonPropertyName("duration")] + public long Duration { get; set; } + + public override string ToString() => JsonSerializer.Serialize(this); } diff --git a/Models/PendingLog.cs b/Models/PendingLog.cs index 67872e9..657fba2 100644 --- a/Models/PendingLog.cs +++ b/Models/PendingLog.cs @@ -1,9 +1,10 @@ -namespace NLogFlake.Models +namespace NLogFlake.Models; + +internal class PendingLog { - internal class PendingLog - { - public int Retries { get; set; } - public string QueueName { get; set; } - public string JsonString { get; set; } - } + public int Retries { get; set; } + + public string? QueueName { get; set; } + + public string? JsonString { get; set; } } From ed981f13f780942c4ad55febd3a71e4c5d1fd5ea Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 16:47:24 +0200 Subject: [PATCH 07/19] :sparkles: Implement CorrelationService --- Services/CorrelationService.cs | 13 +++++++++++++ Services/ICorrelationService.cs | 6 ++++++ 2 files changed, 19 insertions(+) create mode 100644 Services/CorrelationService.cs create mode 100644 Services/ICorrelationService.cs diff --git a/Services/CorrelationService.cs b/Services/CorrelationService.cs new file mode 100644 index 0000000..11ced54 --- /dev/null +++ b/Services/CorrelationService.cs @@ -0,0 +1,13 @@ +namespace NLogFlake.Services; + +public class CorrelationService : ICorrelationService +{ + private readonly Guid _correlationId; + + public string Correlation => _correlationId.ToString(); + + public CorrelationService() + { + _correlationId = Guid.NewGuid(); + } +} diff --git a/Services/ICorrelationService.cs b/Services/ICorrelationService.cs new file mode 100644 index 0000000..170db1e --- /dev/null +++ b/Services/ICorrelationService.cs @@ -0,0 +1,6 @@ +namespace NLogFlake.Services; + +public interface ICorrelationService +{ + string Correlation { get; } +} From ea50c1f0ee096f6eb2ef981999b1fdda8d13c4ea Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 16:47:37 +0200 Subject: [PATCH 08/19] :sparkles: Create interface for VersionService --- Services/IVersionService.cs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Services/IVersionService.cs diff --git a/Services/IVersionService.cs b/Services/IVersionService.cs new file mode 100644 index 0000000..355b6fe --- /dev/null +++ b/Services/IVersionService.cs @@ -0,0 +1,6 @@ +namespace NLogFlake.Services; + +public interface IVersionService +{ + string Version { get; } +} From f7ea004adfbbf26dd6d39008d42d1a938399dc64 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 16:52:22 +0200 Subject: [PATCH 09/19] :sparkles: Include LogFlakeMiddleware --- Helpers/HttpContextHelper.cs | 91 +++++++++++++++++++++++++++++ Helpers/LogFlakeMiddlewareHelper.cs | 82 ++++++++++++++++++++++++++ Middlewares/LogFlakeMiddleware.cs | 84 ++++++++++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 Helpers/HttpContextHelper.cs create mode 100644 Helpers/LogFlakeMiddlewareHelper.cs create mode 100644 Middlewares/LogFlakeMiddleware.cs diff --git a/Helpers/HttpContextHelper.cs b/Helpers/HttpContextHelper.cs new file mode 100644 index 0000000..af3f173 --- /dev/null +++ b/Helpers/HttpContextHelper.cs @@ -0,0 +1,91 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using NLogFlake.Constants; + +namespace NLogFlake.Helpers; + +internal static class HttpContextHelper +{ + internal static async Task> GetLogParametersAsync(HttpContext httpContext, bool includeResponse) + { + string? request = await GetStringBody(httpContext.Request.Body); + Dictionary exceptionParams = new() + { + {"Request uri", new Uri(httpContext.Request.GetDisplayUrl())}, + {"Request method", httpContext.Request.Method}, + {"Request headers", httpContext.Request.Headers}, + }; + + if (!string.IsNullOrWhiteSpace(request)) + { + exceptionParams.Add("Request body", request); + } + + if (includeResponse) + { + exceptionParams.Add("Response headers", httpContext.Response.Headers); + exceptionParams.Add("Response status", httpContext.Response.StatusCode); + + string? response = await GetStringBody(httpContext.Response.Body); + if (!string.IsNullOrWhiteSpace(response)) + { + exceptionParams.Add("Response body", response); + } + } + + string? trace = httpContext.Items[HttpContextConstants.TraceContext]?.ToString(); + if (!string.IsNullOrWhiteSpace(trace)) + { + exceptionParams.Add("Trace", trace); + } + + return exceptionParams; + } + + internal static Claim? GetClaim(HttpContext httpContext, string claim) + { + if (string.IsNullOrWhiteSpace(claim)) + { + return null; + } + + return httpContext.User?.Claims?.FirstOrDefault(_ => _.Type.Trim().Equals(claim.Trim(), StringComparison.CurrentCultureIgnoreCase)); + } + + internal static string? GetClaimValue(HttpContext httpContext, string claimName) + { + Claim? claim = GetClaim(httpContext, claimName); + + if (claim is null) + { + return null; + } + + return claim.Value; + } + + internal static string? GetClientId(HttpContext httpContext) + { + string? clientId = GetClaimValue(httpContext, HttpContextConstants.ClientIdOther); + + if (string.IsNullOrWhiteSpace(clientId)) + { + clientId = GetClaimValue(httpContext, HttpContextConstants.ClientId); + } + + return clientId; + } + + internal static async Task GetStringBody(Stream body) + { + using StreamReader bodyStream = new(body); + bodyStream.BaseStream.Seek(0, SeekOrigin.Begin); + + string stringContent = await bodyStream.ReadToEndAsync(); + + body.Position = 0; + + return stringContent; + } +} diff --git a/Helpers/LogFlakeMiddlewareHelper.cs b/Helpers/LogFlakeMiddlewareHelper.cs new file mode 100644 index 0000000..74a9e7c --- /dev/null +++ b/Helpers/LogFlakeMiddlewareHelper.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Http; +using NLogFlake.Services; + +namespace NLogFlake.Helpers; + +internal static class LogFlakeMiddlewareHelper +{ + internal static string GetLogMessage(string fullPath, string? client, HttpResponse response, IPerformanceCounter? performance, string parentCorrelation) + { + StringBuilder logMessage = new($"Called {fullPath}"); + + if (!string.IsNullOrWhiteSpace(client)) + { + logMessage.Append($" by client {client}"); + } + + if (response is not null) + { + logMessage.Append($" with status code {response.StatusCode}"); + } + + if (performance is not null) + { + long time = performance.Stop(); + + logMessage.Append($" and execution time {time:N0} ms"); + } + + if (!string.IsNullOrWhiteSpace(parentCorrelation)) + { + logMessage.Append($" - parent-correlation: {parentCorrelation}"); + } + + return logMessage.ToString(); + } + + internal static string GetLogErrorMessage(string fullPath, string? client, HttpResponse response) + { + StringBuilder logMessage = new($"Error for method {fullPath}"); + + if (!string.IsNullOrWhiteSpace(client)) + { + logMessage.Append($" by client {client}"); + } + + logMessage.Append($" with status code {response.StatusCode} ({(HttpStatusCode)response.StatusCode})"); + + return logMessage.ToString(); + } + + internal static string GetLogExceptionMessage(string? client, string? exceptionMessage) + { + StringBuilder logMessage = new($"Exception with error:\n{exceptionMessage ?? string.Empty}"); + + if (!string.IsNullOrWhiteSpace(client)) + { + logMessage.Insert(0, $"Client {client}. "); + } + + return logMessage.ToString(); + } + + internal static void ValidateArguments(HttpContext httpContext, ILogFlakeService logFlakeService, ICorrelationService correlationService) + { + if (httpContext is null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (logFlakeService is null) + { + throw new ArgumentNullException(nameof(logFlakeService)); + } + + if (correlationService is null) + { + throw new ArgumentNullException(nameof(correlationService)); + } + } +} diff --git a/Middlewares/LogFlakeMiddleware.cs b/Middlewares/LogFlakeMiddleware.cs new file mode 100644 index 0000000..62ab4fc --- /dev/null +++ b/Middlewares/LogFlakeMiddleware.cs @@ -0,0 +1,84 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using NLogFlake.Constants; +using NLogFlake.Helpers; +using NLogFlake.Services; + +namespace NLogFlake.Middlewares; + +public class LogFlakeMiddleware +{ + private readonly RequestDelegate _next; + + public LogFlakeMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext, ILogFlakeService logFlakeService, ICorrelationService correlationService) + { + LogFlakeMiddlewareHelper.ValidateArguments(httpContext, logFlakeService, correlationService); + + httpContext.Request.EnableBuffering(); + + string fullPath = httpContext.Request.Path; + + Uri uri = new(fullPath); + bool ignoreLogProcessing = logFlakeService.Settings.ExcludedPaths.Contains(GetInitialPath(uri)); + + string correlation = correlationService.Correlation; + string parentCorrelation = httpContext.Request.Headers[HttpContextConstants.ParentCorrelationHeader].ToString(); + if (string.IsNullOrWhiteSpace(parentCorrelation)) + { + httpContext.Request.Headers[HttpContextConstants.ParentCorrelationHeader] = correlation; + } + + IPerformanceCounter? performance = logFlakeService.Settings.PerformanceMonitor && !ignoreLogProcessing ? logFlakeService.MeasurePerformance(fullPath) : null; + + await _next(httpContext); + + string? client = HttpContextHelper.GetClientId(httpContext); + + if (httpContext.Response.StatusCode >= StatusCodes.Status400BadRequest) + { + if (!ignoreLogProcessing) + { + string logMessage = LogFlakeMiddlewareHelper.GetLogErrorMessage(fullPath, client, httpContext.Response); + + logFlakeService.WriteLog(LogLevels.ERROR, logMessage.ToString(), correlation); + } + + if (httpContext.Response.ContentLength is null && httpContext.Items[HttpContextConstants.HasCatchedError] is null) + { + await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new + { + Error = new + { + ErrorCode = httpContext.Response.StatusCode, + ErrorMessage = ((HttpStatusCode)httpContext.Response.StatusCode).ToString() + }, + RequestStatus = "KO", + }), CancellationToken.None); + } + } + + if (logFlakeService.Settings.AutoLogRequest && !ignoreLogProcessing) + { + string logMessage = LogFlakeMiddlewareHelper.GetLogMessage(fullPath, client, httpContext.Response, performance, parentCorrelation); + + Dictionary content = await HttpContextHelper.GetLogParametersAsync(httpContext, logFlakeService.Settings.AutoLogResponse); + + logFlakeService.WriteLog(LogLevels.INFO, logMessage.ToString(), correlation, content); + } + } + + private static string GetInitialPath(Uri uri) + { + string initialPath = string.Join(string.Empty, uri.Segments.Take(3)); + + return uri.Segments.Length > 3 + ? initialPath[..^1] + : initialPath; + } +} From 3b71060e7dc4a268d2a7090d1f60e9703527d163 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 16:52:41 +0200 Subject: [PATCH 10/19] :wrench: Add .editorconfig --- .editorconfig | 243 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..98374a6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,243 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +end_of_line = lf +insert_final_newline = true +indent_size = 4 +indent_style = space +trim_trailing_whitespace = true + +[*.{json,yml,yaml}] +indent_size = 2 + +# C# files +[*.cs] + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = none + +# IDE0290: Use primary constructor +dotnet_diagnostic.IDE0290.severity = none + +# CS1591: Do not directly await a Task +dotnet_diagnostic.CA2007.severity = none + +# Review SQL queries for security vulnerabilities +dotnet_diagnostic.CA2100.severity = none + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = none + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:refactoring +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion + +# Code style extensions +csharp_style_namespace_declarations = file_scoped:warning +dotnet_diagnostic.SA1000.severity = warning # Keywords must be spaced correctly +dotnet_diagnostic.SA1001.severity = warning # Commas must be spaced correctly +dotnet_diagnostic.SA1005.severity = warning # Single line comments must begin with single space +dotnet_diagnostic.SA1012.severity = warning # Opening braces must be spaced correctly +dotnet_diagnostic.SA1013.severity = warning # Closing braces must be spaced correctly +dotnet_diagnostic.SA1015.severity = warning # Closing generic brackets must be spaced correctly +dotnet_diagnostic.SA1025.severity = warning # Code must not contain multiple whitespace in a row +dotnet_diagnostic.SA1026.severity = warning # Code must not contain space after new keyword in implicitly typed array allocation +dotnet_diagnostic.SA1110.severity = warning # Opening parenthesis must be on declaration line +dotnet_diagnostic.SA1115.severity = warning # Parameter must follow comma +dotnet_diagnostic.SA1116.severity = warning # Split parameters must start on line after declaration +dotnet_diagnostic.SA1117.severity = warning # Parameters must be on same line or separate lines +dotnet_diagnostic.SA1119.severity = warning # Statement must not use unnecessary parenthesis +dotnet_diagnostic.SA1121.severity = warning # Use built in type alias +dotnet_diagnostic.SA1122.severity = warning # Use `string.Empty` for empty strings +dotnet_diagnostic.SA1127.severity = warning # Generic type constraints must be on own line +dotnet_diagnostic.SA1128.severity = warning # Constructor initializer must be on own line +dotnet_diagnostic.SA1129.severity = warning # Do not use default value type constructor +dotnet_diagnostic.SA1137.severity = warning # Elements should have the same indentation +dotnet_diagnostic.SA1201.severity = warning # Elements must appear in the correct order +dotnet_diagnostic.SA1202.severity = warning # Elements must be ordered by access +dotnet_diagnostic.SA1203.severity = warning # Constants must appear before fields +dotnet_diagnostic.SA1204.severity = warning # Static elements must appear before instance elements +dotnet_diagnostic.SA1208.severity = warning # System using directives must be placed before other using directives +dotnet_diagnostic.SA1209.severity = warning # Using alias directives must be placed after other using directives +dotnet_diagnostic.SA1210.severity = warning # Using directives must be ordered alphabetically by namespace +dotnet_diagnostic.SA1211.severity = warning # Using alias directives must be ordered alphabetically by alias name +dotnet_diagnostic.SA1214.severity = warning # Readonly elements must appear before non readonly elements +dotnet_diagnostic.SA1216.severity = warning # Using static directives must be placed at the correct location +dotnet_diagnostic.SA1303.severity = warning # Const field names must begin with upper case letter +dotnet_diagnostic.SA1306.severity = warning # Field names must begin with lower case letter +dotnet_diagnostic.SA1308.severity = warning # Variable names must not be prefixed +dotnet_diagnostic.SA1312.severity = warning # Variable names must begin with lower case letter +dotnet_diagnostic.SA1313.severity = warning # Parameter names must begin with lower case letter +dotnet_diagnostic.SA1316.severity = warning # Tuple element names should use correct casing +dotnet_diagnostic.SA1400.severity = warning # Access modifier must be declared +dotnet_diagnostic.SA1408.severity = warning # Conditional expressions must declare precedence +dotnet_diagnostic.SA1413.severity = warning # Use trailing commas in multiline initializers +dotnet_diagnostic.SA1500.severity = warning # Braces for multi line statements must not share line +dotnet_diagnostic.SA1501.severity = warning # Statement must not be on single line +dotnet_diagnostic.SA1502.severity = warning # Element must not be on single line +dotnet_diagnostic.SA1503.severity = warning # Braces must not be omitted +dotnet_diagnostic.SA1505.severity = warning # Opening braces must not be followed by blank line +dotnet_diagnostic.SA1507.severity = warning # Code must not contain multiple blank lines in aRow +dotnet_diagnostic.SA1508.severity = warning # Closing braces must not be preceded by blank line +dotnet_diagnostic.SA1512.severity = warning # Single line comments must not be followed by blank line +dotnet_diagnostic.SA1513.severity = warning # Closing brace must be followed by blank line +dotnet_diagnostic.SA1515.severity = warning # Single line comment must be preceded by blank line +dotnet_diagnostic.SA1516.severity = warning # Elements must be separated by blank line +dotnet_diagnostic.SA1518.severity = warning # Use line endings correctly at end of file +dotnet_diagnostic.SA1649.severity = warning # File name must match type name + +# Code quality +dotnet_style_readonly_field = true:suggestion +dotnet_code_quality_unused_parameters = non_public:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:refactoring +dotnet_style_prefer_conditional_expression_over_return = true:refactoring +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:refactoring +csharp_style_expression_bodied_constructors = true:refactoring +csharp_style_expression_bodied_operators = true:refactoring +csharp_style_expression_bodied_properties = true:refactoring +csharp_style_expression_bodied_indexers = true:refactoring +csharp_style_expression_bodied_accessors = true:refactoring +csharp_style_expression_bodied_lambdas = true:refactoring +csharp_style_expression_bodied_local_functions = true:refactoring + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Analyzers +dotnet_code_quality.ca1802.api_surface = private, internal + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# Shell scripts +[*.{cmd, bat}] +end_of_line = crlf From ab645c21d583dbd9a9667700320a9ebbb14e1576 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 16:54:26 +0200 Subject: [PATCH 11/19] :technologist: Include extensions method for easy configuration --- IApplicationBuilderExtensions.cs | 58 ++++++++++++++++++++++++++++++++ IServicesExtensions.cs | 27 +++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 IApplicationBuilderExtensions.cs create mode 100644 IServicesExtensions.cs diff --git a/IApplicationBuilderExtensions.cs b/IApplicationBuilderExtensions.cs new file mode 100644 index 0000000..b146e9f --- /dev/null +++ b/IApplicationBuilderExtensions.cs @@ -0,0 +1,58 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NLogFlake.Constants; +using NLogFlake.Helpers; +using NLogFlake.Services; + +namespace NLogFlake; + +public static class IApplicationBuilderExtensions +{ + public static void ConfigureLogFlakeExceptionHandler(this IApplicationBuilder app, IConfiguration configuration) + { + app.UseExceptionHandler(applicationBuilder => applicationBuilder.Run(async httpContext => await ConfigureLogFlakeExceptionHandler(httpContext))); + } + + private static async Task ConfigureLogFlakeExceptionHandler(HttpContext httpContext) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + httpContext.Response.ContentType = "application/json"; + + IExceptionHandlerFeature contextFeature = httpContext.Features.Get(); + if (contextFeature is not null) + { + ILogFlakeService logFlakeService = httpContext.RequestServices.GetRequiredService(); + + if (logFlakeService.Settings.AutoLogGlobalExceptions && contextFeature.Error is not OperationCanceledException) + { + ICorrelationService correlationService = httpContext.RequestServices.GetRequiredService(); + string correlation = correlationService.Correlation; + + string? client = HttpContextHelper.GetClientId(httpContext); + + string logMessage = LogFlakeMiddlewareHelper.GetLogExceptionMessage(client, correlation); + + Dictionary parameters = await HttpContextHelper.GetLogParametersAsync(httpContext, false); + logFlakeService.WriteLog(LogLevels.ERROR, correlation, logMessage, parameters); + logFlakeService.WriteException(contextFeature.Error!, correlation); + } + + httpContext.Items[HttpContextConstants.HasCatchedError] = new(); + + await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new + { + Error = new + { + ErrorCode = (int)HttpStatusCode.InternalServerError, + ErrorMessage = contextFeature.Error?.Message, + }, + RequestStatus = "KO", + }), CancellationToken.None); + } + } +} diff --git a/IServicesExtensions.cs b/IServicesExtensions.cs new file mode 100644 index 0000000..de55bf7 --- /dev/null +++ b/IServicesExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NLogFlake.Models.Options; +using NLogFlake.Services; + +namespace NLogFlake; + +public static class IServicesExtensions +{ + public static IServiceCollection AddLogFlake(this IServiceCollection services, IConfiguration configuration) + { + _ = services.Configure(configuration.GetSection(LogFlakeOptions.SectionName)) + .AddOptionsWithValidateOnStart(); + + _ = services.Configure(configuration.GetSection(LogFlakeSettingsOptions.SectionName)) + .AddOptionsWithValidateOnStart(); + + services.AddHttpClient(); + + services.AddScoped(); + + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} From 7dd521e4ac8937621e4e0fa2cf79b4f07aed6ae2 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 16:54:51 +0200 Subject: [PATCH 12/19] :heavy_plus_sign: Add required packages --- LogFlake.csproj | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/LogFlake.csproj b/LogFlake.csproj index 6f07393..69f868f 100644 --- a/LogFlake.csproj +++ b/LogFlake.csproj @@ -12,6 +12,11 @@ README.md LogFlake;CloudPhoenix;Cloud Phoenix;Log Flake;Log;Logs LogFlake Client .net Core + false + enable + enable + 12.0 + NLogFlake @@ -20,6 +25,15 @@ + + + + + + + + + From 5b2dd688f62fbdb063f7eb6db7087c07918d2666 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 17:00:36 +0200 Subject: [PATCH 13/19] :bookmark: Bump version to 1.5.0 --- LogFlake.cs | 2 +- LogFlake.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LogFlake.cs b/LogFlake.cs index b3fa80e..10f6bb3 100644 --- a/LogFlake.cs +++ b/LogFlake.cs @@ -48,7 +48,7 @@ public LogFlake(IOptions logFlakeOptions, IHttpClientFactory ht Timeout = TimeSpan.FromSeconds(PostTimeoutSeconds), }; _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); - _httpClient.DefaultRequestHeaders.Add("User-Agent", "logflake-client-netcore/1.4.2"); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "logflake-client-netcore/1.5.0"); } ~LogFlake() => Shutdown(); diff --git a/LogFlake.csproj b/LogFlake.csproj index 69f868f..887df20 100644 --- a/LogFlake.csproj +++ b/LogFlake.csproj @@ -2,7 +2,7 @@ LogFlake.Client.NetCore - 1.4.2 + 1.5.0 CloudPhoenix Srl CloudPhoenix Srl netstandard2.1 From e68ccf0d31ea67e25757464ae8f085fdb75bec0b Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 17:10:05 +0200 Subject: [PATCH 14/19] :green_heart: Adopt DeepSource suggestions --- Helpers/HttpContextHelper.cs | 6 +++--- IApplicationBuilderExtensions.cs | 4 ++-- LogFlakeException.cs | 6 ++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Helpers/HttpContextHelper.cs b/Helpers/HttpContextHelper.cs index af3f173..a2e6cb2 100644 --- a/Helpers/HttpContextHelper.cs +++ b/Helpers/HttpContextHelper.cs @@ -9,7 +9,7 @@ internal static class HttpContextHelper { internal static async Task> GetLogParametersAsync(HttpContext httpContext, bool includeResponse) { - string? request = await GetStringBody(httpContext.Request.Body); + string? request = await GetStringBodyAsync(httpContext.Request.Body); Dictionary exceptionParams = new() { {"Request uri", new Uri(httpContext.Request.GetDisplayUrl())}, @@ -27,7 +27,7 @@ internal static async Task> GetLogParametersAsync(Htt exceptionParams.Add("Response headers", httpContext.Response.Headers); exceptionParams.Add("Response status", httpContext.Response.StatusCode); - string? response = await GetStringBody(httpContext.Response.Body); + string? response = await GetStringBodyAsync(httpContext.Response.Body); if (!string.IsNullOrWhiteSpace(response)) { exceptionParams.Add("Response body", response); @@ -77,7 +77,7 @@ internal static async Task> GetLogParametersAsync(Htt return clientId; } - internal static async Task GetStringBody(Stream body) + internal static async Task GetStringBodyAsync(Stream body) { using StreamReader bodyStream = new(body); bodyStream.BaseStream.Seek(0, SeekOrigin.Begin); diff --git a/IApplicationBuilderExtensions.cs b/IApplicationBuilderExtensions.cs index b146e9f..9ebc3ce 100644 --- a/IApplicationBuilderExtensions.cs +++ b/IApplicationBuilderExtensions.cs @@ -15,10 +15,10 @@ public static class IApplicationBuilderExtensions { public static void ConfigureLogFlakeExceptionHandler(this IApplicationBuilder app, IConfiguration configuration) { - app.UseExceptionHandler(applicationBuilder => applicationBuilder.Run(async httpContext => await ConfigureLogFlakeExceptionHandler(httpContext))); + app.UseExceptionHandler(applicationBuilder => applicationBuilder.Run(async httpContext => await ConfigureLogFlakeExceptionHandlerAsync(httpContext))); } - private static async Task ConfigureLogFlakeExceptionHandler(HttpContext httpContext) + private static async Task ConfigureLogFlakeExceptionHandlerAsync(HttpContext httpContext) { httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; httpContext.Response.ContentType = "application/json"; diff --git a/LogFlakeException.cs b/LogFlakeException.cs index 1d0c682..c85e412 100644 --- a/LogFlakeException.cs +++ b/LogFlakeException.cs @@ -1,9 +1,7 @@ -using System.Runtime.Serialization; - -namespace NLogFlake; +namespace NLogFlake; [Serializable] -public class LogFlakeException : ApplicationException +public class LogFlakeException : Exception { public LogFlakeException() { } From e058c8b006e3c0ecfdc16155299d2fcef881ef51 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 17:17:20 +0200 Subject: [PATCH 15/19] :bug: Register Middleware --- IApplicationBuilderExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IApplicationBuilderExtensions.cs b/IApplicationBuilderExtensions.cs index 9ebc3ce..e5f7951 100644 --- a/IApplicationBuilderExtensions.cs +++ b/IApplicationBuilderExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using NLogFlake.Constants; using NLogFlake.Helpers; +using NLogFlake.Middlewares; using NLogFlake.Services; namespace NLogFlake; @@ -15,6 +16,8 @@ public static class IApplicationBuilderExtensions { public static void ConfigureLogFlakeExceptionHandler(this IApplicationBuilder app, IConfiguration configuration) { + app.UseMiddleware(); + app.UseExceptionHandler(applicationBuilder => applicationBuilder.Run(async httpContext => await ConfigureLogFlakeExceptionHandlerAsync(httpContext))); } From 8bf251ead8cf66636684b5b640d3bb98db947030 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Mon, 3 Jun 2024 17:17:38 +0200 Subject: [PATCH 16/19] :recycle: Rename Middleware method to follow standard naming --- Middlewares/LogFlakeMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Middlewares/LogFlakeMiddleware.cs b/Middlewares/LogFlakeMiddleware.cs index 62ab4fc..fb97fa0 100644 --- a/Middlewares/LogFlakeMiddleware.cs +++ b/Middlewares/LogFlakeMiddleware.cs @@ -16,7 +16,7 @@ public LogFlakeMiddleware(RequestDelegate next) _next = next; } - public async Task Invoke(HttpContext httpContext, ILogFlakeService logFlakeService, ICorrelationService correlationService) + public async Task InvokeAsync(HttpContext httpContext, ILogFlakeService logFlakeService, ICorrelationService correlationService) { LogFlakeMiddlewareHelper.ValidateArguments(httpContext, logFlakeService, correlationService); From aafb65934a4cdf54539273b23dfc7cb44c115564 Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Wed, 5 Jun 2024 12:03:13 +0200 Subject: [PATCH 17/19] :fire: Remove ASP.NET related things --- Constants/HttpClientConstants.cs | 7 ++ Constants/HttpContextConstants.cs | 16 ---- Helpers/HttpContextHelper.cs | 91 ------------------- Helpers/LogFlakeMiddlewareHelper.cs | 82 ----------------- IApplicationBuilderExtensions.cs | 61 ------------- ...ions.cs => IServiceCollectionExtensions.cs | 17 ++-- LogFlake.cs | 19 ++-- LogFlake.csproj | 9 +- Middlewares/LogFlakeMiddleware.cs | 84 ----------------- Models/LogFlakeSettings.cs | 16 ---- Models/Options/LogFlakeOptions.cs | 2 +- .../LogFlakeSettingsOptions.Validator.cs | 6 -- Models/Options/LogFlakeSettingsOptions.cs | 25 ----- Services/ILogFlakeService.cs | 6 +- Services/LogFlakeService.cs | 22 +---- 15 files changed, 32 insertions(+), 431 deletions(-) create mode 100644 Constants/HttpClientConstants.cs delete mode 100644 Constants/HttpContextConstants.cs delete mode 100644 Helpers/HttpContextHelper.cs delete mode 100644 Helpers/LogFlakeMiddlewareHelper.cs delete mode 100644 IApplicationBuilderExtensions.cs rename IServicesExtensions.cs => IServiceCollectionExtensions.cs (52%) delete mode 100644 Middlewares/LogFlakeMiddleware.cs delete mode 100644 Models/LogFlakeSettings.cs delete mode 100644 Models/Options/LogFlakeSettingsOptions.Validator.cs delete mode 100644 Models/Options/LogFlakeSettingsOptions.cs diff --git a/Constants/HttpClientConstants.cs b/Constants/HttpClientConstants.cs new file mode 100644 index 0000000..0c1b2a4 --- /dev/null +++ b/Constants/HttpClientConstants.cs @@ -0,0 +1,7 @@ +namespace NLogFlake.Constants; + +internal static class HttpClientConstants +{ + internal const int PostTimeoutSeconds = 3; + internal const string ClientName = "LogFlakeClient"; +} diff --git a/Constants/HttpContextConstants.cs b/Constants/HttpContextConstants.cs deleted file mode 100644 index 49e79d0..0000000 --- a/Constants/HttpContextConstants.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace NLogFlake.Constants; - -internal static class HttpContextConstants -{ - internal const string ParentCorrelationHeader = "parent-correlation"; - - internal const string AutoLogGlobalExceptions = "AUTOLOG_GLOBAL_EXCEPTIONS"; - - internal const string TraceContext = "TRACE_CONTEXT"; - - internal const string ClientId = "clientId"; - - internal const string ClientIdOther = "azp"; - - internal const string HasCatchedError = "HasCatchedError"; -} diff --git a/Helpers/HttpContextHelper.cs b/Helpers/HttpContextHelper.cs deleted file mode 100644 index a2e6cb2..0000000 --- a/Helpers/HttpContextHelper.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using NLogFlake.Constants; - -namespace NLogFlake.Helpers; - -internal static class HttpContextHelper -{ - internal static async Task> GetLogParametersAsync(HttpContext httpContext, bool includeResponse) - { - string? request = await GetStringBodyAsync(httpContext.Request.Body); - Dictionary exceptionParams = new() - { - {"Request uri", new Uri(httpContext.Request.GetDisplayUrl())}, - {"Request method", httpContext.Request.Method}, - {"Request headers", httpContext.Request.Headers}, - }; - - if (!string.IsNullOrWhiteSpace(request)) - { - exceptionParams.Add("Request body", request); - } - - if (includeResponse) - { - exceptionParams.Add("Response headers", httpContext.Response.Headers); - exceptionParams.Add("Response status", httpContext.Response.StatusCode); - - string? response = await GetStringBodyAsync(httpContext.Response.Body); - if (!string.IsNullOrWhiteSpace(response)) - { - exceptionParams.Add("Response body", response); - } - } - - string? trace = httpContext.Items[HttpContextConstants.TraceContext]?.ToString(); - if (!string.IsNullOrWhiteSpace(trace)) - { - exceptionParams.Add("Trace", trace); - } - - return exceptionParams; - } - - internal static Claim? GetClaim(HttpContext httpContext, string claim) - { - if (string.IsNullOrWhiteSpace(claim)) - { - return null; - } - - return httpContext.User?.Claims?.FirstOrDefault(_ => _.Type.Trim().Equals(claim.Trim(), StringComparison.CurrentCultureIgnoreCase)); - } - - internal static string? GetClaimValue(HttpContext httpContext, string claimName) - { - Claim? claim = GetClaim(httpContext, claimName); - - if (claim is null) - { - return null; - } - - return claim.Value; - } - - internal static string? GetClientId(HttpContext httpContext) - { - string? clientId = GetClaimValue(httpContext, HttpContextConstants.ClientIdOther); - - if (string.IsNullOrWhiteSpace(clientId)) - { - clientId = GetClaimValue(httpContext, HttpContextConstants.ClientId); - } - - return clientId; - } - - internal static async Task GetStringBodyAsync(Stream body) - { - using StreamReader bodyStream = new(body); - bodyStream.BaseStream.Seek(0, SeekOrigin.Begin); - - string stringContent = await bodyStream.ReadToEndAsync(); - - body.Position = 0; - - return stringContent; - } -} diff --git a/Helpers/LogFlakeMiddlewareHelper.cs b/Helpers/LogFlakeMiddlewareHelper.cs deleted file mode 100644 index 74a9e7c..0000000 --- a/Helpers/LogFlakeMiddlewareHelper.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Net; -using System.Text; -using Microsoft.AspNetCore.Http; -using NLogFlake.Services; - -namespace NLogFlake.Helpers; - -internal static class LogFlakeMiddlewareHelper -{ - internal static string GetLogMessage(string fullPath, string? client, HttpResponse response, IPerformanceCounter? performance, string parentCorrelation) - { - StringBuilder logMessage = new($"Called {fullPath}"); - - if (!string.IsNullOrWhiteSpace(client)) - { - logMessage.Append($" by client {client}"); - } - - if (response is not null) - { - logMessage.Append($" with status code {response.StatusCode}"); - } - - if (performance is not null) - { - long time = performance.Stop(); - - logMessage.Append($" and execution time {time:N0} ms"); - } - - if (!string.IsNullOrWhiteSpace(parentCorrelation)) - { - logMessage.Append($" - parent-correlation: {parentCorrelation}"); - } - - return logMessage.ToString(); - } - - internal static string GetLogErrorMessage(string fullPath, string? client, HttpResponse response) - { - StringBuilder logMessage = new($"Error for method {fullPath}"); - - if (!string.IsNullOrWhiteSpace(client)) - { - logMessage.Append($" by client {client}"); - } - - logMessage.Append($" with status code {response.StatusCode} ({(HttpStatusCode)response.StatusCode})"); - - return logMessage.ToString(); - } - - internal static string GetLogExceptionMessage(string? client, string? exceptionMessage) - { - StringBuilder logMessage = new($"Exception with error:\n{exceptionMessage ?? string.Empty}"); - - if (!string.IsNullOrWhiteSpace(client)) - { - logMessage.Insert(0, $"Client {client}. "); - } - - return logMessage.ToString(); - } - - internal static void ValidateArguments(HttpContext httpContext, ILogFlakeService logFlakeService, ICorrelationService correlationService) - { - if (httpContext is null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (logFlakeService is null) - { - throw new ArgumentNullException(nameof(logFlakeService)); - } - - if (correlationService is null) - { - throw new ArgumentNullException(nameof(correlationService)); - } - } -} diff --git a/IApplicationBuilderExtensions.cs b/IApplicationBuilderExtensions.cs deleted file mode 100644 index e5f7951..0000000 --- a/IApplicationBuilderExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Net; -using System.Text.Json; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NLogFlake.Constants; -using NLogFlake.Helpers; -using NLogFlake.Middlewares; -using NLogFlake.Services; - -namespace NLogFlake; - -public static class IApplicationBuilderExtensions -{ - public static void ConfigureLogFlakeExceptionHandler(this IApplicationBuilder app, IConfiguration configuration) - { - app.UseMiddleware(); - - app.UseExceptionHandler(applicationBuilder => applicationBuilder.Run(async httpContext => await ConfigureLogFlakeExceptionHandlerAsync(httpContext))); - } - - private static async Task ConfigureLogFlakeExceptionHandlerAsync(HttpContext httpContext) - { - httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - httpContext.Response.ContentType = "application/json"; - - IExceptionHandlerFeature contextFeature = httpContext.Features.Get(); - if (contextFeature is not null) - { - ILogFlakeService logFlakeService = httpContext.RequestServices.GetRequiredService(); - - if (logFlakeService.Settings.AutoLogGlobalExceptions && contextFeature.Error is not OperationCanceledException) - { - ICorrelationService correlationService = httpContext.RequestServices.GetRequiredService(); - string correlation = correlationService.Correlation; - - string? client = HttpContextHelper.GetClientId(httpContext); - - string logMessage = LogFlakeMiddlewareHelper.GetLogExceptionMessage(client, correlation); - - Dictionary parameters = await HttpContextHelper.GetLogParametersAsync(httpContext, false); - logFlakeService.WriteLog(LogLevels.ERROR, correlation, logMessage, parameters); - logFlakeService.WriteException(contextFeature.Error!, correlation); - } - - httpContext.Items[HttpContextConstants.HasCatchedError] = new(); - - await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new - { - Error = new - { - ErrorCode = (int)HttpStatusCode.InternalServerError, - ErrorMessage = contextFeature.Error?.Message, - }, - RequestStatus = "KO", - }), CancellationToken.None); - } - } -} diff --git a/IServicesExtensions.cs b/IServiceCollectionExtensions.cs similarity index 52% rename from IServicesExtensions.cs rename to IServiceCollectionExtensions.cs index de55bf7..b91e3b6 100644 --- a/IServicesExtensions.cs +++ b/IServiceCollectionExtensions.cs @@ -1,21 +1,19 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using NLogFlake.Constants; using NLogFlake.Models.Options; using NLogFlake.Services; namespace NLogFlake; -public static class IServicesExtensions +public static class IServiceCollectionExtensions { public static IServiceCollection AddLogFlake(this IServiceCollection services, IConfiguration configuration) { _ = services.Configure(configuration.GetSection(LogFlakeOptions.SectionName)) - .AddOptionsWithValidateOnStart(); + .AddOptionsWithValidateOnStart(); - _ = services.Configure(configuration.GetSection(LogFlakeSettingsOptions.SectionName)) - .AddOptionsWithValidateOnStart(); - - services.AddHttpClient(); + services.AddHttpClient(HttpClientConstants.ClientName, ConfigureClient); services.AddScoped(); @@ -24,4 +22,11 @@ public static IServiceCollection AddLogFlake(this IServiceCollection services, I return services; } + + private static void ConfigureClient(HttpClient client) + { + client.Timeout = TimeSpan.FromSeconds(HttpClientConstants.PostTimeoutSeconds); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + client.DefaultRequestHeaders.Add("User-Agent", "logflake-client-netcore/1.5.0"); + } } diff --git a/LogFlake.cs b/LogFlake.cs index 10f6bb3..0115dfb 100644 --- a/LogFlake.cs +++ b/LogFlake.cs @@ -18,13 +18,12 @@ internal class LogFlake : ILogFlake private readonly ConcurrentQueue _logsQueue = new(); private readonly ManualResetEvent _processLogs = new(false); - private readonly HttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private Thread LogsProcessorThread { get; set; } private bool IsShuttingDown { get; set; } internal int FailedPostRetries { get; set; } = 3; - internal int PostTimeoutSeconds { get; set; } = 3; internal bool EnableCompression { get; set; } = true; internal void SetHostname() => SetHostname(null); @@ -37,18 +36,12 @@ public LogFlake(IOptions logFlakeOptions, IHttpClientFactory ht { AppId = logFlakeOptions.Value.AppId!; - Server = logFlakeOptions.Value.Endpoint ?? new Uri(ServersConstants.PRODUCTION); + Server = new Uri(logFlakeOptions.Value.Endpoint ?? ServersConstants.PRODUCTION); LogsProcessorThread = new Thread(LogsProcessor); LogsProcessorThread.Start(); - _httpClient = new HttpClient - { - BaseAddress = Server, - Timeout = TimeSpan.FromSeconds(PostTimeoutSeconds), - }; - _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); - _httpClient.DefaultRequestHeaders.Add("User-Agent", "logflake-client-netcore/1.5.0"); + _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); } ~LogFlake() => Shutdown(); @@ -95,6 +88,8 @@ private async Task Post(string queueName, string jsonString) { string requestUri = $"/api/ingestion/{AppId}/{queueName}"; HttpResponseMessage result; + using HttpClient httpClient = _httpClientFactory.CreateClient(HttpClientConstants.ClientName); + httpClient.BaseAddress = Server; if (EnableCompression) { byte[] jsonStringBytes = Encoding.UTF8.GetBytes(jsonString); @@ -103,12 +98,12 @@ private async Task Post(string queueName, string jsonString) ByteArrayContent content = new(compressed); content.Headers.Remove("Content-Type"); content.Headers.Add("Content-Type", "application/octet-stream"); - result = await _httpClient.PostAsync(requestUri, content); + result = await httpClient.PostAsync(requestUri, content); } else { StringContent content = new(jsonString, Encoding.UTF8, "application/json"); - result = await _httpClient.PostAsync(requestUri, content); + result = await httpClient.PostAsync(requestUri, content); } return result.IsSuccessStatusCode; diff --git a/LogFlake.csproj b/LogFlake.csproj index 887df20..4fab0d6 100644 --- a/LogFlake.csproj +++ b/LogFlake.csproj @@ -11,8 +11,8 @@ https://github.com/CloudPhoenix/logflake-client-netcore README.md LogFlake;CloudPhoenix;Cloud Phoenix;Log Flake;Log;Logs - LogFlake Client .net Core - false + LogFlake Client .NET Core + false enable enable 12.0 @@ -25,14 +25,9 @@ - - - - - diff --git a/Middlewares/LogFlakeMiddleware.cs b/Middlewares/LogFlakeMiddleware.cs deleted file mode 100644 index fb97fa0..0000000 --- a/Middlewares/LogFlakeMiddleware.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Net; -using System.Text.Json; -using Microsoft.AspNetCore.Http; -using NLogFlake.Constants; -using NLogFlake.Helpers; -using NLogFlake.Services; - -namespace NLogFlake.Middlewares; - -public class LogFlakeMiddleware -{ - private readonly RequestDelegate _next; - - public LogFlakeMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task InvokeAsync(HttpContext httpContext, ILogFlakeService logFlakeService, ICorrelationService correlationService) - { - LogFlakeMiddlewareHelper.ValidateArguments(httpContext, logFlakeService, correlationService); - - httpContext.Request.EnableBuffering(); - - string fullPath = httpContext.Request.Path; - - Uri uri = new(fullPath); - bool ignoreLogProcessing = logFlakeService.Settings.ExcludedPaths.Contains(GetInitialPath(uri)); - - string correlation = correlationService.Correlation; - string parentCorrelation = httpContext.Request.Headers[HttpContextConstants.ParentCorrelationHeader].ToString(); - if (string.IsNullOrWhiteSpace(parentCorrelation)) - { - httpContext.Request.Headers[HttpContextConstants.ParentCorrelationHeader] = correlation; - } - - IPerformanceCounter? performance = logFlakeService.Settings.PerformanceMonitor && !ignoreLogProcessing ? logFlakeService.MeasurePerformance(fullPath) : null; - - await _next(httpContext); - - string? client = HttpContextHelper.GetClientId(httpContext); - - if (httpContext.Response.StatusCode >= StatusCodes.Status400BadRequest) - { - if (!ignoreLogProcessing) - { - string logMessage = LogFlakeMiddlewareHelper.GetLogErrorMessage(fullPath, client, httpContext.Response); - - logFlakeService.WriteLog(LogLevels.ERROR, logMessage.ToString(), correlation); - } - - if (httpContext.Response.ContentLength is null && httpContext.Items[HttpContextConstants.HasCatchedError] is null) - { - await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new - { - Error = new - { - ErrorCode = httpContext.Response.StatusCode, - ErrorMessage = ((HttpStatusCode)httpContext.Response.StatusCode).ToString() - }, - RequestStatus = "KO", - }), CancellationToken.None); - } - } - - if (logFlakeService.Settings.AutoLogRequest && !ignoreLogProcessing) - { - string logMessage = LogFlakeMiddlewareHelper.GetLogMessage(fullPath, client, httpContext.Response, performance, parentCorrelation); - - Dictionary content = await HttpContextHelper.GetLogParametersAsync(httpContext, logFlakeService.Settings.AutoLogResponse); - - logFlakeService.WriteLog(LogLevels.INFO, logMessage.ToString(), correlation, content); - } - } - - private static string GetInitialPath(Uri uri) - { - string initialPath = string.Join(string.Empty, uri.Segments.Take(3)); - - return uri.Segments.Length > 3 - ? initialPath[..^1] - : initialPath; - } -} diff --git a/Models/LogFlakeSettings.cs b/Models/LogFlakeSettings.cs deleted file mode 100644 index 6377f86..0000000 --- a/Models/LogFlakeSettings.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace NLogFlake.Models; - -public sealed class LogFlakeSettings -{ - public bool AutoLogRequest { get; set; } - - public bool AutoLogResponse { get; set; } - - public bool AutoLogUnauthorized { get; set; } - - public bool AutoLogGlobalExceptions { get; set; } - - public bool PerformanceMonitor { get; set; } - - public IEnumerable? ExcludedPaths { get; set; } -} diff --git a/Models/Options/LogFlakeOptions.cs b/Models/Options/LogFlakeOptions.cs index 750cfb6..b3e2254 100644 --- a/Models/Options/LogFlakeOptions.cs +++ b/Models/Options/LogFlakeOptions.cs @@ -10,5 +10,5 @@ internal sealed class LogFlakeOptions public string? AppId { get; set; } [Url] - public Uri? Endpoint { get; set; } + public string? Endpoint { get; set; } } diff --git a/Models/Options/LogFlakeSettingsOptions.Validator.cs b/Models/Options/LogFlakeSettingsOptions.Validator.cs deleted file mode 100644 index ef6d4f7..0000000 --- a/Models/Options/LogFlakeSettingsOptions.Validator.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Microsoft.Extensions.Options; - -namespace NLogFlake.Models.Options; - -[OptionsValidator] -internal sealed partial class LogFlakeSettingsOptionsValidator : IValidateOptions; diff --git a/Models/Options/LogFlakeSettingsOptions.cs b/Models/Options/LogFlakeSettingsOptions.cs deleted file mode 100644 index 96a16f1..0000000 --- a/Models/Options/LogFlakeSettingsOptions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace NLogFlake.Models.Options; - -public sealed class LogFlakeSettingsOptions -{ - public const string SectionName = "LogFlakeSettings"; - - [Required] - public bool AutoLogRequest { get; set; } - - [Required] - public bool AutoLogGlobalExceptions { get; set; } - - [Required] - public bool AutoLogUnauthorized { get; set; } - - [Required] - public bool AutoLogResponse { get; set; } - - [Required] - public bool PerformanceMonitor { get; set; } - - public IEnumerable? ExcludedPaths { get; set; } -} diff --git a/Services/ILogFlakeService.cs b/Services/ILogFlakeService.cs index 4a2fd11..af5240e 100644 --- a/Services/ILogFlakeService.cs +++ b/Services/ILogFlakeService.cs @@ -1,12 +1,8 @@ -using NLogFlake.Models; - namespace NLogFlake.Services; public interface ILogFlakeService { - LogFlakeSettings Settings { get; } - - void WriteLog(LogLevels logLevels, string? message, string? correlation, Dictionary? parameters = null); + void WriteLog(LogLevels logLevel, string? message, string? correlation, Dictionary? parameters = null); void WriteException(Exception ex, string? correlation, string? message = null, Dictionary? parameters = null); diff --git a/Services/LogFlakeService.cs b/Services/LogFlakeService.cs index edbaead..0985be5 100644 --- a/Services/LogFlakeService.cs +++ b/Services/LogFlakeService.cs @@ -1,7 +1,3 @@ -using Microsoft.Extensions.Options; -using NLogFlake.Models; -using NLogFlake.Models.Options; - namespace NLogFlake.Services; public class LogFlakeService : ILogFlakeService @@ -10,30 +6,18 @@ public class LogFlakeService : ILogFlakeService private readonly string _version; - public LogFlakeSettings Settings { get; } - - public LogFlakeService(ILogFlake logFlake, IOptions logFlakeSettingsOptions, IVersionService versionService) + public LogFlakeService(ILogFlake logFlake, IVersionService versionService) { _logFlake = logFlake; - Settings = new LogFlakeSettings - { - AutoLogRequest = logFlakeSettingsOptions.Value.AutoLogRequest, - AutoLogResponse = logFlakeSettingsOptions.Value.AutoLogResponse, - AutoLogUnauthorized = logFlakeSettingsOptions.Value.AutoLogUnauthorized, - AutoLogGlobalExceptions = logFlakeSettingsOptions.Value.AutoLogGlobalExceptions, - PerformanceMonitor = logFlakeSettingsOptions.Value.PerformanceMonitor, - ExcludedPaths = logFlakeSettingsOptions.Value.ExcludedPaths, - }; - _version = versionService.Version; } - public void WriteLog(LogLevels logLevels, string? message, string? correlation, Dictionary? parameters = null) + public void WriteLog(LogLevels logLevel, string? message, string? correlation, Dictionary? parameters = null) { parameters?.Add("Assembly version", _version); - _logFlake.SendLog(logLevels, correlation, message, parameters); + _logFlake.SendLog(logLevel, correlation, message, parameters); } public void WriteException(Exception ex, string? correlation, string? message = null, Dictionary? parameters = null) From 0610b2aad7eac90b46e52367656637585e7628eb Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Wed, 5 Jun 2024 12:24:44 +0200 Subject: [PATCH 18/19] :green_heart: Adopt DeepSource suggestions --- LogFlake.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LogFlake.cs b/LogFlake.cs index 0115dfb..fd2abc6 100644 --- a/LogFlake.cs +++ b/LogFlake.cs @@ -62,7 +62,7 @@ private void LogsProcessor() { _ = _logsQueue.TryDequeue(out PendingLog? log); log.Retries++; - bool success = Post(log.QueueName!, log.JsonString!).Result; + bool success = Post(log.QueueName!, log.JsonString!).GetAwaiter().GetResult(); if (!success && log.Retries < FailedPostRetries) { _logsQueue.Enqueue(log); @@ -87,7 +87,7 @@ private async Task Post(string queueName, string jsonString) try { string requestUri = $"/api/ingestion/{AppId}/{queueName}"; - HttpResponseMessage result; + HttpResponseMessage result = new(System.Net.HttpStatusCode.InternalServerError); using HttpClient httpClient = _httpClientFactory.CreateClient(HttpClientConstants.ClientName); httpClient.BaseAddress = Server; if (EnableCompression) From 94a2357e6f646bc370f8c5df4d6e983d3ca15b8f Mon Sep 17 00:00:00 2001 From: Matteo Tammaccaro Date: Wed, 5 Jun 2024 14:25:00 +0200 Subject: [PATCH 19/19] :pencil: Update README --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d0b0415..d372d63 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,56 @@ > This repository contains the sources for the client-side components of the LogFlake product suite for applications logs and performance collection for .NET applications. -

🏠 [LogFlake Website](https://logflake.io) | 🔥 [CloudPhoenix Website](https://cloudphoenix.it)

+

🏠 LogFlake Website | 🔥 CloudPhoenix Website

## Downloads -| NuGet Package Name | Version | Downloads | -|:-------------------------------------------------------------------------------------:|:------------------------------------------------------------------------:|:---------------------------------------------------------------------------:| +|NuGet Package Name|Version|Downloads| +|:-:|:-:|:-:| | [LogFlake.Client.NetCore](https://www.nuget.org/packages/LogFlake.Client.NetCore) | ![NuGet Version](https://img.shields.io/nuget/v/logflake.client.netcore) | ![NuGet Downloads](https://img.shields.io/nuget/dt/logflake.client.netcore) | ## Usage -Retrieve your _application-key_ from Application Settings in LogFlake UI. +1. Retrieve your _application-key_ from Application Settings in LogFlake UI; +2. Add in your `secrets.json` file the following section: +```json +"LogFlake": { + "AppId": "application-key", + "Endpoint": "https://logflake-instance-here" // optional, if missing uses production endpoint +} +``` +3. Implement and register as Sigleton the interface `IVersionService`; +4. In your `Program.cs` files, register the LogFlake-related services: +```csharp +// configuration is an instance of IConfiguration +services.AddLogFlake(configuration); +``` +5. In your services, simply require `ILogFlakeService` as a dependency; +```csharp +public class SimpleService : ISimpleService +{ + private readonly ILogFlakeService _logFlakeService; + + public SimpleService(ILogFlakeService logFlakeService) + { + _logFlakeService = logFlakeService ?? throw new ArgumentNullException(nameof(logFlakeService)); + } +} + +``` +6. Use it in your service ```csharp -const logger = new LogFlake("application-key"); -logger.SendLog(LogLevels.INFO, "Hello World"); +// SimpleService.cs + +public void MyMethod() +{ + try + { + doSomething(); + _logFlakeService.WriteLog(LogLevels.INFO, "Hello World", "correlation"); + } + catch (MeaningfulException ex) + { + _logFlakeService.WriteException(ex, "correlation"); + } +} ```