diff --git a/README.md b/README.md index 1d83840..748b525 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,22 @@ Das Tray-Icon zeigt den aktuellen Zustand: - Grün: Worker ist aktiviert, Backend-Konfiguration ist vollständig, Drucker ist verfügbar und der letzte Backend-Kontakt war erfolgreich. - Rot: Konfiguration fehlt, Drucker ist nicht verfügbar, Worker ist deaktiviert oder der Backend-Kontakt ist fehlgeschlagen. +## Logs und 401-Fehler + +Logs liegen standardmäßig unter: + +`C:\ProgramData\LabelPrintAgent\logs\label-print-agent-YYYYMMDD.log` + +Bei HTTP-Fehlern wie `401 Unauthorized` protokolliert der Agent: + +- HTTP-Methode und URL +- Statuscode und Reason Phrase +- ob ein Authorization-Header gesetzt wurde +- Authorization-Scheme, z. B. `Bearer` +- Token-Länge, aber niemals den Token selbst +- `WWW-Authenticate`-Header des Backends +- gekürzten Response-Body des Backends + ## Nicht mehr im Agent - keine Layout-JSON-Verwaltung diff --git a/src/LabelPrintAgent/Backend/BackendClient.cs b/src/LabelPrintAgent/Backend/BackendClient.cs index 4a3db8b..dbe4afe 100644 --- a/src/LabelPrintAgent/Backend/BackendClient.cs +++ b/src/LabelPrintAgent/Backend/BackendClient.cs @@ -3,6 +3,7 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using LabelPrintAgent.Configuration; +using Serilog; namespace LabelPrintAgent.Backend; @@ -28,7 +29,7 @@ public sealed class BackendClient return null; } - response.EnsureSuccessStatusCode(); + await EnsureSuccessWithDiagnosticsAsync(response, request, cancellationToken); return await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); } @@ -49,7 +50,7 @@ public sealed class BackendClient var settings = _settingsStore.Load(); using var request = CreateRequest(HttpMethod.Get, MakeAbsoluteUrl(settings.Backend.BaseUrl, job.LabelImageUrl), settings); using var response = await _httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); + await EnsureSuccessWithDiagnosticsAsync(response, request, cancellationToken); var bytesFromUrl = await response.Content.ReadAsByteArrayAsync(cancellationToken); return CreateBitmap(bytesFromUrl); } @@ -83,7 +84,7 @@ public sealed class BackendClient using var request = CreateRequest(HttpMethod.Post, MakeAbsoluteUrl(settings.Backend.BaseUrl, path), settings); request.Content = JsonContent.Create(body); using var response = await _httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); + await EnsureSuccessWithDiagnosticsAsync(response, request, cancellationToken); } public async Task WatchServerSentEventsAsync(Func onLabelJobAvailable, CancellationToken cancellationToken = default) @@ -93,7 +94,7 @@ public sealed class BackendClient request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - response.EnsureSuccessStatusCode(); + await EnsureSuccessWithDiagnosticsAsync(response, request, cancellationToken); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var reader = new StreamReader(stream); @@ -125,6 +126,48 @@ public sealed class BackendClient return request; } + private static async Task EnsureSuccessWithDiagnosticsAsync( + HttpResponseMessage response, + HttpRequestMessage request, + CancellationToken cancellationToken) + { + if (response.IsSuccessStatusCode) + { + return; + } + + string responseBody; + try + { + responseBody = response.Content is null + ? string.Empty + : await response.Content.ReadAsStringAsync(cancellationToken); + } + catch (Exception ex) + { + responseBody = $""; + } + + var authorization = request.Headers.Authorization; + var wwwAuthenticate = string.Join("; ", response.Headers.WwwAuthenticate.Select(value => value.ToString())); + + Log.Warning( + "Backend request failed: {Method} {Url} -> {StatusCode} {ReasonPhrase}. " + + "AuthorizationHeaderPresent={AuthorizationHeaderPresent}, AuthorizationScheme={AuthorizationScheme}, " + + "TokenLength={TokenLength}, WwwAuthenticate={WwwAuthenticate}, ResponseBody={ResponseBody}", + request.Method.Method, + request.RequestUri, + (int)response.StatusCode, + response.ReasonPhrase, + authorization is not null, + authorization?.Scheme ?? string.Empty, + authorization?.Parameter?.Length ?? 0, + wwwAuthenticate, + Truncate(responseBody, 2000)); + + response.EnsureSuccessStatusCode(); + } + private static string BuildUrl(string baseUrl, string path, string agentId) { var separator = path.Contains('?') ? '&' : '?'; @@ -144,6 +187,16 @@ public sealed class BackendClient private static string EnsureTrailingSlash(string url) => url.EndsWith('/') ? url : url + "/"; + private static string Truncate(string value, int maxLength) + { + if (string.IsNullOrEmpty(value) || value.Length <= maxLength) + { + return value; + } + + return value[..maxLength] + "..."; + } + private static Bitmap CreateBitmap(byte[] bytes) { using var stream = new MemoryStream(bytes);