From 9be0b678c326fe8ea9da69e31fe51f1567f72ca8 Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:55:17 -0500 Subject: [PATCH] Add latency diagnostic to tsh (#35679) The new `tsh latency` command allows visualizing latency to resources in real time. Currently, supported is limited to SSH instances, but may be extended to anything that implements a `latency.Pinger` to support other protocols in the future. --- api/client/proxy/client.go | 13 ++ go.mod | 29 ++-- go.sum | 30 +++- lib/utils/diagnostics/latency/monitor.go | 12 +- lib/web/terminal.go | 2 +- tool/tsh/common/latency.go | 196 +++++++++++++++++++++++ tool/tsh/common/tsh.go | 51 ++++++ 7 files changed, 311 insertions(+), 22 deletions(-) create mode 100644 tool/tsh/common/latency.go diff --git a/api/client/proxy/client.go b/api/client/proxy/client.go index d6ae2320589..098d0e418ef 100644 --- a/api/client/proxy/client.go +++ b/api/client/proxy/client.go @@ -33,6 +33,7 @@ import ( "github.com/gravitational/teleport/api/breaker" "github.com/gravitational/teleport/api/client" + authpb "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/client/proxy/transport/transportv1" "github.com/gravitational/teleport/api/defaults" transportv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/transport/v1" @@ -416,3 +417,15 @@ func (c *Client) ClusterDetails(ctx context.Context) (ClusterDetails, error) { return ClusterDetails{FIPS: details.FipsEnabled}, nil } + +// Ping measures the round trip latency of sending a message to the Proxy. +func (c *Client) Ping(ctx context.Context) error { + // TODO(tross): Update to call Ping when it is added to the transport service. + // For now we don't really care what method is used we just want to measure + // how long it takes to get a reply. This will always fail with a not implemented + // error since the Proxy gRPC server doesn't serve the auth service proto. However, + // we use it because it's already imported in the api package. + clt := authpb.NewAuthServiceClient(c.grpcConn) + _, _ = clt.Ping(ctx, &authpb.PingRequest{}) + return nil +} diff --git a/go.mod b/go.mod index 11fd1977a78..008a945d4a4 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/ClickHouse/ch-go v0.61.0 github.com/ClickHouse/clickhouse-go/v2 v2.16.0 github.com/HdrHistogram/hdrhistogram-go v1.1.2 + github.com/Masterminds/sprig/v3 v3.2.3 github.com/Microsoft/go-winio v0.6.1 github.com/ThalesIgnite/crypto11 v1.2.5 github.com/alecthomas/kingpin/v2 v2.3.2 // replaced @@ -60,12 +61,16 @@ require ( github.com/beevik/etree v1.2.0 github.com/bufbuild/connect-go v1.10.0 github.com/buildkite/bintest/v3 v3.2.0 + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.9.1 github.com/coreos/go-oidc v2.2.1+incompatible // replaced github.com/coreos/go-semver v0.3.1 github.com/creack/pty v1.1.21 github.com/crewjam/saml v0.4.14 github.com/datastax/go-cassandra-native-protocol v0.0.0-20220706104457-5e8aad05cf90 github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 + github.com/distribution/reference v0.5.0 github.com/dustin/go-humanize v1.0.1 github.com/elastic/go-elasticsearch/v8 v8.11.1 github.com/flynn/hid v0.0.0-20190502022136-f1b9b6cc019a @@ -87,6 +92,7 @@ require ( github.com/google/btree v1.1.2 github.com/google/go-attestation v0.5.1 github.com/google/go-cmp v0.6.0 + github.com/google/go-containerregistry v0.17.0 github.com/google/go-querystring v1.1.0 github.com/google/go-tpm-tools v0.4.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 @@ -101,6 +107,7 @@ require ( github.com/gravitational/trace v1.3.1 github.com/gravitational/ttlmap v0.0.0-20171116003245-91fd36b9004c github.com/grpc-ecosystem/go-grpc-middleware/providers/openmetrics/v2 v2.0.0-rc.3 + github.com/guptarohit/asciigraph v0.5.6 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/icza/mjpeg v0.0.0-20220812133530-f79265a232f2 @@ -128,6 +135,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/moby/term v0.5.0 github.com/okta/okta-sdk-golang/v2 v2.20.0 + github.com/opencontainers/go-digest v1.0.0 github.com/opensearch-project/opensearch-go/v2 v2.3.0 github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 github.com/pelletier/go-toml v1.9.5 @@ -143,6 +151,8 @@ require ( github.com/schollz/progressbar/v3 v3.14.1 github.com/segmentio/parquet-go v0.0.0-20230622230624-510764ae9e80 github.com/sethvargo/go-diceware v0.3.0 + github.com/sigstore/cosign/v2 v2.2.2 + github.com/sigstore/sigstore v1.7.6 github.com/sijms/go-ora/v2 v2.7.9 github.com/sirupsen/logrus v1.9.3 github.com/snowflakedb/gosnowflake v1.7.1 @@ -208,15 +218,6 @@ require ( github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f // indirect ) -require ( - github.com/Masterminds/sprig/v3 v3.2.3 - github.com/distribution/reference v0.5.0 - github.com/google/go-containerregistry v0.17.0 - github.com/opencontainers/go-digest v1.0.0 - github.com/sigstore/cosign/v2 v2.2.2 - github.com/sigstore/sigstore v1.7.6 -) - require ( cloud.google.com/go v0.110.10 // indirect cloud.google.com/go/longrunning v0.5.4 // indirect @@ -249,6 +250,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -256,6 +258,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf // indirect @@ -360,12 +363,14 @@ require ( github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/lithammer/dedent v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect @@ -381,6 +386,10 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/montanaflynn/stats v0.7.0 // indirect github.com/mtibben/percent v0.2.1 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect diff --git a/go.sum b/go.sum index 905fc48b859..8294dbe570f 100644 --- a/go.sum +++ b/go.sum @@ -277,6 +277,8 @@ github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin v0.0.0-2022033116504 github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw= github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= @@ -319,6 +321,12 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -339,6 +347,8 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -793,6 +803,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8 github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/guptarohit/asciigraph v0.5.6 h1:0tra3HEhfdj1sP/9IedrCpfSiXYTtHdCgBhBL09Yx6E= +github.com/guptarohit/asciigraph v0.5.6/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= @@ -1023,6 +1035,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -1064,10 +1078,13 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= @@ -1134,6 +1151,14 @@ github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -1293,6 +1318,7 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= diff --git a/lib/utils/diagnostics/latency/monitor.go b/lib/utils/diagnostics/latency/monitor.go index ff7781b5ed4..363e39a59d2 100644 --- a/lib/utils/diagnostics/latency/monitor.go +++ b/lib/utils/diagnostics/latency/monitor.go @@ -226,23 +226,17 @@ type SSHClient interface { // SSH connection. To calculate round trip time, a keepalive@openssh.com request // is sent. type SSHPinger struct { - clt SSHClient - clock clockwork.Clock + clt SSHClient } // NewSSHPinger creates a new [SSHPinger] with the provided configuration. -func NewSSHPinger(clock clockwork.Clock, clt SSHClient) (*SSHPinger, error) { +func NewSSHPinger(clt SSHClient) (*SSHPinger, error) { if clt == nil { return nil, trace.BadParameter("ssh client not provided to SSHPinger") } - if clock == nil { - clock = clockwork.NewRealClock() - } - return &SSHPinger{ - clt: clt, - clock: clock, + clt: clt, }, nil } diff --git a/lib/web/terminal.go b/lib/web/terminal.go index 79d4587ce81..e3453ea8145 100644 --- a/lib/web/terminal.go +++ b/lib/web/terminal.go @@ -712,7 +712,7 @@ func monitorSessionLatency(ctx context.Context, clock clockwork.Clock, stream *W return trace.Wrap(err, "creating websocket pinger") } - sshPinger, err := latency.NewSSHPinger(clock, sshClient) + sshPinger, err := latency.NewSSHPinger(sshClient) if err != nil { return trace.Wrap(err, "creating ssh pinger") } diff --git a/tool/tsh/common/latency.go b/tool/tsh/common/latency.go new file mode 100644 index 00000000000..6089f58c7bd --- /dev/null +++ b/tool/tsh/common/latency.go @@ -0,0 +1,196 @@ +// Teleport +// Copyright (C) 2023 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package common + +import ( + "context" + "fmt" + "time" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/gravitational/trace" + "github.com/guptarohit/asciigraph" + + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/diagnostics/latency" +) + +// keyMap is used to display the help text at +// the bottom of the screen. +type keyMap struct { + quit key.Binding +} + +func (k keyMap) ShortHelp() []key.Binding { + return []key.Binding{k.quit} +} + +func (k keyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{{k.quit}} +} + +var helpKeys = keyMap{ + quit: key.NewBinding( + key.WithKeys("q", "esc", "ctrl+c"), + key.WithHelp("q", "quit"), + ), +} + +// latencyModel is a [tea.Model] that contains the state required +// to display the latency graph. +type latencyModel struct { + ready bool + h, w int + help help.Model + clientLabel, serverLabel string + clientLegend, serverLegend viewport.Model + clientMax, serverMax int64 + clientLatency, serverLatency *utils.CircularBuffer +} + +// newLatencyModel constructs a new latencyModel. +func newLatencyModel(clientLabel, serverLabel string) (*latencyModel, error) { + clientStats, err := utils.NewCircularBuffer(50) + if err != nil { + return nil, trace.Wrap(err) + } + + serverStats, err := utils.NewCircularBuffer(50) + if err != nil { + return nil, trace.Wrap(err) + } + + return &latencyModel{ + clientLabel: clientLabel, + serverLabel: serverLabel, + help: help.New(), + clientLegend: viewport.New(1, 2), + serverLegend: viewport.New(1, 2), + clientLatency: clientStats, + serverLatency: serverStats, + }, nil +} + +// Init is a noop but required to implement [tea.Model]. +func (m *latencyModel) Init() tea.Cmd { + return nil +} + +// Update reacts to screen resizing and new latency statistics. +func (m *latencyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + } + case tea.WindowSizeMsg: + m.ready = true + m.h = msg.Height + m.w = msg.Width + m.clientLegend.Width = m.w / 2 + m.serverLegend.Width = m.clientLegend.Width + case latency.Statistics: + m.clientLatency.Add(float64(msg.Client)) + m.serverLatency.Add(float64(msg.Server)) + m.clientMax = max(m.clientMax, msg.Client) + m.serverMax = max(m.serverMax, msg.Server) + m.clientLegend.SetContent(fmt.Sprintf("%s: last recorded %dms (max %dms)\t", m.clientLabel, msg.Client, m.clientMax)) + m.serverLegend.SetContent(fmt.Sprintf("%s: last recorded %dms (max %dms)", m.serverLabel, msg.Server, m.serverMax)) + } + return m, nil +} + +var ( + // clientColor is the color of the client plot and the client information in the legend. + clientColor = asciigraph.Blue + // serverColor is the color of the server plot and the client information in the legend. + serverColor = asciigraph.Goldenrod + clientStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(fmt.Sprintf("%d", clientColor))) + serverStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(fmt.Sprintf("%d", serverColor))) +) + +// View renders the current latency graph to be displayed. +func (m *latencyModel) View() string { + if !m.ready { + return "" + } + + clientData := m.clientLatency.Data(150) + serverData := m.serverLatency.Data(150) + + if clientData == nil || serverData == nil { + return "" + } + + legend := lipgloss.JoinHorizontal( + lipgloss.Top, + clientStyle.Render(m.clientLegend.View()), + serverStyle.Render(m.serverLegend.View()), + ) + + plot := asciigraph.PlotMany( + [][]float64{clientData, serverData}, + asciigraph.Height(m.h-4), + asciigraph.Width(m.w), + asciigraph.SeriesColors(clientColor, serverColor), + asciigraph.Caption(legend), + ) + + return lipgloss.JoinVertical(lipgloss.Top, plot, lipgloss.PlaceHorizontal(m.w, lipgloss.Bottom, m.help.View(helpKeys))) +} + +// showLatency creates a [latency.Monitor] using the provided [latency.Pinger]s to capture latency and +// render it to the terminal via a plot graph. +func showLatency(ctx context.Context, clientPinger, serverPinger latency.Pinger, clientLabel, serverLabel string) error { + m, err := newLatencyModel(clientLabel, serverLabel) + if err != nil { + return trace.Wrap(err) + } + + p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithContext(ctx), tea.WithoutSignalHandler()) + + monitor, err := latency.NewMonitor(latency.MonitorConfig{ + InitialPingInterval: time.Millisecond, + InitialReportInterval: 500 * time.Millisecond, + PingInterval: time.Second, + ReportInterval: time.Second, + ClientPinger: clientPinger, + ServerPinger: serverPinger, + Reporter: latency.ReporterFunc(func(ctx context.Context, stats latency.Statistics) error { + p.Send(stats) + return nil + }), + }) + if err != nil { + return trace.Wrap(err) + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + go monitor.Run(ctx) + + if _, err := p.Run(); err != nil { + return trace.Wrap(err) + } + + return nil +} diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index da863adba33..baac20f5c2a 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -86,6 +86,7 @@ import ( "github.com/gravitational/teleport/lib/sshutils/x11" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/diagnostics/latency" "github.com/gravitational/teleport/lib/utils/mlock" "github.com/gravitational/teleport/tool/common" ) @@ -952,6 +953,13 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { // logout deletes obtained session certificates in ~/.tsh logout := app.Command("logout", "Delete a cluster certificate.") + // latency + latency := app.Command("latency", "Run latency diagnostics.").Hidden() + + latencySSH := latency.Command("ssh", "Measure latency to a particular SSH host.") + latencySSH.Arg("[user@]host", "Remote hostname and the login to use").Required().StringVar(&cf.UserHost) + latencySSH.Flag("cluster", clusterHelp).Short('c').StringVar(&cf.SiteName) + // bench bench := app.Command("bench", "Run Teleport benchmark tests.").Hidden() bench.Flag("cluster", clusterHelp).Short('c').StringVar(&cf.SiteName) @@ -1243,6 +1251,8 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { err = onVersion(&cf) case ssh.FullCommand(): err = onSSH(&cf) + case latencySSH.FullCommand(): + err = onSSHLatency(&cf) case benchSSH.FullCommand(): err = onBenchmark( &cf, @@ -3283,6 +3293,47 @@ func retryWithAccessRequest( return trace.Wrap(fn()) } +func onSSHLatency(cf *CLIConf) error { + tc, err := makeClient(cf) + if err != nil { + return trace.Wrap(err) + } + + clt, err := tc.ConnectToCluster(cf.Context) + if err != nil { + return trace.Wrap(err) + } + defer clt.Close() + + // detect the common error when users use host:port address format + _, port, err := net.SplitHostPort(tc.Host) + // client has used host:port notation + if err == nil { + return trace.BadParameter("please use ssh subcommand with '--port=%v' flag instead of semicolon", port) + } + + addr := net.JoinHostPort(tc.Host, strconv.Itoa(tc.HostPort)) + + nodeClient, err := tc.ConnectToNode( + cf.Context, + clt, + client.NodeDetails{Addr: addr, Namespace: tc.Namespace, Cluster: tc.SiteName}, + tc.Config.HostLogin, + ) + if err != nil { + tc.ExitStatus = 1 + return trace.Wrap(err) + } + defer nodeClient.Close() + + targetPinger, err := latency.NewSSHPinger(nodeClient.Client) + if err != nil { + return trace.Wrap(err) + } + + return trace.Wrap(showLatency(cf.Context, clt.ProxyClient, targetPinger, "Proxy", tc.Host)) +} + // onSSH executes 'tsh ssh' command func onSSH(cf *CLIConf) error { tc, err := makeClient(cf)