Logging & diagnostics

SSHLogLevel

public enum SSHLogLevel: Int, Sendable {
    case debug
    case info
    case warning
    case error
}

SSHLogEvent

public struct SSHLogEvent: Equatable, Sendable {
    public var level: SSHLogLevel
    public var phase: String
    public var message: String
    public var metadata: [String: String]
    public var timestamp: Date

    public init(
        level: SSHLogLevel,
        phase: String,
        message: String,
        metadata: [String: String] = [:],
        timestamp: Date = Date()
    )

    public var redacted: SSHLogEvent { get }
}

SSHLogHandler

public typealias SSHLogHandler = @Sendable (SSHLogEvent) -> Void

SSHLogRecorder

public final class SSHLogRecorder: @unchecked Sendable {
    public init(capacity: Int = 200)
    public func record(_ event: SSHLogEvent)
    public var events: [SSHLogEvent] { get }
}

Stored events are always redacted: record calls event.redacted before insertion. The buffer is a fixed-capacity ring — oldest events drop when full.

SSHDiagnosticReport

public struct SSHDiagnosticReport: Equatable, Sendable {
    public var phase: String
    public var host: String
    public var port: UInt16
    public var username: String
    public var authentication: String
    public var hostKeyPolicy: String
    public var metadata: [String: String]
    public var recentEvents: [SSHLogEvent]
    public var generatedAt: Date

    public init(
        phase: String,
        host: String,
        port: UInt16,
        username: String,
        authentication: String,
        hostKeyPolicy: String,
        metadata: [String: String] = [:],
        recentEvents: [SSHLogEvent] = [],
        generatedAt: Date = Date()
    )
}

The initializer applies the same redaction policy to metadata and the embedded recentEvents.

SSHPortLatencyProbe

public enum SSHPortLatencyProbe {
    public static func measure(
        configuration: SSHClientConfiguration,
        serviceCommand: String = "true"
    ) async throws -> SSHPortLatencyReport
}

Async-first; a callback overload exists (callbackQueue: + completion:) for callers that need it.

SSHPortLatencyReport

public struct SSHPortLatencyReport: Equatable, Sendable {
    public var host: String
    public var port: UInt16
    public var route: SSHPortLatencyRoute
    public var connectDuration: TimeInterval
    public var sshServiceDuration: TimeInterval
    public var totalDuration: TimeInterval
}

SSHPortLatencyRoute

public enum SSHPortLatencyRoute: Equatable, Sendable {
    case direct
    case socks5(host: String, port: UInt16)
    case httpConnect(host: String, port: UInt16)
    case proxyJump(host: String, port: UInt16)
}