Add app log in Diagnostics screen (#234)
This commit is contained in:
parent
fbc17877b1
commit
54c53707e0
|
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- IVPN provider.
|
||||
- OpenVPN: Support for `--route-nopull`. [#230](https://github.com/passepartoutvpn/passepartout-apple/pull/230)
|
||||
- App log in Diagnostics screen. [#234](https://github.com/passepartoutvpn/passepartout-apple/pull/234)
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -140,36 +140,40 @@ extension Constants {
|
|||
}
|
||||
|
||||
enum Log {
|
||||
enum App {
|
||||
static let url = containerURL(filename: "App.log")
|
||||
|
||||
static let format = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M"
|
||||
}
|
||||
|
||||
enum Tunnel {
|
||||
static let path = containerPath(filename: "Tunnel.log")
|
||||
|
||||
static let format = "$DHH:mm:ss$d - $M"
|
||||
}
|
||||
|
||||
private static let parentPath = "Library/Caches"
|
||||
|
||||
private static func containerLogURL(filename: String) -> URL {
|
||||
Files.containerURL
|
||||
.appendingPathComponent(parentPath)
|
||||
.appendingPathComponent(filename)
|
||||
}
|
||||
|
||||
private static func containerLogPath(filename: String) -> String {
|
||||
"\(parentPath)/\(filename)"
|
||||
}
|
||||
|
||||
static let appLogURL = containerLogURL(filename: "App.log")
|
||||
|
||||
static let tunnelLogPath = containerLogPath(filename: "Tunnel.log")
|
||||
|
||||
static let logLevel: SwiftyBeaver.Level = {
|
||||
static let level: SwiftyBeaver.Level = {
|
||||
guard let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"], let levelNum = Int(levelString) else {
|
||||
return .info
|
||||
}
|
||||
return .init(rawValue: levelNum) ?? .info
|
||||
}()
|
||||
|
||||
static let logFormat = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M"
|
||||
static let maxBytes = 100000
|
||||
|
||||
static let tunnelLogFormat = "$DHH:mm:ss$d - $M"
|
||||
|
||||
static let tunnelLogMaxBytes = 100000
|
||||
|
||||
static let tunnelLogRefreshInterval: TimeInterval = 5.0
|
||||
static let refreshInterval: TimeInterval = 5.0
|
||||
|
||||
private static func containerURL(filename: String) -> URL {
|
||||
Files.containerURL
|
||||
.appendingPathComponent(parentPath)
|
||||
.appendingPathComponent(filename)
|
||||
}
|
||||
|
||||
private static func containerPath(filename: String) -> String {
|
||||
"\(parentPath)/\(filename)"
|
||||
}
|
||||
}
|
||||
|
||||
enum URLs {
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import PassepartoutLibrary
|
||||
|
||||
extension AppContext {
|
||||
static let shared = AppContext(coreContext: .shared)
|
||||
|
@ -32,3 +33,7 @@ extension AppContext {
|
|||
extension ProductManager {
|
||||
static let shared = AppContext.shared.productManager
|
||||
}
|
||||
|
||||
extension LogManager {
|
||||
static let shared = AppContext.shared.logManager
|
||||
}
|
||||
|
|
|
@ -29,29 +29,29 @@ import PassepartoutLibrary
|
|||
|
||||
@MainActor
|
||||
class AppContext {
|
||||
private let logManager: LogManager
|
||||
|
||||
private let reviewer: Reviewer
|
||||
let logManager: LogManager
|
||||
|
||||
let productManager: ProductManager
|
||||
|
||||
private let reviewer: Reviewer
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
init(coreContext: CoreContext) {
|
||||
logManager = LogManager(logFile: Constants.Log.appLogURL)
|
||||
logManager.logLevel = Constants.Log.logLevel
|
||||
logManager.logFormat = Constants.Log.logFormat
|
||||
logManager = LogManager(logFile: Constants.Log.App.url)
|
||||
logManager.logLevel = Constants.Log.level
|
||||
logManager.logFormat = Constants.Log.App.format
|
||||
logManager.configureLogging()
|
||||
pp_log.info("Logging to: \(logManager.logFile!)")
|
||||
|
||||
reviewer = Reviewer()
|
||||
reviewer.eventCountBeforeRating = Constants.Rating.eventCount
|
||||
|
||||
productManager = ProductManager(
|
||||
appType: Constants.InApp.appType,
|
||||
buildProducts: Constants.InApp.buildProducts
|
||||
)
|
||||
|
||||
reviewer = Reviewer()
|
||||
reviewer.eventCountBeforeRating = Constants.Rating.eventCount
|
||||
|
||||
// post
|
||||
|
||||
configureObjects(coreContext: coreContext)
|
||||
|
|
|
@ -28,6 +28,8 @@ import Combine
|
|||
import PassepartoutLibrary
|
||||
|
||||
struct DebugLogView: View {
|
||||
private let title: String
|
||||
|
||||
private let url: URL
|
||||
|
||||
private let timer: AnyPublisher<Date, Never>
|
||||
|
@ -36,7 +38,7 @@ struct DebugLogView: View {
|
|||
|
||||
@State private var isSharing = false
|
||||
|
||||
private let maxBytes = UInt64(Constants.Log.tunnelLogMaxBytes)
|
||||
private let maxBytes = UInt64(Constants.Log.maxBytes)
|
||||
|
||||
private let appName = Constants.Global.appName
|
||||
|
||||
|
@ -44,11 +46,17 @@ struct DebugLogView: View {
|
|||
|
||||
private let shareFilename = Unlocalized.Issues.Filenames.debugLog
|
||||
|
||||
init(url: URL, updateInterval: TimeInterval) {
|
||||
init(title: String, url: URL, refreshInterval: TimeInterval?) {
|
||||
self.title = title
|
||||
self.url = url
|
||||
timer = Timer.TimerPublisher(interval: updateInterval, runLoop: .main, mode: .common)
|
||||
.autoconnect()
|
||||
.eraseToAnyPublisher()
|
||||
if let refreshInterval = refreshInterval {
|
||||
timer = Timer.TimerPublisher(interval: refreshInterval, runLoop: .main, mode: .common)
|
||||
.autoconnect()
|
||||
.eraseToAnyPublisher()
|
||||
} else {
|
||||
timer = Empty(outputType: Date.self, failureType: Never.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -78,7 +86,7 @@ struct DebugLogView: View {
|
|||
#endif
|
||||
.edgesIgnoringSafeArea([.leading, .trailing])
|
||||
.onReceive(timer, perform: refreshLog)
|
||||
.navigationTitle(L10n.DebugLog.title)
|
||||
.navigationTitle(title)
|
||||
.themeDebugLogStyle()
|
||||
}
|
||||
|
||||
|
|
|
@ -57,8 +57,6 @@ extension DiagnosticsView {
|
|||
|
||||
private let vpnProtocol: VPNProtocolType = .openVPN
|
||||
|
||||
private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval
|
||||
|
||||
init(providerName: ProviderName?) {
|
||||
providerManager = .shared
|
||||
vpnManager = .shared
|
||||
|
@ -108,16 +106,10 @@ extension DiagnosticsView {
|
|||
|
||||
private var debugLogSection: some View {
|
||||
Section {
|
||||
let url = debugLogURL
|
||||
NavigationLink(L10n.DebugLog.title) {
|
||||
url.map {
|
||||
DebugLogView(
|
||||
url: $0,
|
||||
updateInterval: logUpdateInterval
|
||||
)
|
||||
}
|
||||
}.disabled(url == nil)
|
||||
DebugLogSection(appLogURL: appLogURL, tunnelLogURL: tunnelLogURL)
|
||||
Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $vpnManager.masksPrivateData)
|
||||
} header: {
|
||||
Text(L10n.DebugLog.title)
|
||||
} footer: {
|
||||
Text(L10n.Diagnostics.Sections.DebugLog.footer)
|
||||
}
|
||||
|
@ -160,8 +152,12 @@ extension DiagnosticsView.OpenVPNView {
|
|||
// "withFallbacks: false" for view to hide nil options
|
||||
return cfg.builder(withFallbacks: false)
|
||||
}
|
||||
|
||||
private var debugLogURL: URL? {
|
||||
|
||||
private var appLogURL: URL? {
|
||||
LogManager.shared.logFile
|
||||
}
|
||||
|
||||
private var tunnelLogURL: URL? {
|
||||
vpnManager.debugLogURL(forProtocol: vpnProtocol)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,6 @@ extension DiagnosticsView {
|
|||
|
||||
private let providerName: ProviderName?
|
||||
|
||||
private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval
|
||||
|
||||
init(providerName: ProviderName?) {
|
||||
vpnManager = .shared
|
||||
self.providerName = providerName
|
||||
|
@ -43,21 +41,21 @@ extension DiagnosticsView {
|
|||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
let url = debugLogURL
|
||||
NavigationLink(L10n.DebugLog.title) {
|
||||
url.map {
|
||||
DebugLogView(
|
||||
url: $0,
|
||||
updateInterval: logUpdateInterval
|
||||
)
|
||||
}
|
||||
}.disabled(url == nil)
|
||||
DebugLogSection(appLogURL: appLogURL, tunnelLogURL: tunnelLogURL)
|
||||
} header: {
|
||||
Text(L10n.DebugLog.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var debugLogURL: URL? {
|
||||
vpnManager.debugLogURL(forProtocol: .wireGuard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DiagnosticsView.WireGuardView {
|
||||
private var appLogURL: URL? {
|
||||
LogManager.shared.logFile
|
||||
}
|
||||
|
||||
private var tunnelLogURL: URL? {
|
||||
vpnManager.debugLogURL(forProtocol: .wireGuard)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,3 +47,46 @@ struct DiagnosticsView: View {
|
|||
}.navigationTitle(L10n.Diagnostics.title)
|
||||
}
|
||||
}
|
||||
|
||||
extension DiagnosticsView {
|
||||
struct DebugLogSection: View {
|
||||
let appLogURL: URL?
|
||||
|
||||
let tunnelLogURL: URL?
|
||||
|
||||
private let refreshInterval = Constants.Log.refreshInterval
|
||||
|
||||
var body: some View {
|
||||
appLink
|
||||
tunnelLink
|
||||
}
|
||||
|
||||
private var appLink: some View {
|
||||
navigationLink(
|
||||
withTitle: L10n.Diagnostics.Items.AppLog.title,
|
||||
url: appLogURL,
|
||||
refreshInterval: nil
|
||||
)
|
||||
}
|
||||
|
||||
private var tunnelLink: some View {
|
||||
navigationLink(
|
||||
withTitle: Unlocalized.VPN.vpn,
|
||||
url: tunnelLogURL,
|
||||
refreshInterval: refreshInterval
|
||||
)
|
||||
}
|
||||
|
||||
private func navigationLink(withTitle title: String, url: URL?, refreshInterval: TimeInterval?) -> some View {
|
||||
NavigationLink(title) {
|
||||
url.map {
|
||||
DebugLogView(
|
||||
title: title,
|
||||
url: $0,
|
||||
refreshInterval: refreshInterval
|
||||
)
|
||||
}
|
||||
}.disabled(url == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -210,6 +210,10 @@ internal enum L10n {
|
|||
}
|
||||
}
|
||||
internal enum Items {
|
||||
internal enum AppLog {
|
||||
/// App
|
||||
internal static let title = L10n.tr("Localizable", "diagnostics.items.app_log.title", fallback: "App")
|
||||
}
|
||||
internal enum MasksPrivateData {
|
||||
/// Mask network data
|
||||
internal static let caption = L10n.tr("Localizable", "diagnostics.items.masks_private_data.caption", fallback: "Mask network data")
|
||||
|
|
|
@ -114,8 +114,8 @@ class CoreContext {
|
|||
|
||||
private func configureObjects() {
|
||||
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
|
||||
vpnManager.tunnelLogPath = Constants.Log.tunnelLogPath
|
||||
vpnManager.tunnelLogFormat = Constants.Log.tunnelLogFormat
|
||||
vpnManager.tunnelLogPath = Constants.Log.Tunnel.path
|
||||
vpnManager.tunnelLogFormat = Constants.Log.Tunnel.format
|
||||
|
||||
profileManager.observeUpdates()
|
||||
vpnManager.observeUpdates()
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Diagnose";
|
||||
"diagnostics.sections.debug_log.footer" = "Zensier-Status wird aktiv nach erneutem Verbinden. Netzwerk-Daten sind Hostnamen, IP-Adressen, Routingtabellen, SSID. Zugangsdaten und Private Keys werden nie gelogged.";
|
||||
"diagnostics.items.server_configuration.caption" = "Serverkonfiguration";
|
||||
"diagnostics.items.app_log.title" = "App";
|
||||
"diagnostics.items.masks_private_data.caption" = "Netzwerkdaten zensieren";
|
||||
"diagnostics.items.report_issue.caption" = "Verbindungsproblem melden";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Διαγνωστικά";
|
||||
"diagnostics.sections.debug_log.footer" = "Η κατάσταση κάλυψης θα είναι αποτελεσματική μετά την επανασύνδεση. Τα δεδομένα δικτύου είναι του διακομιστή, διευθύνσεις IP, δρομολόγηση και SSID. Τα διαπιστευτήρια και τα ιδιωτικά κλειδιά δεν καταγράφονται ανεξάρτητα.";
|
||||
"diagnostics.items.server_configuration.caption" = "Ρυθμίσεις Διακομιστή";
|
||||
"diagnostics.items.app_log.title" = "Εφαρμογή";
|
||||
"diagnostics.items.masks_private_data.caption" = "Μάσκα δεδομένα δικτύου";
|
||||
"diagnostics.items.report_issue.caption" = "Αναφορά ζητήματος συνδεσιμότητας";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Diagnostics";
|
||||
"diagnostics.sections.debug_log.footer" = "Masking status will be effective after reconnecting. Network data are hostnames, IP addresses, routing, SSID. Credentials and private keys are not logged regardless.";
|
||||
"diagnostics.items.server_configuration.caption" = "Server configuration";
|
||||
"diagnostics.items.app_log.title" = "App";
|
||||
"diagnostics.items.masks_private_data.caption" = "Mask network data";
|
||||
"diagnostics.items.report_issue.caption" = "Report connectivity issue";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Diagnósticos";
|
||||
"diagnostics.sections.debug_log.footer" = "El estado de ocultación será efectivo tras reconectar. Los datos de red son hostnames, direcciones IP, routing, SSID. Las credenciales y las claves privadas no son registrados a pesar.";
|
||||
"diagnostics.items.server_configuration.caption" = "Configuración del servidor";
|
||||
"diagnostics.items.app_log.title" = "App";
|
||||
"diagnostics.items.masks_private_data.caption" = "Ocultar datos de red";
|
||||
"diagnostics.items.report_issue.caption" = "Reportar problema de conectividad";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Diagnostiques";
|
||||
"diagnostics.sections.debug_log.footer" = "Camouflage du status sera effectif après la reconnection. Les données réseaux sont les noms d'hôtes, adresses IP, routage, SSID. Les identifiants et clés privés ne sont pas enregistrés.";
|
||||
"diagnostics.items.server_configuration.caption" = "Configuration serveur";
|
||||
"diagnostics.items.app_log.title" = "Application";
|
||||
"diagnostics.items.masks_private_data.caption" = "Masquer les données de réseau";
|
||||
"diagnostics.items.report_issue.caption" = "Rapporter un problème de connection";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Diagnostica";
|
||||
"diagnostics.sections.debug_log.footer" = "Il mascheramento sarà effettivo dopo una riconnessione. I dati di rete sono hostname, indirizzi IP, routing, SSID. Credenziali e chiavi private non sono registrati in ogni caso.";
|
||||
"diagnostics.items.server_configuration.caption" = "Configurazione del server";
|
||||
"diagnostics.items.app_log.title" = "App";
|
||||
"diagnostics.items.masks_private_data.caption" = "Maschera dati rete";
|
||||
"diagnostics.items.report_issue.caption" = "Segnala problema connettività";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Diagnose";
|
||||
"diagnostics.sections.debug_log.footer" = "De maskeerstatus is effectief na opnieuw verbinden. Netwerkgegevens zijn hostnamen, IP-adressen, routing, SSID's. Inloggegevens en privésleutels worden niet geregistreerd.";
|
||||
"diagnostics.items.server_configuration.caption" = "Server configuratie";
|
||||
"diagnostics.items.app_log.title" = "App";
|
||||
"diagnostics.items.masks_private_data.caption" = "Netwerkgegevens maskeren";
|
||||
"diagnostics.items.report_issue.caption" = "Probleem met connectiviteit melden";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Diagnostyka";
|
||||
"diagnostics.sections.debug_log.footer" = "Status maskowania będzie widoczny po ponownym połączeniu. Dane połączenia to nazwy hostów, adresy IP, routing, SSID. Loginy i klucze prywatne nie są zapisywane.";
|
||||
"diagnostics.items.server_configuration.caption" = "Konfiguracja serwera";
|
||||
"diagnostics.items.app_log.title" = "Aplikacja";
|
||||
"diagnostics.items.masks_private_data.caption" = "Maskuj dane sieci";
|
||||
"diagnostics.items.report_issue.caption" = "Zgłoś problemy z połączeniem";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Diagnóstico";
|
||||
"diagnostics.sections.debug_log.footer" = "O status será escondido após reconectado. Os dados da rede são hostnames, endereços de IP, rotas, SSID. Credenciais e chaves privadas não será logadas em nenhum dos casos.";
|
||||
"diagnostics.items.server_configuration.caption" = "Configuração do servidor";
|
||||
"diagnostics.items.app_log.title" = "Aplicativo";
|
||||
"diagnostics.items.masks_private_data.caption" = "Esconder dados da rede";
|
||||
"diagnostics.items.report_issue.caption" = "Reportar problemas de conexão";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Диагностика";
|
||||
"diagnostics.sections.debug_log.footer" = "Маскировка включится после повторного подключения. Информация о сети - это названия хост профилей, IP адрес, маршрутизация и SSID. Данные для входа и приватные ключи не собираются.";
|
||||
"diagnostics.items.server_configuration.caption" = "Конфигурация сервера";
|
||||
"diagnostics.items.app_log.title" = "Приложение";
|
||||
"diagnostics.items.masks_private_data.caption" = "Маскировать информацию сети";
|
||||
"diagnostics.items.report_issue.caption" = "Сообщить о проблеме подкл.";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "Diagnostics";
|
||||
"diagnostics.sections.debug_log.footer" = "Masking status kommer att fungera efter återanslutning. Nätverksdata är värdnamn, IP-adresser, routing, SSID. Referenser och privata nycklar loggas inte oavsett.";
|
||||
"diagnostics.items.server_configuration.caption" = "Server konfiguration";
|
||||
"diagnostics.items.app_log.title" = "App";
|
||||
"diagnostics.items.masks_private_data.caption" = "Mask nätverksdata";
|
||||
"diagnostics.items.report_issue.caption" = "Rapportera anslutningsproblem";
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
"diagnostics.title" = "分析数据";
|
||||
"diagnostics.sections.debug_log.footer" = "在重连后状态隐藏才有效。网络数据包括主机名、IP地址、路由、SSID、认证方式,但私钥不会出现在日志中。";
|
||||
"diagnostics.items.server_configuration.caption" = "服务端配置";
|
||||
"diagnostics.items.app_log.title" = "应用程序";
|
||||
"diagnostics.items.masks_private_data.caption" = "隐藏网络数据";
|
||||
"diagnostics.items.report_issue.caption" = "报告连接问题";
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
import Foundation
|
||||
import SwiftyBeaver
|
||||
|
||||
@MainActor
|
||||
public class LogManager {
|
||||
public let logFile: URL?
|
||||
|
||||
|
|
Loading…
Reference in New Issue