TM-SGNL-iOS/SignalServiceKit/DebugLogs/DebugLogger.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

289 lines
9.5 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import AudioToolbox
import CocoaLumberjack
import LibSignalClient
import SignalRingRTC
private final class DebugLogFileManager: DDLogFileManagerDefault {
private static func deleteLogFiles(inDirectory logsDirPath: String, olderThanDate cutoffDate: Date) {
let logsDirectory = URL(fileURLWithPath: logsDirPath)
let fileManager = FileManager.default
guard let logFiles = try? fileManager.contentsOfDirectory(at: logsDirectory, includingPropertiesForKeys: [kCFURLContentModificationDateKey as URLResourceKey], options: .skipsHiddenFiles) else {
return
}
for logFile in logFiles {
guard logFile.pathExtension == "log" else {
// This file is not a log file; don't touch it.
continue
}
var lastModified: Date?
do {
var lastModifiedAnyObject: AnyObject?
try (logFile as NSURL).getResourceValue(&lastModifiedAnyObject, forKey: .contentModificationDateKey)
if let mtime = lastModifiedAnyObject as? NSDate {
lastModified = mtime as Date
}
} catch {
// Couldn't get the modification date.
continue
}
guard let lastModified else {
// retrieving last modification date didn't throw but didn't return NSDate type
continue
}
if lastModified.isAfter(cutoffDate) {
// Still within the window.
continue
}
// Attempt to remove the item, but don't stress if it fails.
try? fileManager.removeItem(at: logFile)
}
}
override func didArchiveLogFile(atPath logFilePath: String, wasRolled: Bool) {
guard CurrentAppContext().isMainApp else {
return
}
// Use this opportunity to delete old log files from extensions as well.
// Compute an approximate "N days ago", ignoring calendars and dayling savings changes.
let cutoffDate = Date(timeIntervalSinceNow: -kDayInterval * Double(maximumNumberOfLogFiles))
for logsDirPath in DebugLogger.allLogsDirPaths {
Self.deleteLogFiles(inDirectory: logsDirPath, olderThanDate: cutoffDate)
}
}
}
public final class ErrorLogger: DDFileLogger {
public static func playAlertSound() {
AudioServicesPlayAlertSound(SystemSoundID(1023))
}
public override func log(message logMessage: DDLogMessage) {
super.log(message: logMessage)
if Preferences.isAudibleErrorLoggingEnabled {
Self.playAlertSound()
}
}
}
public final class DebugLogger {
private init() {}
public static var shared = DebugLogger()
public static let mainAppDebugLogsDirPath = {
let dirPath = OWSFileSystem.cachesDirectoryPath().appendingPathComponent("Logs")
OWSFileSystem.ensureDirectoryExists(dirPath)
return dirPath
}()
public static let shareExtensionDebugLogsDirPath = {
let dirPath = OWSFileSystem.appSharedDataDirectoryPath().appendingPathComponent("ShareExtensionLogs")
OWSFileSystem.ensureDirectoryExists(dirPath)
return dirPath
}()
public static let nseDebugLogsDirPath = {
let dirPath = OWSFileSystem.appSharedDataDirectoryPath().appendingPathComponent("NSELogs")
OWSFileSystem.ensureDirectoryExists(dirPath)
return dirPath
}()
#if TESTABLE_BUILD
public static let testDebugLogsDirPath = TestAppContext.testDebugLogsDirPath
#endif
// We don't need to include testDebugLogsDirPath when we upload debug logs.
public static let allLogsDirPaths: [String] = [
DebugLogger.mainAppDebugLogsDirPath,
DebugLogger.shareExtensionDebugLogsDirPath,
DebugLogger.nseDebugLogsDirPath,
]
public static let errorLogsDir = URL.init(fileURLWithPath: OWSFileSystem.cachesDirectoryPath().appendingPathComponent("ErrorLogs"))
public var fileLogger: DDFileLogger?
public var allLogFilePaths: Set<String> {
let fileManager = FileManager.default
var logPathSet = Set<String>()
for logDirPath in DebugLogger.allLogsDirPaths {
do {
for filename in try fileManager.contentsOfDirectory(atPath: logDirPath) {
let logPath = logDirPath.appendingPathComponent(filename)
logPathSet.insert(logPath)
}
} catch {
owsFailDebug("Failed to find log files: \(error)")
}
}
// To be extra conservative, also add all logs from log file manager.
// This should be redundant with the logic above.
if let fileLogger {
logPathSet.formUnion(fileLogger.logFileManager.unsortedLogFilePaths)
}
return logPathSet
}
public func enableErrorReporting() {
let errorLogger = ErrorLogger(logFileManager: DDLogFileManagerDefault(logsDirectory: Self.errorLogsDir.path))
errorLogger.logFormatter = ScrubbingLogFormatter()
DDLog.add(errorLogger, with: .error)
}
// MARK: Enable/Disable
public func enableFileLogging(appContext: AppContext, canLaunchInBackground: Bool) {
let logsDirPath = appContext.debugLogsDirPath
let logFileManager = DebugLogFileManager(
logsDirectory: logsDirPath,
defaultFileProtectionLevel: canLaunchInBackground ? .completeUntilFirstUserAuthentication : .completeUnlessOpen
)
// Keep last 3 days of logs - or last 3 logs (if logs rollover due to max
// file size). Keep extra log files in internal builds.
logFileManager.maximumNumberOfLogFiles = DebugFlags.extraDebugLogs ? 32 : 3
let fileLogger = DDFileLogger(logFileManager: logFileManager)
fileLogger.rollingFrequency = kDayInterval
fileLogger.maximumFileSize = 3 * 1024 * 1024
fileLogger.logFormatter = ScrubbingLogFormatter()
self.fileLogger = fileLogger
DDLog.add(fileLogger)
}
public func disableFileLogging() {
guard let fileLogger else { return }
DDLog.remove(fileLogger)
self.fileLogger = nil
}
public func enableTTYLoggingIfNeeded() {
#if DEBUG
guard let ttyLogger = DDTTYLogger.sharedInstance else { return }
ttyLogger.logFormatter = LogFormatter()
DDLog.add(ttyLogger)
#endif
}
// MARK: - Handlers
public static func registerLibsignal() {
LibsignalLoggerImpl().setUpLibsignalLogging(level: { () -> LibsignalLogLevel in
if ShouldLogVerbose() {
return .trace
}
if ShouldLogDebug() {
return .debug
}
if ShouldLogInfo() {
return .info
}
if ShouldLogWarning() {
return .warn
}
return .error
}())
}
public static func registerRingRTC() {
let maxLogLevel: RingRTCLogLevel
#if DEBUG
if
let overrideLogLevelString = ProcessInfo().environment["RINGRTC_MAX_LOG_LEVEL"],
let overrideLogLevelRaw = UInt8(overrideLogLevelString),
let overrideLogLevel = RingRTCLogLevel(rawValue: overrideLogLevelRaw)
{
maxLogLevel = overrideLogLevel
} else {
maxLogLevel = .trace
}
#else
maxLogLevel = .trace
#endif
RingRTCLoggerImpl(maxLogLevel: maxLogLevel).setUpRingRTCLogging(maxLogLevel: min(maxLogLevel, { () -> RingRTCLogLevel in
if ShouldLogVerbose() {
return .trace
}
if ShouldLogDebug() {
return .debug
}
if ShouldLogInfo() {
return .info
}
if ShouldLogWarning() {
return .warn
}
return .error
}()))
}
}
private extension LibsignalLogLevel {
var logFlag: DDLogFlag {
switch self {
case .error: return .error
case .warn: return .warning
case .info: return .info
case .debug: return .debug
case .trace: return .verbose
}
}
}
final class LibsignalLoggerImpl: LibsignalLogger {
func log(level: LibsignalLogLevel, file: UnsafePointer<CChar>?, line: UInt32, message: UnsafePointer<CChar>) {
Logger.log(
String(cString: message),
flag: level.logFlag,
file: file.map(String.init(cString:)) ?? "",
function: "",
line: Int(line)
)
}
func flush() {
Logger.flush()
}
}
private extension RingRTCLogLevel {
var logFlag: DDLogFlag {
switch self {
case .error: return .error
case .warn: return .warning
case .info: return .info
case .debug: return .debug
case .trace: return .verbose
}
}
}
final class RingRTCLoggerImpl: RingRTCLogger {
private nonisolated let maxLogLevel: RingRTCLogLevel
init(maxLogLevel: RingRTCLogLevel) {
self.maxLogLevel = maxLogLevel
}
func log(level: RingRTCLogLevel, file: String, function: String, line: UInt32, message: String) {
guard level <= maxLogLevel else {
return
}
Logger.log(
message,
flag: level.logFlag,
file: file,
function: function,
line: Int(line)
)
}
func flush() {
Logger.flush()
}
}