Table of Contents

Logging and diagnostics

SshSharp reports what it does through the standard .NET channels: ILogger for logs, an ActivitySource for traces and a Meter for metrics, all named SshSharp. None of them ever contain passwords, private keys, command text, user names or host names.

When to use

  • A connection fails and the exception message is not enough: log at Debug.
  • You run SshSharp in a service: export traces and metrics with OpenTelemetry.
  • You need to know what a server negotiated (for an audit, or to find hosts without post-quantum key exchange).

Log

using var loggerFactory = LoggerFactory.Create(builder => builder
    .AddSimpleConsole(o => o.TimestampFormat = "HH:mm:ss ")
    .SetMinimumLevel(LogLevel.Debug));

await using var client = await SshClient.ConnectAsync("example.com", 22, new SshClientOptions
{
    UserName = "deploy",
    Password = Environment.GetEnvironmentVariable("DEPLOY_PASSWORD"),
    HostKeyVerifier = verifier,
    Logger = loggerFactory.CreateLogger("SshSharp"),   // never logs passwords, keys, command text or user names
}, cancellationToken);

At Information: connected (algorithms and host key fingerprint), authenticated (which method), identity files and agent keys that were skipped and why, host keys recorded by UpdateHostKeys, and connections without post-quantum key exchange. At Debug: key exchanges and rekeys, channels and sessions, each authentication attempt.

With dependency injection, clients log through the container's ILoggerFactory automatically, under the category SshSharp.

Inspect a connection

Console.WriteLine($"server:    {client.ServerIdentification}");                      // SSH-2.0-OpenSSH_9.9 ...
Console.WriteLine($"host key:  {client.HostKey.KeyType} {client.HostKey.Fingerprint}");
Console.WriteLine($"kex:       {client.Algorithms.KeyExchange} (post-quantum: {client.Algorithms.IsPostQuantum})");
Console.WriteLine($"cipher:    {client.Algorithms.EncryptionClientToServer}");
Console.WriteLine($"strict kex: {client.Algorithms.StrictKeyExchange}");
Console.WriteLine($"round trip: {(await client.PingAsync(cancellationToken)).TotalMilliseconds:F1} ms");
Console.WriteLine($"client:    {SshSharpInfo.SoftwareVersion}");                       // SshSharp_1.0.0

Trace and measure with OpenTelemetry

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing.AddSource(SshTelemetry.SourceName))
    .WithMetrics(metrics => metrics.AddMeter(SshTelemetry.SourceName));

The SshSharp.Connect span covers connecting, key exchange and authentication (each ProxyJump hop is a span of its own inside it), and SshSharp.Command covers each RunCommandAsync. The metrics count connections, channels, commands, bytes and key exchanges, and time connecting. Errors and diagnostics lists every tag and instrument.

Listen without OpenTelemetry

The BCL's listeners see the same data, for a quick look in a console tool or a test:

// Without OpenTelemetry: the connect and command spans, straight from the ActivitySource.
var listener = new ActivityListener
{
    ShouldListenTo = source => source.Name == SshTelemetry.SourceName,
    Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
    ActivityStopped = activity => Console.WriteLine(
        $"{activity.OperationName} {activity.Duration.TotalMilliseconds:F0} ms {activity.Status} " +
        string.Join(' ', activity.Tags.Select(t => $"{t.Key}={t.Value}"))),
};
ActivitySource.AddActivityListener(listener);
var listener = new MeterListener
{
    InstrumentPublished = (instrument, l) =>
    {
        if (instrument.Meter.Name == SshTelemetry.SourceName)
        {
            l.EnableMeasurementEvents(instrument);
        }
    },
};
listener.SetMeasurementEventCallback<long>((instrument, value, tags, _) => Console.WriteLine($"{instrument.Name} {value:+#;-#;0}"));
listener.SetMeasurementEventCallback<double>((instrument, value, tags, _) => Console.WriteLine($"{instrument.Name} {value:F3}"));
listener.Start();

Notes and pitfalls

  • Host names are not recorded, by design (see the threat model): add server.address to a parent span of your own when you want it.
  • Nothing is recorded unless something listens, so the instrumentation costs nothing when unused.
  • Exception messages can contain what the server said (an SFTP error, a refused command). Treat them as server-controlled text before showing them to end users.
  • Comparing with ssh -vvv. The Debug log shows the same stages (key exchange, host key, authentication methods), so it is the place to look when ssh connects and SshSharp does not.

See also