diff --git a/Core/Telemetry/SeqLogger.cs b/Core/Telemetry/SeqLogger.cs index 4516383..321d722 100644 --- a/Core/Telemetry/SeqLogger.cs +++ b/Core/Telemetry/SeqLogger.cs @@ -1,185 +1,238 @@ -using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; using System.Text; using System.Text.Json; namespace Core.Telemetry { - public record SeqConfiguration(string IngestionEndpoint, string ApiKey, string Environment); + public record SeqConfiguration(string IngestionEndpoint, string ApiKey, string Environment); - public class SeqHttpClient - { - HttpClient _httpClient; + public class SeqHttpClient + { + HttpClient _httpClient; - public SeqHttpClient(SeqConfiguration seqConfiguration) - { - _httpClient = new HttpClient() - { - BaseAddress = new Uri("http://localhost:5341"), - Timeout = TimeSpan.FromSeconds(30) - }; + public SeqHttpClient(SeqConfiguration seqConfiguration, HttpMessageHandler httpMessageHandler) + { + _httpClient = new HttpClient(httpMessageHandler) + { + BaseAddress = new Uri(seqConfiguration.IngestionEndpoint), + Timeout = TimeSpan.FromSeconds(30) + }; - _httpClient.DefaultRequestHeaders.Accept.Clear(); - _httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); - if (seqConfiguration.ApiKey != null) - _httpClient.DefaultRequestHeaders.Add("X-Seq-ApiKey", seqConfiguration.ApiKey); - } + _httpClient.DefaultRequestHeaders.Accept.Clear(); + _httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); + if (seqConfiguration.ApiKey != null) + _httpClient.DefaultRequestHeaders.Add("X-Seq-ApiKey", seqConfiguration.ApiKey); + } + public SeqHttpClient(SeqConfiguration seqConfiguration) : this(seqConfiguration, new HttpClientHandler()) { } - public async Task SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) - { - return await _httpClient.SendAsync(httpRequestMessage, cancellationToken); - } - } + public async Task SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) + { + return await _httpClient.SendAsync(httpRequestMessage, cancellationToken); + } + } - public class SeqLogger - { - private readonly SeqHttpClient _httpClient; - private readonly string _environmentName; - private readonly string _machineName; - private readonly SeqConfiguration _configuration; + public class SeqLogger + { + private readonly SeqHttpClient _httpClient; + private readonly string _environmentName; + private readonly string _machineName; + private readonly SeqConfiguration _configuration; - public SeqLogger(SeqHttpClient httpClient, string environmentName, SeqConfiguration configuration) - { - _httpClient = httpClient; - _environmentName = configuration.Environment; - _machineName = Environment.MachineName; - } + public SeqLogger(SeqHttpClient httpClient, string environmentName, SeqConfiguration configuration) + { + _httpClient = httpClient; + _environmentName = configuration.Environment; + _machineName = Environment.MachineName; + } - public async Task LogTraceTelemetryAsync(TraceTelemetry trace, CancellationToken cancellationToken = default) - { - var seqEvent = new Dictionary - { - { "@t", trace.Timestamp.UtcDateTime.ToString("o") }, - { "@mt", trace.Message }, - { "@l", MapSeverityToLevel(trace.SeverityLevel) }, - { "Environment", _environmentName }, - { "MachineName", _machineName } - }; + public async Task LogAsync(TraceTelemetry trace, CancellationToken cancellationToken = default) + { + var seqEvent = new Dictionary + { + { "@t", trace.Timestamp.UtcDateTime.ToString("o") }, + { "@mt", trace.Message }, + { "@l", MapSeverityToLevel(trace.SeverityLevel) }, + { "Environment", _environmentName }, + { "MachineName", _machineName } + }; - foreach (var prop in trace.Properties) - { - seqEvent.Add(prop.Key, prop.Value); - } + foreach (var prop in trace.Properties) + { + seqEvent.Add(prop.Key, prop.Value); + } - await SendToSeqAsync(seqEvent, cancellationToken); - } + await SendToSeqAsync(seqEvent, cancellationToken); + } - public async Task LogEventTelemetryAsync(EventTelemetry evt, CancellationToken cancellationToken = default) - { - var seqEvent = new Dictionary - { - { "@t", evt.Timestamp.UtcDateTime.ToString("o") }, - { "@mt", evt.Name }, - { "@l", "Information" }, - { "Environment", _environmentName }, - { "MachineName", _machineName } - }; + public async Task LogAsync(EventTelemetry evt, CancellationToken cancellationToken = default) + { + var seqEvent = new Dictionary + { + { "@t", evt.Timestamp.UtcDateTime.ToString("o") }, + { "@mt", evt.Name }, + { "@l", "Information" }, + { "Environment", _environmentName }, + { "MachineName", _machineName } + }; - foreach (var prop in evt.Properties) - { - seqEvent.Add(prop.Key, prop.Value); - } + foreach (var prop in evt.Properties) + { + seqEvent.Add(prop.Key, prop.Value); + } - foreach (var metric in evt.Metrics) - { - seqEvent.Add($"metric_{metric.Key}", metric.Value); - } + foreach (var metric in evt.Metrics) + { + seqEvent.Add($"metric_{metric.Key}", metric.Value); + } - await SendToSeqAsync(seqEvent, cancellationToken); - } + await SendToSeqAsync(seqEvent, cancellationToken); + } - public async Task LogExceptionTelemetryAsync(ExceptionTelemetry ex, CancellationToken cancellationToken = default) - { - var seqEvent = new Dictionary - { - { "@t", ex.Timestamp.UtcDateTime.ToString("o") }, - { "@mt", ex.Exception.Message }, - { "@l", "Error" }, - { "Environment", _environmentName }, - { "MachineName", _machineName }, - { "ExceptionType", ex.Exception.GetType().Name }, - { "StackTrace", ex.Exception.StackTrace } - }; + public async Task LogAsync(ExceptionTelemetry ex, CancellationToken cancellationToken = default) + { + var seqEvent = new Dictionary + { + { "@t", ex.Timestamp.UtcDateTime.ToString("o") }, + { "@mt", ex.Exception.Message }, + { "@l", "Error" }, + { "@x", FormatExceptionForSeq(ex.Exception) }, + { "Environment", _environmentName }, + { "MachineName", _machineName }, + { "ExceptionType", ex.Exception.GetType().Name }, + }; - foreach (var prop in ex.Properties) - { - seqEvent.Add(prop.Key, prop.Value); - } + foreach (var prop in ex.Properties) + { + seqEvent.Add(prop.Key, prop.Value); + } - await SendToSeqAsync(seqEvent, cancellationToken); - } + await SendToSeqAsync(seqEvent, cancellationToken); + } - public async Task LogDependencyTelemetryAsync(DependencyTelemetry dep, CancellationToken cancellationToken = default) - { - var seqEvent = new Dictionary - { - { "@t", dep.Timestamp.UtcDateTime.ToString("o") }, - { "@mt", $"Dependency: {dep.Name}" }, - { "@l", dep.Success??true ? "Information" : "Error" }, - { "Environment", _environmentName }, - { "MachineName", _machineName }, - { "DependencyType", dep.Type }, - { "Target", dep.Target }, - { "Duration", dep.Duration.TotalMilliseconds } - }; + public async Task LogAsync(DependencyTelemetry dep, CancellationToken cancellationToken = default) + { + var seqEvent = new Dictionary + { + { "@t", dep.Timestamp.UtcDateTime.ToString("o") }, + { "@mt", $"Dependency: {dep.Name}" }, + { "@l", dep.Success??true ? "Information" : "Error" }, + { "Environment", _environmentName }, + { "MachineName", _machineName }, + { "DependencyType", dep.Type }, + { "Target", dep.Target }, + { "Duration", dep.Duration.TotalMilliseconds } + }; - foreach (var prop in dep.Properties) - { - seqEvent.Add(prop.Key, prop.Value); - } + foreach (var prop in dep.Properties) + { + seqEvent.Add(prop.Key, prop.Value); + } - await SendToSeqAsync(seqEvent, cancellationToken); - } + await SendToSeqAsync(seqEvent, cancellationToken); + } - public async Task LogRequestTelemetryAsync(RequestTelemetry req, CancellationToken cancellationToken = default) - { - var seqEvent = new Dictionary - { - { "@t", req.Timestamp.UtcDateTime.ToString("o") }, - { "@mt", $"Request: {req.Name}" }, - { "@l", req.Success??true ? "Information" : "Error" }, - { "Environment", _environmentName }, - { "MachineName", _machineName }, - { "Url", req.Url }, - { "ResponseCode", req.ResponseCode }, - { "Duration", req.Duration.TotalMilliseconds } - }; + public async Task LogAsync(RequestTelemetry req, CancellationToken cancellationToken = default) + { + var seqEvent = new Dictionary + { + { "@t", req.Timestamp.UtcDateTime.ToString("o") }, + { "@mt", $"Request: {req.Name}" }, + { "@l", req.Success??true ? "Information" : "Error" }, + { "Environment", _environmentName }, + { "MachineName", _machineName }, + { "Url", req.Url }, + { "ResponseCode", req.ResponseCode }, + { "Duration", req.Duration.TotalMilliseconds } + }; - foreach (var prop in req.Properties) - { - seqEvent.Add(prop.Key, prop.Value); - } + foreach (var prop in req.Properties) + { + seqEvent.Add(prop.Key, prop.Value); + } - await SendToSeqAsync(seqEvent, cancellationToken); - } + await SendToSeqAsync(seqEvent, cancellationToken); + } - private async Task SendToSeqAsync(Dictionary seqEvent, CancellationToken cancellationToken) - { - var content = new StringContent( - Newtonsoft.Json.JsonConvert.SerializeObject(seqEvent), - Encoding.UTF8, - "application/vnd.serilog.clef"); + private async Task SendToSeqAsync(Dictionary seqEvent, CancellationToken cancellationToken) + { + var content = new StringContent( + Newtonsoft.Json.JsonConvert.SerializeObject(seqEvent), + Encoding.UTF8, + "application/vnd.serilog.clef"); - var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/ingest/clef") - { - Content = content - }; + var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/ingest/clef") + { + Content = content + }; - await _httpClient.SendAsync(requestMessage, cancellationToken); - } + var result = await _httpClient.SendAsync(requestMessage, cancellationToken); - private string MapSeverityToLevel(SeverityLevel? severity) - { - return severity switch - { - SeverityLevel.Verbose => "Verbose", - SeverityLevel.Information => "Information", - SeverityLevel.Warning => "Warning", - SeverityLevel.Error => "Error", - SeverityLevel.Critical => "Fatal", - _ => "Information" - }; - } - } + result.EnsureSuccessStatusCode(); + } + + private string MapSeverityToLevel(SeverityLevel? severity) + { + return severity switch + { + SeverityLevel.Verbose => "Verbose", + SeverityLevel.Information => "Information", + SeverityLevel.Warning => "Warning", + SeverityLevel.Error => "Error", + SeverityLevel.Critical => "Fatal", + _ => "Information" + }; + } + private string FormatExceptionForSeq(Exception ex) + { + var sb = new StringBuilder(); + var exceptionCount = 0; + + void FormatSingleException(Exception currentEx, int depth) + { + if (depth > 0) sb.AppendLine("\n--- Inner Exception ---"); + + sb.AppendLine($"Exception Type: {currentEx.GetType().FullName}"); + sb.AppendLine($"Message: {currentEx.Message}"); + sb.AppendLine($"Source: {currentEx.Source}"); + sb.AppendLine($"HResult: 0x{currentEx.HResult:X8}"); + sb.AppendLine("Stack Trace:"); + sb.AppendLine(currentEx.StackTrace?.Trim()); + + if (currentEx.Data.Count > 0) + { + sb.AppendLine("Additional Data:"); + foreach (var key in currentEx.Data.Keys) + { + sb.AppendLine($" {key}: {currentEx.Data[key]}"); + } + } + } + + void RecurseExceptions(Exception currentEx, int depth = 0) + { + if (currentEx is AggregateException aggEx) + { + foreach (var inner in aggEx.InnerExceptions) + { + RecurseExceptions(inner, depth); + depth++; + } + } + else if (currentEx.InnerException != null) + { + RecurseExceptions(currentEx.InnerException, depth + 1); + } + + FormatSingleException(currentEx, depth); + exceptionCount++; + } + + RecurseExceptions(ex); + sb.Insert(0, $"EXCEPTION CHAIN ({exceptionCount} exceptions):\n"); + return sb.ToString(); + } + } } diff --git a/Tests/TelemetryLogging/SeqLoggerTests.cs b/Tests/TelemetryLogging/SeqLoggerTests.cs index 8ee1d03..8c98655 100644 --- a/Tests/TelemetryLogging/SeqLoggerTests.cs +++ b/Tests/TelemetryLogging/SeqLoggerTests.cs @@ -1,109 +1,134 @@ using Core.Telemetry; +using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; namespace Tests.TelemetryLogging { - [TestClass] - public class SeqLoggerTests : TestFixture - { - private SeqLogger _logger; - private SeqHttpClient _httpClient; - private readonly string _testId; + [TestClass] + public class SeqLoggerTests : TestFixture + { + private SeqLogger _logger; + private SeqHttpClient _httpClient; + private readonly string _testId; - public SeqLoggerTests() - { - _testId = Guid.NewGuid().ToString(); - var config = new SeqConfiguration("http://localhost:5341", null, "MSTEST"); - _httpClient = new SeqHttpClient(config); - _logger = new SeqLogger(_httpClient, Environment.MachineName, config); - } + public SeqLoggerTests() + { + _testId = Guid.NewGuid().ToString(); + var config = new SeqConfiguration("http://localhost:5341", null, "MSTEST"); + _httpClient = new SeqHttpClient(config); + _logger = new SeqLogger(_httpClient, Environment.MachineName, config); + } - [TestMethod] - public async Task LogTraceTelemetry_SendsCorrectData() - { - // Arrange - var traceTelemetry = new TraceTelemetry - { - Message = "Test trace message", - SeverityLevel = SeverityLevel.Error, - Timestamp = DateTimeOffset.UtcNow - }; - traceTelemetry.Properties.Add("TestId", _testId); + [TestMethod] + public async Task LogTraceTelemetry_SendsCorrectDataWithErrorLevel() + { + // Arrange + var traceTelemetry = new TraceTelemetry + { + Message = "Test trace error message", + SeverityLevel = SeverityLevel.Error, + Timestamp = DateTimeOffset.UtcNow + }; + traceTelemetry.Properties.Add("TestId", _testId); - // Act - await _logger.LogTraceTelemetryAsync(traceTelemetry); + // Act + await _logger.LogAsync(traceTelemetry); - // Du kan nu tjekke Seq med følgende query: - // TestId = 'guid-værdi-her' - } + // Du kan nu tjekke Seq med følgende query: + // TestId = 'guid-værdi-her' + } + [TestMethod] + public async Task LogTraceTelemetry_SendsCorrectDataWithWarningLevel() + { + // Arrange + var traceTelemetry = new TraceTelemetry + { + Message = "Test trace warning message", + SeverityLevel = SeverityLevel.Warning, + Timestamp = DateTimeOffset.UtcNow + }; + traceTelemetry.Properties.Add("TestId", _testId); - [TestMethod] - public async Task LogEventTelemetry_SendsCorrectData() - { - // Arrange - var eventTelemetry = new EventTelemetry - { - Name = "Test Event", - Timestamp = DateTimeOffset.UtcNow - }; - eventTelemetry.Properties.Add("TestId", _testId); - eventTelemetry.Metrics.Add("TestMetric", 42.0); + // Act + await _logger.LogAsync(traceTelemetry); - // Act - await _logger.LogEventTelemetryAsync(eventTelemetry); - } + } + [TestMethod] + public async Task LogEventTelemetry_SendsCorrectData() + { + // Arrange + var eventTelemetry = new EventTelemetry + { + Name = "Test Event", + Timestamp = DateTimeOffset.UtcNow + }; + eventTelemetry.Properties.Add("TestId", _testId); + eventTelemetry.Metrics.Add("TestMetric", 42.0); - [TestMethod] - public async Task LogExceptionTelemetry_SendsCorrectData() - { - // Arrange - var exception = new Exception("Test exception"); - var exceptionTelemetry = new ExceptionTelemetry(exception) - { - Timestamp = DateTimeOffset.UtcNow - }; - exceptionTelemetry.Properties.Add("TestId", _testId); + // Act + await _logger.LogAsync(eventTelemetry); + } - // Act - await _logger.LogExceptionTelemetryAsync(exceptionTelemetry); - } + [TestMethod] + public async Task LogExceptionTelemetry_SendsCorrectData() + { + try + { + int t = 0; + var result = 10 / t; - [TestMethod] - public async Task LogDependencyTelemetry_SendsCorrectData() - { - // Arrange - var dependencyTelemetry = new DependencyTelemetry - { - Name = "SQL Query", - Type = "SQL", - Target = "TestDB", - Success = true, - Duration = TimeSpan.FromMilliseconds(100), - Timestamp = DateTimeOffset.UtcNow - }; - dependencyTelemetry.Properties.Add("TestId", _testId); + } + catch (Exception e) + { - // Act - await _logger.LogDependencyTelemetryAsync(dependencyTelemetry); - } + // Arrange + var exceptionTelemetry = new ExceptionTelemetry(e) + { + Timestamp = DateTimeOffset.UtcNow + }; + exceptionTelemetry.Properties.Add("TestId", _testId); - [TestMethod] - public async Task LogRequestTelemetry_SendsCorrectData() - { - // Arrange - var requestTelemetry = new RequestTelemetry - { - Name = "GET /api/test", - Success = true, - ResponseCode = "200", - Duration = TimeSpan.FromMilliseconds(50), - Url = new Uri("http://test.com/api/test"), - Timestamp = DateTimeOffset.UtcNow - }; - requestTelemetry.Properties.Add("TestId", _testId); + // Act + await _logger.LogAsync(exceptionTelemetry); + } + } - // Act - await _logger.LogRequestTelemetryAsync(requestTelemetry); - } - } + [TestMethod] + public async Task LogDependencyTelemetry_SendsCorrectData() + { + // Arrange + var dependencyTelemetry = new DependencyTelemetry + { + Name = "SQL Query", + Type = "SQL", + Target = "TestDB", + Success = true, + Duration = TimeSpan.FromMilliseconds(100), + Timestamp = DateTimeOffset.UtcNow + }; + dependencyTelemetry.Properties.Add("TestId", _testId); + + // Act + await _logger.LogAsync(dependencyTelemetry); + } + + [TestMethod] + public async Task LogRequestTelemetry_SendsCorrectData() + { + // Arrange + var requestTelemetry = new RequestTelemetry + { + Name = "GET /api/test", + Success = true, + ResponseCode = "200", + Duration = TimeSpan.FromMilliseconds(50), + Url = new Uri("http://test.com/api/test"), + Timestamp = DateTimeOffset.UtcNow + }; + requestTelemetry.Properties.Add("TestId", _testId); + + // Act + await _logger.LogAsync(requestTelemetry); + } + } }