elfisa-pharmacy/src/ElectronicPharmacy/Classes/AppDebugLog.cs
Magomed a6601567f2 Improve order UX and harden API debugging.
Prompt for Location ID after login, move cart delete there, refine price-list Backspace navigation, and log every API failure with stack and response body.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 16:31:00 +03:00

236 lines
7.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace Электроннаяармация.Classes
{
/// <summary>
/// File logger for API/auth/network debugging.
/// Logs are written to Logs/api-debug-YYYY-MM-DD.log next to the executable.
/// </summary>
public static class AppDebugLog
{
private static readonly object Sync = new object();
private const int MaxBodyLength = 8000;
private const string DisableEnvVar = "ELF_API_DEBUG";
private const string DisableMarkerFile = "api-debug.off";
public static bool IsEnabled
{
get
{
var env = Environment.GetEnvironmentVariable(DisableEnvVar);
if (string.Equals(env, "0", StringComparison.OrdinalIgnoreCase) ||
string.Equals(env, "false", StringComparison.OrdinalIgnoreCase))
{
return false;
}
var markerPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DisableMarkerFile);
return !File.Exists(markerPath);
}
}
public static string LogDirectory =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
public static string CurrentLogFilePath =>
Path.Combine(LogDirectory, $"api-debug-{DateTime.Now:yyyy-MM-dd}.log");
public static void Info(string category, string message)
{
Write("INFO", category, message, null);
}
public static void Warning(string category, string message)
{
Write("WARN", category, message, null);
}
public static void Error(string category, string message, Exception exception = null)
{
Write("ERROR", category, message, exception);
}
public static void ApiRequest(string method, string url, string requestBody = null, string extra = null)
{
var message = new StringBuilder();
message.Append(method).Append(" ").Append(url);
if (!string.IsNullOrWhiteSpace(extra))
{
message.Append(" | ").Append(extra);
}
if (!string.IsNullOrWhiteSpace(requestBody))
{
message.Append(" | body=").Append(Truncate(SanitizeJson(requestBody)));
}
Write("REQ", "Api", message.ToString(), null);
}
public static void ApiResponse(string method, string url, int statusCode, long elapsedMs, string responseBody = null)
{
var message = new StringBuilder();
message.Append(method).Append(" ").Append(url);
message.Append(" -> HTTP ").Append(statusCode);
message.Append(" (").Append(elapsedMs).Append(" ms)");
if (!string.IsNullOrWhiteSpace(responseBody))
{
message.Append(" | body=").Append(Truncate(SanitizeJson(responseBody)));
}
Write(statusCode >= 400 ? "RESP-ERR" : "RESP", "Api", message.ToString(), null);
}
public static void ApiFailure(string method, string url, long elapsedMs, Exception exception)
{
var message = $"{method} {url} failed after {elapsedMs} ms: {exception?.Message}";
Write("FAIL", "Api", message, exception);
}
/// <summary>
/// Логирует ошибку HTTP-ответа API (4xx/5xx) с телом ответа.
/// </summary>
public static void ApiHttpError(
string method,
string url,
int statusCode,
string responseBody,
string context = null)
{
var message = new StringBuilder();
message.Append(method).Append(" ").Append(url);
message.Append(" -> HTTP ").Append(statusCode);
if (!string.IsNullOrWhiteSpace(context))
{
message.Append(" | ").Append(context);
}
if (!string.IsNullOrWhiteSpace(responseBody))
{
message.Append(" | body=").Append(Truncate(SanitizeJson(responseBody)));
}
Write("HTTP-ERR", "Api", message.ToString(), null);
}
public static string MaskToken(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
return "(empty)";
}
if (token.Length <= 8)
{
return "***";
}
return token.Substring(0, 4) + "..." + token.Substring(token.Length - 4);
}
public static string SanitizeJson(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return string.Empty;
}
var sanitized = Regex.Replace(
json,
@"""password""\s*:\s*""[^""]*""",
@"""password"":""***""",
RegexOptions.IgnoreCase);
sanitized = Regex.Replace(
sanitized,
@"""token""\s*:\s*""([^""]*)""",
m => @"""token"":""" + MaskToken(m.Groups[1].Value) + @"""",
RegexOptions.IgnoreCase);
return sanitized;
}
public static string FormatException(Exception exception)
{
if (exception == null)
{
return string.Empty;
}
var sb = new StringBuilder();
var current = exception;
var depth = 0;
while (current != null && depth < 8)
{
if (depth > 0)
{
sb.AppendLine();
sb.Append(" -> ");
}
sb.Append(current.GetType().Name).Append(": ").Append(current.Message);
if (!string.IsNullOrWhiteSpace(current.StackTrace) && depth == 0)
{
sb.AppendLine();
sb.Append(current.StackTrace);
}
current = current.InnerException;
depth++;
}
return sb.ToString();
}
private static void Write(string level, string category, string message, Exception exception)
{
if (!IsEnabled)
{
return;
}
try
{
var line = new StringBuilder();
line.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
line.Append(" [").Append(level).Append("] ");
line.Append("[").Append(category).Append("] ");
line.Append(message);
if (exception != null)
{
line.AppendLine();
line.Append(FormatException(exception));
}
lock (Sync)
{
Directory.CreateDirectory(LogDirectory);
File.AppendAllText(CurrentLogFilePath, line + Environment.NewLine, Encoding.UTF8);
}
Debug.WriteLine(line.ToString());
}
catch
{
// Logging must never break the app.
}
}
private static string Truncate(string text)
{
if (string.IsNullOrEmpty(text) || text.Length <= MaxBodyLength)
{
return text ?? string.Empty;
}
return text.Substring(0, MaxBodyLength) + "...[truncated]";
}
}
}