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]";
}
}
}