using Microsoft.ApplicationInsights.DataContracts; using System.Text; namespace PlanTempus.Core.Logging { public class SeqLogger { private readonly SeqHttpClient _httpClient; private readonly string _environmentName; private readonly string _machineName; private readonly SeqConfiguration _configuration; public SeqLogger(SeqHttpClient httpClient, SeqConfiguration configuration) { _httpClient = httpClient; _configuration = configuration; } 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_{prop.Key}", prop.Value); foreach (var prop in trace.Context.GlobalProperties) seqEvent.Add($"global_{prop.Key}", prop.Value); await SendToSeqAsync(seqEvent, cancellationToken); } 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_{prop.Key}", prop.Value); foreach (var prop in evt.Context.GlobalProperties) seqEvent.Add($"global_{prop.Key}", prop.Value); foreach (var metric in evt.Metrics) seqEvent.Add($"metric_{metric.Key}", metric.Value); await SendToSeqAsync(seqEvent, cancellationToken); } 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_{prop.Key}", prop.Value); foreach (var prop in ex.Context.GlobalProperties) seqEvent.Add($"global_{prop.Key}", prop.Value); await SendToSeqAsync(seqEvent, cancellationToken); } 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_{prop.Key}", prop.Value); foreach (var prop in dep.Context.GlobalProperties) seqEvent.Add($"global_{prop.Key}", prop.Value); await SendToSeqAsync(seqEvent, cancellationToken); } public async Task LogAsync(RequestTelemetry req, CancellationToken cancellationToken = default) { await Task.CompletedTask; throw new NotImplementedException(); } public async Task LogAsync( Microsoft.ApplicationInsights.Extensibility.IOperationHolder operationHolder, CancellationToken cancellationToken = default) { var req = operationHolder.Telemetry; //https://docs.datalust.co/v2025.1/docs/posting-raw-events var seqEvent = new Dictionary { { "@t", req.Timestamp.UtcDateTime.ToString("o") }, { "@mt", req.Name }, { "@l", req.Success ?? true ? "Information" : "Error" }, { "@sp", req.Id }, //Span id Unique identifier of a span Yes, if the event is a span { "@tr", req.Context.Operation.Id }, //Trace id An identifier that groups all spans and logs that are in the same trace Yes, if the event is a span { "@sk", "Server" }, //Span kind Describes the relationship of the span to others in the trace: Client, Server, Internal, Producer, or Consumer { "@st", req.Timestamp.UtcDateTime.Subtract(req.Duration).ToString("o") }, //Start The start ISO 8601 timestamp of this span Yes, if the event is a span { "SourceContext", typeof(T).FullName }, { "Url", req.Url }, { "RequestId", req.Id }, { "ItemTypeFlag", req.ItemTypeFlag.ToString() } }; if (!string.IsNullOrEmpty(req.ResponseCode)) { if (int.TryParse(req.ResponseCode, out int statusCode)) { if (Enum.IsDefined(typeof(System.Net.HttpStatusCode), statusCode)) seqEvent["StatusCode"] = $"{statusCode} {(System.Net.HttpStatusCode)statusCode}"; else seqEvent["StatusCode"] = $"{statusCode} Unknown"; } } if (!string.IsNullOrEmpty(req.Context.Operation.ParentId)) seqEvent["@ps"] = req.Context.Operation.ParentId; if (req.Properties.TryGetValue("httpMethod", out string method)) { seqEvent["RequestMethod"] = method; seqEvent["@mt"] = $"{req.Properties["httpMethod"]} {req.Name}"; req.Properties.Remove("httpMethod"); } foreach (var prop in req.Properties) seqEvent.Add($"prop_{prop.Key}", prop.Value); foreach (var prop in req.Context.GlobalProperties) seqEvent.Add($"{prop.Key}", prop.Value); 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"); var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/ingest/clef") { Content = content }; var result = await _httpClient.SendAsync(requestMessage, cancellationToken); result.EnsureSuccessStatusCode(); } private static string MapSeverityToLevel(SeverityLevel? severity) { return severity switch { SeverityLevel.Verbose => "Verbose", SeverityLevel.Information => "Information", SeverityLevel.Warning => "Warning", SeverityLevel.Error => "Error", SeverityLevel.Critical => "Fatal", _ => "Information" }; } private static 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(); } } }