From adb34c13177969672bc3f619bd5761a2da311429 Mon Sep 17 00:00:00 2001 From: Dan Fabulich Date: Sat, 25 Apr 2026 01:16:45 -0700 Subject: [PATCH] Log stacktraces This way, you can `catch { logger.error("There was an error: \(error)") }` and log the stack trace in logcat --- Sources/SkipFoundation/Logger.swift | 135 +++++++++++++++--- .../Foundation/LoggerTests.swift | 14 ++ 2 files changed, 130 insertions(+), 19 deletions(-) diff --git a/Sources/SkipFoundation/Logger.swift b/Sources/SkipFoundation/Logger.swift index 43b33b0..d9e3999 100644 --- a/Sources/SkipFoundation/Logger.swift +++ b/Sources/SkipFoundation/Logger.swift @@ -4,9 +4,66 @@ /// Skip `Logger` aliases to `SkipLogger` type and wraps `java.util.logging.Logger` public typealias Logger = SkipLogger -public typealias LogMessage = String public typealias OSLogType = SkipLogger.LogType +/// Message wrapper that mirrors Apple-style logger entrypoints while allowing +/// platform-specific interpolation behavior on Android. +public struct LogMessage : ExpressibleByStringInterpolation, CustomStringConvertible, Sendable { + #if SKIP + // Mirror existing Skip pattern to avoid Kotlin unresolved nested interpolation type. + public typealias StringInterpolation = LogMessage.StringInterpolation + #endif + + public let description: String + + public init(stringLiteral value: String) { + self.description = value + } + + public init(stringInterpolation: StringInterpolation) { + self.description = stringInterpolation.description + } + + public struct StringInterpolation : StringInterpolationProtocol, Sendable { + var parts: [String] = [] + + public var description: String { + parts.joined() + } + + public init(literalCapacity: Int, interpolationCount: Int) { + parts.reserveCapacity(interpolationCount * 2 + 1) + } + + public mutating func appendLiteral(_ literal: String) { + parts.append(literal) + } + + public mutating func appendInterpolation(_ value: T) { + parts.append(String(describing: value)) + } + + public mutating func appendInterpolation(_ error: any Error) { + parts.append(Self.renderedError(error)) + } + + private static func renderedError(_ error: any Error) -> String { + if let throwable = error as? java.lang.Throwable { + return throwableStackTraceString(throwable) + } + return String(describing: error) + } + + private static func throwableStackTraceString(_ throwable: java.lang.Throwable) -> String { + let writer = java.io.StringWriter() + let printWriter = java.io.PrintWriter(writer) + throwable.printStackTrace(printWriter) + printWriter.flush() + return writer.toString() + } + } +} + /// Logger cover for versions before Logger was available (which coincides with Concurrency). public class SkipLogger { let logName: String @@ -38,77 +95,117 @@ public class SkipLogger { } } + public func log(level: OSLogType, _ message: String) { + log(level: level, LogMessage(stringLiteral: message)) + } + public func log(_ message: LogMessage) { do { - android.util.Log.i(logName, message) + android.util.Log.i(logName, message.description) } catch { - java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.INFO, message) + java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.INFO, message.description) } } + public func log(_ message: String) { + log(LogMessage(stringLiteral: message)) + } + public func trace(_ message: LogMessage) { do { - android.util.Log.v(logName, message) + android.util.Log.v(logName, message.description) } catch { - java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.FINER, message) + java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.FINER, message.description) } } + public func trace(_ message: String) { + trace(LogMessage(stringLiteral: message)) + } + public func debug(_ message: LogMessage) { do { - android.util.Log.d(logName, message) + android.util.Log.d(logName, message.description) } catch { - java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.FINE, message) + java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.FINE, message.description) } } + public func debug(_ message: String) { + debug(LogMessage(stringLiteral: message)) + } + public func info(_ message: LogMessage) { do { - android.util.Log.i(logName, message) + android.util.Log.i(logName, message.description) } catch { - java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.INFO, message) + java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.INFO, message.description) } } + public func info(_ message: String) { + info(LogMessage(stringLiteral: message)) + } + public func notice(_ message: LogMessage) { do { - android.util.Log.i(logName, message) + android.util.Log.i(logName, message.description) } catch { - java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.CONFIG, message) + java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.CONFIG, message.description) } } + public func notice(_ message: String) { + notice(LogMessage(stringLiteral: message)) + } + public func warning(_ message: LogMessage) { do { - android.util.Log.w(logName, message) + android.util.Log.w(logName, message.description) } catch { - java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.WARNING, message) + java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.WARNING, message.description) } } + public func warning(_ message: String) { + warning(LogMessage(stringLiteral: message)) + } + public func error(_ message: LogMessage) { do { - android.util.Log.e(logName, message) + android.util.Log.e(logName, message.description) } catch { - java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.SEVERE, message) + java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.SEVERE, message.description) } } + public func error(_ message: String) { + error(LogMessage(stringLiteral: message)) + } + public func critical(_ message: LogMessage) { do { - android.util.Log.wtf(logName, message) + android.util.Log.wtf(logName, message.description) } catch { - java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.SEVERE, message) + java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.SEVERE, message.description) } } + public func critical(_ message: String) { + critical(LogMessage(stringLiteral: message)) + } + public func fault(_ message: LogMessage) { do { - android.util.Log.wtf(logName, message) + android.util.Log.wtf(logName, message.description) } catch { - java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.SEVERE, message) + java.util.logging.Logger.getLogger(logName).log(java.util.logging.Level.SEVERE, message.description) } } + + public func fault(_ message: String) { + fault(LogMessage(stringLiteral: message)) + } } #endif diff --git a/Tests/SkipFoundationTests/Foundation/LoggerTests.swift b/Tests/SkipFoundationTests/Foundation/LoggerTests.swift index 63aa747..b649c1e 100644 --- a/Tests/SkipFoundationTests/Foundation/LoggerTests.swift +++ b/Tests/SkipFoundationTests/Foundation/LoggerTests.swift @@ -23,4 +23,18 @@ final class LoggerTests: XCTestCase { public func testLogError() { logger.error("logger error test") } + + #if SKIP + public func testLogMessageInterpolatesThrowableStackTrace() { + var message: LogMessage = "" + do { + throw java.lang.IllegalStateException("test failure") + } catch { + message = "logger throwable test: \(error)" + } + XCTAssertTrue(message.description.contains("IllegalStateException")) + XCTAssertTrue(message.description.contains("test failure")) + XCTAssertTrue(message.description.contains("testLogMessageInterpolatesThrowableStackTrace")) + } + #endif }