using System; using System.Diagnostics; using System.IO; using System.Text; using System.Text.RegularExpressions; namespace Электронная_Фармация.Classes { /// /// File logger for API/auth/network debugging. /// Logs are written to Logs/api-debug-YYYY-MM-DD.log next to the executable. /// 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); } /// /// Логирует ошибку HTTP-ответа API (4xx/5xx) с телом ответа. /// 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]"; } } }