Traversio

Diagnostics

Capture copyable SSH failure details and recent log events in an app-friendly way.

Diagnostics are part of normal application integration. The most useful inputs for support and debugging are:

  • structured SSHClientLogEvent values collected during connection setup and later operations
  • a copyable SSHConnectionFailure.diagnosticReport or SSHOperationFailure.diagnosticReport

Keep this pattern close to the code that opens a connection or starts an operation, then surface the exported text through your own UI or support flow.

Available Diagnostics

Traversio already exposes the stable pieces needed for a support export:

  • SSHClientLogRecorder for a bounded in-memory recent-event buffer
  • SSHClientLogHandler.recorder(_:minimumLevel:) or SSHClientLogRecorder.logHandler(minimumLevel:) to wire that buffer into the public connection entry points
  • SSHClientLogHandler.sink(...) to receive structured events
  • SSHClientLogEvent.formattedLine for plain-text export without inventing a formatter first
  • SSHConnectionFailure.diagnosticReport for connection-setup failures
  • SSHOperationFailure.diagnosticReport for post-auth failures such as session, forwarding, or SFTP work

Those reports include the endpoint, stage or scope, negotiated algorithms when known, remote disconnect/debug details, channel IDs when relevant, and SFTP status details when the server returned them.

ProxyJump failures are easier to localize because the connection log stream records:

  • when the overall ProxyJump setup starts
  • when each hop starts
  • when Traversio opens the nested direct-tcpip channel
  • when the final target connection begins

Built-In Recorder

Use SSHClientLogRecorder when the app needs a bounded recent-event buffer for support export:

import Traversio

let diagnostics = SSHClientLogRecorder(maximumEventCount: 80)
let logHandler = diagnostics.logHandler(minimumLevel: .info)

The recorder keeps the newest events, drops older ones once the buffer is full, and exposes that truncation through SSHClientLogRecorderSnapshot.droppedEventCount.

If you prefer the factory on the handler type, this is equivalent:

let diagnostics = SSHClientLogRecorder(maximumEventCount: 80)
let logHandler = SSHClientLogHandler.recorder(
    diagnostics,
    minimumLevel: .info
)

snapshot() gives you:

  • events
  • formattedLines
  • formattedText
  • droppedEventCount
  • diagnosticReport(for:)

Custom Pipelines

If you already have your own logging stack, keep using SSHClientLogHandler.sink(...) and forward the same event into both places:

import Traversio

let diagnostics = SSHClientLogRecorder(maximumEventCount: 80)

let logHandler = SSHClientLogHandler.sink(minimumLevel: .info) { event in
    diagnostics.record(event)
    myLogger.write(event.formattedLine)
}

That keeps Traversio's bounded support-export buffer and your app's existing log pipeline in sync.

Exporting A Support Payload

This is the smallest useful flow for a copy button, a bug-report sheet, or a support attachment:

import Traversio

@available(macOS 26.0, iOS 26.0, *)
func runCheckedCommand(configuration: SSHClientConfiguration) async {
    let diagnostics = SSHClientLogRecorder(maximumEventCount: 80)
    let logHandler = diagnostics.logHandler(minimumLevel: .info)

    do {
        try await SSHClient.withConnection(
            configuration: configuration,
            logHandler: logHandler
        ) { connection in
            _ = try await connection.execute("uptime")
        }
    } catch {
        if let report = diagnostics.diagnosticReport(for: error) {
            print(report)
        }
    }
}

If you only want the log lines:

let logText = diagnostics.snapshot().formattedText

You can also export only the wrapped failure when recent events are optional:

import Traversio

func diagnosticText(from error: Error) -> String? {
    guard let clientError = error as? SSHClientError else {
        return nil
    }

    switch clientError {
    case let .connectionFailed(failure):
        return failure.diagnosticReport
    case let .operationFailed(failure):
        return failure.diagnosticReport
    default:
        return nil
    }
}

Placement In Your App

The most practical place to wire this up is the boundary where your app creates SSHClientConfiguration and starts the connection or operation. That captures:

  • setup failures such as TCP, banner, key exchange, host-trust, auth, timeout, or ProxyJump issues
  • later failures such as channel startup, forwarding, or SFTP request errors

Surface the exported text through your own UI. A typical pattern is:

  1. keep the last 50-100 log events in memory
  2. when an operation fails, call diagnosticReport(for:) on the recorder or on its snapshot
  3. offer a single "Copy Diagnostic Report" action to the user

On this page