Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions src/SharedKernel/Logging/Middleware/OutboundLoggingHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,44 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage

var (reqHeaders, reqBody) = await CaptureRequestAsync(request, cancellationToken);

var response = await base.SendAsync(request, cancellationToken);
HttpResponseMessage response;
try
{
response = await base.SendAsync(request, cancellationToken);
}
catch (Exception ex)
{
var elapsedMs = Stopwatch.GetElapsedTime(timestamp).TotalMilliseconds;

var errorBody = new Dictionary<string, object?>
{
["exceptionType"] = ex.GetType().Name,
["message"] = ex.Message
};

LogHttpOutFailure(
ex,
request.Method.Method,
request.RequestUri?.GetLeftPart(UriPartial.Path) ?? "",
elapsedMs,
"HttpOut",
string.IsNullOrEmpty(request.RequestUri?.Query) ? null : request.RequestUri!.Query,
LogFormatting.ToJsonString(reqHeaders),
LogFormatting.ToJsonString(reqBody),
LogFormatting.ToJsonString(errorBody));

throw;
}

var elapsedMs = Stopwatch.GetElapsedTime(timestamp)
.TotalMilliseconds;
var elapsed = Stopwatch.GetElapsedTime(timestamp).TotalMilliseconds;

var (resHeaders, resBody) = await CaptureResponseAsync(response, cancellationToken);

LogHttpOut(
request.Method.Method,
request.RequestUri?.GetLeftPart(UriPartial.Path) ?? "",
(int)response.StatusCode,
elapsedMs,
elapsed,
"HttpOut",
string.IsNullOrEmpty(request.RequestUri?.Query) ? null : request.RequestUri!.Query,
LogFormatting.ToJsonString(reqHeaders),
Expand Down Expand Up @@ -150,4 +176,17 @@ private partial void LogHttpOut(string method,
string requestBody,
string responseHeaders,
string responseBody);

[LoggerMessage(Level = LogLevel.Warning,
Message = "[HTTP OUT] {Method} {HostPath} -> FAILED in {ElapsedMs}ms | " +
"{Kind} query={Query} requestHeader={RequestHeaders} requestBody={RequestBody} error={ErrorBody}")]
private partial void LogHttpOutFailure(Exception ex,
string method,
string hostPath,
double elapsedMs,
string kind,
string? query,
string requestHeaders,
string requestBody,
string errorBody);
}
38 changes: 35 additions & 3 deletions src/SharedKernel/Logging/Middleware/SignalRLoggingHubFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,30 @@ internal sealed partial class SignalRLoggingHubFilter(ILogger<SignalRLoggingHubF
var rawArgsJson = JsonSerializer.Serialize(invocationContext.HubMethodArguments);
var redactedArgs = RedactionHelper.RedactBody("application/json", rawArgsJson);

var result = await next(invocationContext);
object? result;
try
{
result = await next(invocationContext);
}
catch (Exception ex)
{
var elapsedMs = Stopwatch.GetElapsedTime(timestamp).TotalMilliseconds;

var elapsedMs = Stopwatch.GetElapsedTime(timestamp).TotalMilliseconds;
var errorBody = new Dictionary<string, object?>
{
["exceptionType"] = ex.GetType().Name,
["message"] = ex.Message
};

LogInvoke(hubName, methodName, elapsedMs, "SignalR", connectionId, userId, LogFormatting.ToJsonString(redactedArgs));
LogInvokeFailure(ex, hubName, methodName, elapsedMs, "SignalR", connectionId, userId,
LogFormatting.ToJsonString(redactedArgs), LogFormatting.ToJsonString(errorBody));

throw;
}

var elapsed = Stopwatch.GetElapsedTime(timestamp).TotalMilliseconds;

LogInvoke(hubName, methodName, elapsed, "SignalR", connectionId, userId, LogFormatting.ToJsonString(redactedArgs));

return result;
}
Expand Down Expand Up @@ -64,6 +83,19 @@ private partial void LogInvoke(
string? userId,
string args);

[LoggerMessage(Level = LogLevel.Warning,
Message = "[SignalR] {Hub}.{HubMethod} FAILED in {ElapsedMs}ms | {Kind} connId={ConnId} userId={UserId} args={Args} error={ErrorBody}")]
private partial void LogInvokeFailure(
Exception ex,
string hub,
string hubMethod,
double elapsedMs,
string kind,
string? connId,
string? userId,
string args,
string errorBody);

[LoggerMessage(Level = LogLevel.Information,
Message = "[Connected] SignalR {Hub} | {Kind} connId={ConnId} userId={UserId}")]
private partial void LogConnected(
Expand Down
6 changes: 3 additions & 3 deletions src/SharedKernel/SharedKernel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
<PackageIcon>pandatech.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>

<Version>2.2.3</Version>
<PackageReleaseNotes>Cors now will start to expose Content-Disposition header</PackageReleaseNotes>
<Version>2.2.4</Version>
<PackageReleaseNotes>Log HTTP out and SignalR hub invocation failures (timeout, connection errors, exceptions) that were previously silently lost</PackageReleaseNotes>

<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSymbols>true</IncludeSymbols>
Expand Down Expand Up @@ -71,7 +71,7 @@
<PackageReference Include="Pandatech.FluentMinimalApiMapper" Version="4.0.0"/>
<PackageReference Include="Pandatech.PandaVaultClient" Version="6.0.0"/>
<PackageReference Include="Pandatech.ResponseCrafter" Version="7.0.0"/>
<PackageReference Include="Scalar.AspNetCore" Version="2.13.1" />
<PackageReference Include="Scalar.AspNetCore" Version="2.13.3" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0"/>
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0"/>
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.2"/>
Expand Down