/* * Copyright (c) 2023 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using Newtonsoft.Json; using ProtonVPN.Common.Core.Helpers; using ProtonVPN.Common.Legacy.Helpers; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; namespace ProtonVPN.Logging.Log4Net; public class Log4NetLogger : Log4NetLoggerInitializer, ILogger { private const int MAX_RECENT_LOG_LINES = 100; private readonly IList _recentLogs = new List(); public Log4NetLogger(ILoggerConfiguration loggerConfiguration) : base(loggerConfiguration) { Info($"=== Booting Proton VPN {AssemblyVersion.Get()} [{OSVersion.GetPlatformString()}] {OSArchitecture.StringValue} ==="); } public IList GetRecentLogs() { return _recentLogs; } public void Debug( string message, Exception exception = null, int stackTraceDepth = 0, [CallerFilePath] string sourceFilePath = "", [CallerMemberName] string sourceMemberName = "", [CallerLineNumber] int sourceLineNumber = 0) where TEvent : ILogEvent, new() { CallerProfile callerProfile = new(sourceFilePath, sourceMemberName, sourceLineNumber); string fullLogMessage = CreateFullLogMessage(message, callerProfile, stackTraceDepth); if (exception == null) { InternalLogger.Debug(fullLogMessage); AddMessageToRecentLogs(fullLogMessage); } else { InternalLogger.Debug(fullLogMessage, exception); AddMessageToRecentLogs(fullLogMessage, exception); } } public void Info( string message, Exception exception = null, int stackTraceDepth = 0, [CallerFilePath] string sourceFilePath = "", [CallerMemberName] string sourceMemberName = "", [CallerLineNumber] int sourceLineNumber = 0) where TEvent : ILogEvent, new() { CallerProfile callerProfile = new(sourceFilePath, sourceMemberName, sourceLineNumber); string fullLogMessage = CreateFullLogMessage(message, callerProfile, stackTraceDepth); if (exception == null) { InternalLogger.Info(fullLogMessage); AddMessageToRecentLogs(fullLogMessage); } else { InternalLogger.Info(fullLogMessage, exception); AddMessageToRecentLogs(fullLogMessage, exception); } } public void Warn( string message, Exception exception = null, int stackTraceDepth = 0, [CallerFilePath] string sourceFilePath = "", [CallerMemberName] string sourceMemberName = "", [CallerLineNumber] int sourceLineNumber = 0) where TEvent : ILogEvent, new() { CallerProfile callerProfile = new(sourceFilePath, sourceMemberName, sourceLineNumber); string fullLogMessage = CreateFullLogMessage(message, callerProfile, stackTraceDepth); if (exception == null) { InternalLogger.Warn(fullLogMessage); AddMessageToRecentLogs(fullLogMessage); } else { InternalLogger.Warn(fullLogMessage, exception); AddMessageToRecentLogs(fullLogMessage, exception); } } public void Error( string message, Exception exception = null, int stackTraceDepth = 0, [CallerFilePath] string sourceFilePath = "", [CallerMemberName] string sourceMemberName = "", [CallerLineNumber] int sourceLineNumber = 0) where TEvent : ILogEvent, new() { CallerProfile callerProfile = new(sourceFilePath, sourceMemberName, sourceLineNumber); string fullLogMessage = CreateFullLogMessage(message, callerProfile, stackTraceDepth); if (exception == null) { InternalLogger.Error(fullLogMessage); AddMessageToRecentLogs(fullLogMessage); } else { InternalLogger.Error(fullLogMessage, exception); AddMessageToRecentLogs(fullLogMessage, exception); } } public void Fatal( string message, Exception exception = null, int stackTraceDepth = 0, [CallerFilePath] string sourceFilePath = "", [CallerMemberName] string sourceMemberName = "", [CallerLineNumber] int sourceLineNumber = 0) where TEvent : ILogEvent, new() { CallerProfile callerProfile = new(sourceFilePath, sourceMemberName, sourceLineNumber); string fullLogMessage = CreateFullLogMessage(message, callerProfile, stackTraceDepth); if (exception == null) { InternalLogger.Fatal(fullLogMessage); AddMessageToRecentLogs(fullLogMessage); } else { InternalLogger.Fatal(fullLogMessage, exception); AddMessageToRecentLogs(fullLogMessage, exception); } } private string CreateFullLogMessage(string message, CallerProfile callerProfile, int stackTraceDepth = 0) where TEvent : ILogEvent, new() { string json = GenerateMetadataJson(callerProfile, stackTraceDepth); return $"{new TEvent()} | {message} | {json}"; } private string GenerateMetadataJson(CallerProfile callerProfile, int stackTraceDepth = 0) { IDictionary metadataDictionary = new Dictionary(); string callerInfo = $"{callerProfile.SourceClassName}.{callerProfile.SourceMemberName}:{callerProfile.SourceLineNumber}"; if (stackTraceDepth > 0) { string[] stackFrames = CaptureStackTrace(stackTraceDepth); if (stackFrames.Length > 0) { callerInfo = $"{callerInfo} < {string.Join(" < ", stackFrames)}"; } } metadataDictionary.Add("Caller", callerInfo); return JsonConvert.SerializeObject(metadataDictionary); } private static string[] CaptureStackTrace(int depth) { // Skip: CaptureStackTrace, GenerateMetadataJson, CreateFullLogMessage, logging method (Info/Warn/etc), and the actual caller (already in CallerProfile) StackTrace stackTrace = new(skipFrames: 5, fNeedFileInfo: true); List frames = new(); for (int i = 0; i < stackTrace.FrameCount && frames.Count < depth; i++) { if (stackTrace.GetFrame(i) is not StackFrame frame || frame.GetMethod() is not MethodBase method) { continue; } // Filter out framework code, compiler-generated code, and other non-application code if (!IsApplicationCode(method)) { continue; } int lineNumber = frame.GetFileLineNumber(); string className = method.DeclaringType?.Name ?? "Unknown"; string methodName = method.Name; if (lineNumber > 0) { frames.Add($"{className}.{methodName}:{lineNumber}"); } else { frames.Add($"{className}.{methodName}"); } } return [.. frames]; } private static bool IsApplicationCode(MethodBase method) { if (method.DeclaringType == null) { return false; } string typeNamespace = method.DeclaringType.Namespace ?? string.Empty; // Only include ProtonVPN application code if (!typeNamespace.StartsWith("ProtonVPN", StringComparison.Ordinal)) { return false; } // Exclude compiler-generated code if (method.DeclaringType.IsDefined(typeof(CompilerGeneratedAttribute), false) || method.IsDefined(typeof(CompilerGeneratedAttribute), false)) { return false; } return true; } private void AddMessageToRecentLogs(string message, Exception exception = null, [CallerMemberName] string level = "") { message = $"{DateTime.UtcNow:O} | {level.ToUpper()} | {message}"; if (exception != null) { message += $" {exception}"; } _recentLogs.Add(message); while (_recentLogs.Count > MAX_RECENT_LOG_LINES) { _recentLogs.RemoveAt(0); } } }