Improve debug logs and move methods to library (#1076)

- Move availableLogs() / purgeLogs() to library
- Append and rotate logs by size (500k)
- Add marker between app/tunnel launches
- Purge logs on each save (3 days)
- Unify debug log content view across platforms
    - macOS: Table + inspect full line
    - iOS/tvOS: Use List
    - Scroll to bottom onLoad()
This commit is contained in:
Davide 2025-01-19 00:42:58 +01:00 committed by GitHub
parent a6e44872e9
commit bd9f8d63a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 157 additions and 187 deletions

View File

@ -1,39 +0,0 @@
//
// DebugLogContentView+iOS.swift
// Passepartout
//
// Created by Davide De Rosa on 8/31/24.
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout 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.
//
// Passepartout 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 Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if os(iOS)
import SwiftUI
struct DebugLogContentView: View {
let lines: [String]
var body: some View {
TextEditor(text: .constant(lines.joined(separator: "\n")))
.font(.caption)
}
}
#endif

View File

@ -1,42 +0,0 @@
//
// DebugLogContentView+macOS.swift
// Passepartout
//
// Created by Davide De Rosa on 8/31/24.
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout 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.
//
// Passepartout 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 Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if os(macOS)
import SwiftUI
struct DebugLogContentView: View {
let lines: [String]
var body: some View {
List {
ForEach(Array(lines.enumerated()), id: \.offset) {
Text($0.element)
}
}
}
}
#endif

View File

@ -1,45 +0,0 @@
//
// DebugLogContentView.swift
// Passepartout
//
// Created by Davide De Rosa on 11/23/24.
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout 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.
//
// Passepartout 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 Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import SwiftUI
struct DebugLogContentView: View {
let lines: [String]
var body: some View {
ScrollView {
LazyVStack {
ForEach(Array(lines.enumerated()), id: \.offset) {
Button($0.element) {
//
}
.buttonStyle(.plain)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.caption)
}
}
}
}
}

View File

@ -122,7 +122,7 @@ extension ExtendedTunnel {
public func currentLog(parameters: Constants.Log) async -> [String] { public func currentLog(parameters: Constants.Log) async -> [String] {
let output = try? await tunnel.sendMessage(.localLog( let output = try? await tunnel.sendMessage(.localLog(
sinceLast: parameters.sinceLast, sinceLast: parameters.sinceLast,
maxLevel: parameters.maxLevel maxLevel: parameters.options.maxLevel
)) ))
switch output { switch output {
case .debugLog(let log): case .debugLog(let log):

View File

@ -145,9 +145,7 @@ public struct Constants: Decodable, Sendable {
public let sinceLast: TimeInterval public let sinceLast: TimeInterval
public let maxLevel: DebugLog.Level public let options: LocalLogger.Options
public let maxNumberOfLines: Int
public let maxAge: TimeInterval? public let maxAge: TimeInterval?
} }

View File

@ -30,67 +30,27 @@ extension PassepartoutConfiguration {
public func configureLogging(to url: URL, parameters: Constants.Log, logsPrivateData: Bool) { public func configureLogging(to url: URL, parameters: Constants.Log, logsPrivateData: Bool) {
pp_log(.app, .debug, "Log to: \(url)") pp_log(.app, .debug, "Log to: \(url)")
setLocalLogger(options: .init( setLocalLogger(
url: url, url: url,
maxNumberOfLines: parameters.maxNumberOfLines, options: parameters.options,
maxLevel: parameters.maxLevel,
mapper: parameters.formatter.formattedLine mapper: parameters.formatter.formattedLine
)) )
if logsPrivateData { if logsPrivateData {
logsAddresses = true logsAddresses = true
logsModules = true logsModules = true
} }
if let maxAge = parameters.maxAge { appendLog(parameters.options.maxLevel, message: "")
purgeLogs(at: url, beyond: maxAge) appendLog(parameters.options.maxLevel, message: "--- BEGIN ---")
} appendLog(parameters.options.maxLevel, message: "")
} }
public func currentLog(parameters: Constants.Log) -> [String] { public func currentLog(parameters: Constants.Log) -> [String] {
currentLogLines( currentLogLines(
sinceLast: parameters.sinceLast, sinceLast: parameters.sinceLast,
maxLevel: parameters.maxLevel maxLevel: parameters.options.maxLevel
) )
.map(parameters.formatter.formattedLine) .map(parameters.formatter.formattedLine)
} }
public func availableLogs(at url: URL) -> [Date: URL] {
let parent = url.deletingLastPathComponent()
let prefix = url.lastPathComponent
do {
let contents = try FileManager.default.contentsOfDirectory(at: parent, includingPropertiesForKeys: nil)
return contents.reduce(into: [:]) { found, item in
let filename = item.lastPathComponent
guard filename.hasPrefix(prefix) else {
return
}
guard let timestampString = filename.split(separator: ".").last,
let timestamp = TimeInterval(timestampString) else {
return
}
let date = Date(timeIntervalSince1970: timestamp)
found[date] = item
}
} catch {
return [:]
}
}
public func flushLog() {
try? saveLog()
}
}
private extension PassepartoutConfiguration {
func purgeLogs(at url: URL, beyond maxAge: TimeInterval) {
let logs = availableLogs(at: url)
let minDate = Date().addingTimeInterval(-maxAge)
logs.forEach { date, url in
guard date >= minDate else {
try? FileManager.default.removeItem(at: url)
return
}
}
}
} }

View File

@ -37,9 +37,12 @@
"appPath": "app.log", "appPath": "app.log",
"tunnelPath": "tunnel.log", "tunnelPath": "tunnel.log",
"sinceLast": 86400, "sinceLast": 86400,
"maxLevel": 3, "options": {
"maxNumberOfLines": 10000, "maxLevel": 3,
"maxAge": 604800, "maxSize": 500000,
"maxBufferedLines": 5000,
"maxAge": 259200
},
"formatter": { "formatter": {
"timestamp": "HH:mm:ss", "timestamp": "HH:mm:ss",
"message": "%@ - %@" "message": "%@ - %@"

View File

@ -266,8 +266,23 @@ extension View {
modifier(ThemeHoverListRowModifier()) modifier(ThemeHoverListRowModifier())
} }
public func themeTip(_ text: String, edge: Edge) -> some View { public func themeTip<Label>(
modifier(ThemeTipModifier(text: text, edge: edge)) _ text: String,
edge: Edge,
width: Double = 150.0,
alignment: Alignment = .center,
label: @escaping () -> Label = {
ThemeImage(.tip)
.imageScale(.large)
}
) -> some View where Label: View {
modifier(ThemeTipModifier(
text: text,
edge: edge,
width: width,
alignment: alignment,
label: label
))
} }
#endif #endif
} }
@ -617,11 +632,17 @@ struct ThemeLockScreenModifier<LockedContent>: ViewModifier where LockedContent:
} }
} }
struct ThemeTipModifier: ViewModifier { struct ThemeTipModifier<Label>: ViewModifier where Label: View {
let text: String let text: String
let edge: Edge let edge: Edge
let width: Double
let alignment: Alignment
let label: () -> Label
@State @State
private var isPresenting = false private var isPresenting = false
@ -631,9 +652,8 @@ struct ThemeTipModifier: ViewModifier {
Button { Button {
isPresenting = true isPresenting = true
} label: { } label: {
ThemeImage(.tip) label()
} }
.imageScale(.large)
.buttonStyle(.borderless) .buttonStyle(.borderless)
.popover(isPresented: $isPresenting, arrowEdge: edge) { .popover(isPresented: $isPresenting, arrowEdge: edge) {
VStack { VStack {
@ -642,7 +662,7 @@ struct ThemeTipModifier: ViewModifier {
.foregroundStyle(.primary) .foregroundStyle(.primary)
.lineLimit(nil) .lineLimit(nil)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.frame(width: 150.0) .frame(width: width, alignment: alignment)
} }
.padding(12) .padding(12)
} }

View File

@ -0,0 +1,110 @@
//
// DebugLogContentView.swift
// Passepartout
//
// Created by Davide De Rosa on 11/23/24.
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout 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.
//
// Passepartout 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 Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import SwiftUI
public struct DebugLogContentView: View {
@EnvironmentObject
private var theme: Theme
public let lines: [String]
public init(lines: [String]) {
self.lines = lines
}
public var body: some View {
ScrollViewReader { proxy in
scrollView
.onLoad {
withAnimation {
proxy.scrollTo(lines.count - 1, anchor: .bottom)
}
}
}
}
}
#if os(macOS)
private extension DebugLogContentView {
struct Entry: Identifiable {
let id: Int
let line: String
}
var scrollView: some View {
let entries = lines
.enumerated()
.map {
Entry.init(id: $0.offset, line: $0.element)
}
return Table(entries) {
TableColumn("") { entry in
HStack {
EmptyView()
.themeTip(entry.line, edge: .bottom, width: 400.0, alignment: .leading) {
ThemeImage(.search)
}
.environmentObject(theme) // TODO: #873, Table loses environment
Text(entry.line)
.font(.caption)
}
}
}
.withoutColumnHeaders()
}
}
#else
private extension DebugLogContentView {
var scrollView: some View {
List(lines.indices, id: \.self, rowContent: entryView)
.listStyle(.plain)
}
func entryView(for index: Int) -> some View {
Text(lines[index])
.themeMultiLine(true)
.scrollableOnTV()
.buttonStyle(.plain)
.font(.caption)
.frame(maxWidth: .infinity, alignment: .leading)
.id(index)
}
}
#endif
#Preview {
DebugLogContentView(
lines: Array(repeating: "foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar ", count: 200)
)
}

@ -1 +1 @@
Subproject commit f6d85fdf1e186fa13c820166f3a414962bcc52c1 Subproject commit b140470e9c2567fcb5e7053b030c1df509d7cd24

View File

@ -124,6 +124,11 @@
value = "1" value = "1"
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable
key = "SWIFTUI_ATTRIBUTEDGRAPH_DEBUG"
value = "1"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
<StoreKitConfigurationFileReference <StoreKitConfigurationFileReference
identifier = "../../Passepartout/Passepartout.storekit"> identifier = "../../Passepartout/Passepartout.storekit">